Существует большая разница между созданием модели машинного обучения, которая работает на вашем компьютере, и созданием этой модели, доступной для использования другими. При плохой реализации ваши пользователи будут разочарованы тем, что ваше программное обеспечение ненадежно. И это может занять месяцы, чтобы реализовать это хорошо!

Моя предыдущая статья поделилась принципами, которые я усвоил за эти годы. В этом примере рассматривается код и критикуется подход. Когда я начал внедрять модели машинного обучения в веб-сервисы, меня смущала сложность, и я надеюсь, что это пошаговое руководство поможет читателям в подобных ситуациях.

Я создал серию прототипов, чтобы изучить различные инструменты и идеи в MLOps для базового текстового классификатора. Этот пост будет посвящен самому последнему прототипу. Он автоматизировал обучение, тестирование и развертывание моделей в веб-сервисе с помощью AWS Lambda.

Я ограничил количество специфичных для машинного обучения инструментов по двум причинам: 1) я хотел использовать инфраструктуру, аналогичную традиционным программным проектам, и 2) на работе часто было проще соблюдать требования соответствия таким образом.

Примечание. Вы можете заметить, что некоторым из этих репозиториев уже несколько лет. Изначально я делал их только для себя. С годами я обнаружил, что часто использовал их в качестве примеров для других, и это побудило меня наконец написать об этом.

Основы

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

Однако с таким подходом возникает много проблем. Вот некоторые из этих проблем:

  • Это означает, что нам нужно будет тратить гораздо больше времени только на управление серверами, например, на установку исправлений безопасности, обеспечение того, чтобы интернет и питание не отключались, или на ремонт в случае поломки аппаратного компонента.
  • Процесс подвержен ошибкам и ошибкам, например, код, который работает на вашем компьютере, но не на сервере. Или вы можете забыть запустить тесты.
  • Служба под рабочим столом может обрабатывать только определенное количество запросов в секунду. Если ваш сайт внезапно станет популярным, он может не справиться с потоком пользователей.

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

Развертывание моделей в Lambda

В этом репозитории есть обучение и обслуживание модели scikit-learn для классификации новостей, но оно должно работать для PyTorch и других библиотек.

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

По мере изучения репозитория я буду обобщать терминологию в конце каждого раздела.

Шлюз API и Lambda

HTTP-запрос принимается шлюзом API и перенаправляется в Lambda. Если доступен рабочий процесс Lambda, он перенаправляет запрос рабочему процессу. Если нет, он запускает новый рабочий процесс, который загрузит код, загрузит модель и выполнит запрос. Если загрузка кода и модели занимает более 30 секунд, запрос завершится ошибкой, но в конце концов он загрузится, и тогда запросы будут работать.

Для тестирования я использовал запрос POST с такой простой полезной нагрузкой:

{
    "text": "The Seattle Seahawks crushed the Cleveland Browns on Thursday 52-7"
}

Шлюз API получает запрос и вызывает наш обработчик Lambda с информацией о запросе в параметрах.

В лямбда-коде API-шлюз отправляет эту информацию в двух словах: event и context. Из serve/app/app.py:

def lambda_handler(event, context):
    request_body = json.loads(event["body"])
    prediction = model.predict([request_body["text"]])[0]

    return {
        "statusCode": 200,
        "body": json.dumps({
            "response": prediction,
            "request": request_body
        })
    }

Шлюз API отправляет тело запроса POST в событии ["тело"] в виде простой строки, поэтому нам нужно проанализировать в нем JSON. Затем мы прогоняем текстовое поле через модель. Обратите внимание, что нам нужно превратить его в список входных данных (с одним элементом), а затем взять первый прогноз из выходных данных. Это связано с тем, что методы прогнозирования scikit-learn предназначены для пакетов данных. Затем API-шлюзу требуется ответ в определенной форме, включая код состояния HTTP (200). По привычке я также люблю возвращать данные запроса, чтобы упростить отладку.

Новая терминология

  • AWS Lambda: запускает наш код на сервере и автоматически увеличивает количество рабочих процессов.
  • AWS API Gateway: служит мостом между нашей лямбдой и запросами, поступающими из Интернета.
  • HTTP POST: это метод HTTP, который мы используем. Хотя GET был бы более подходящим семантически методом, отправка JSON обычно не поддерживается с помощью GET. По моему опыту, POST более распространен, потому что структура входных данных часто сложна.

Docker и Python, вызываемые Lambda

Из сервировки/приложения/Dockerfile:

CMD ["app.lambda_handler"]

Это последняя строка Dockerfile, и она сообщает Lambda, что нужно вызвать функцию lambda_handler в app.py.

Когда Lambda запускается, она импортирует файл app.py, который запускает все, кроме функции, например загрузку модели в нашем случае. С Lambdas мы называем это холодным стартом. Затем он доступен до тех пор, пока рабочий процесс не будет закрыт. Модель запекается в том же образе Docker, что и код, и загружается так же, как и любой другой файл. Образ Docker создается и развертывается из действий Github, когда код объединяется с основным, что изменяет что-либо в папке serving. Точно так же конфигурация шлюза API и Lambda может быть обновлена ​​одновременно. Один из файлов в разделе serving — это сам файл модели, который хранится с использованием контроля версий данных (DVC). Если модель изменится при слиянии с main, образ Docker будет перестроен и повторно развернут с новой моделью.

Вот где модель загружается в serve/app/app.py без тайминга и метрик:

def load_model():
    ...
    model_path = os.path.join(os.path.dirname(__file__), "data/model.joblib.gz", )
    model = joblib.load(model_path)

    ...

    return model

model = load_model()

Все, что мы делаем, это вызываем joblib.load по пути к файлу. Это работает, потому что модель была сохранена с помощью joblib, а обслуживающий код имеет все зависимости, необходимые для модели, из scikit-learn.

Первая строка — убедиться, что путь к файлу указан относительно app.py, а не относительно того, откуда запускается app.py, потому что я не знаю, обеспечивает ли Lambda это согласованность. Я делаю это по привычке, потому что в прошлом я обжигался, когда код запускался из неожиданного каталога.

Этот файл модели запекается в образ Docker в serve/app/Dockerfile:

COPY data ./data

Это просто копирование папки serve/app/data в образ Docker с машины, на которой создается образ Docker. Если по какой-то причине нам нужна была другая модель, мы просто помещали ее в эту папку, и она также была доступна внутри Docker.

Новые термины

  • Холодный старт: когда нет немедленно доступных рабочих процессов Lambda, Lambda должна запустить один из них.
  • Docker: Docker — это способ упаковки кода, данных и зависимостей вместе таким образом, чтобы их можно было одинаково запускать на любом компьютере.
  • Контроль версий данных (DVC): я использую DVC для управления версиями больших файлов, таких как модели, потому что git плохо работает с большими файлами.
  • Github Actions: Github, который будет запускать наш код на своих серверах в течение короткого времени, вызванный изменениями в Github или по таймеру. Это бесплатно для мелкомасштабного использования.
  • основная ветвь: при разработке Github обычно помещают текущую версию кода в ветку с именем «основная» и создают другие ветки при написании нового кода.
  • слияние: когда изменения из одной ветки git включаются в другую ветку. В данном случае я говорю о слиянии кода из ветки разработки в основную ветку.

Дополнительная литература

Сборка и развертывание с помощью CDK из Github Actions

Образ Docker создается и загружается в AWS Elastic Container Registry (ECR) в CDK в serve/deployment/stacks/lambda_service.py:

handler = lambda_.DockerImageFunction(
    self,
    "ExampleTextClassifierHandler",
    code=lambda_.DockerImageCode.from_image_asset("../app"),
    timeout=cdk.Duration.seconds(60),
    memory_size=3008
)

Функция from_image_asset создает для нас образ Docker, который принимает в качестве параметра каталог, содержащий Dockerfile, который вы хотите создать. Я устанавливаю размер памяти на 3 ГБ, что, вероятно, больше, чем необходимо, но это поможет с задержкой, потому что Lambda масштабирует ЦП с большим объемом ОЗУ. 60-секундный тайм-аут находится на уровне Lambda. Имейте в виду, что шлюз API имеет независимый тайм-аут с максимальным значением 30 секунд.

Образ Docker необходим для Lambda из-за размера модели и размера зависимостей Python. В противном случае я бы использовал развертывание zip-файла Lambda, которое быстрее.

CDK — это платформа для инфраструктуры как кода (IaC). Это означает, что мы определяем нашу конфигурацию AWS в коде. Это помогает нам стандартизировать конфигурацию и развертывание, что снижает количество аварий. Поскольку он хранится в репозитории, это также означает, что мы можем использовать процесс запроса на вытягивание, чтобы получить экспертную оценку нашей конфигурации. Это также означает, что мы одновременно обновляем наш сервисный код и инфраструктуру, поэтому мы можем объединить новую модель, например, с увеличенным объемом памяти. И это также означает, что мы можем легче отменить изменения конфигурации. Кроме того, в качестве последнего преимущества CDK реализуется таким образом, что большинство развертываний выполняется практически без простоя.

Стек CDK запускается из .github/workflows/deploy_service.yml:

    - run: cdk deploy --require-approval never
      env:
        AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
        AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
        AWS_DEFAULT_REGION: us-west-2

Это запустит cdk deploy. Мы должны отключить интерактивные вопросы утверждения, иначе он зависнет. Другие части — это установка переменных среды для пользователя AWS IAM, скопированных из секретов действий Github, установленных в этом репозитории (я устанавливаю их вручную). Затем, когда CDK создаст ваш шлюз API, он отобразит автоматически сгенерированный URL-адрес в Github Actions.

Далее в файле deploy_service.yaml мы устанавливаем DVC и получаем модель:

    - run: pip install dvc[s3]
    - run: dvc pull
      env:
        AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
        AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}

Это также требует учетных данных AWS для чтения из корзины S3.

В самом верху файла мы видим, когда запускается рабочий процесс Github Action:

on:
  push:
    branches:
      - main
    paths:
    - 'serving/**'
    - '.github/workflows/deploy_service.yml'

Это запускает действие deploy_service.yaml при обновлении main (что происходит при слиянии PR) И при изменении чего-либо в разделе serve/ или при изменении самого deploy_service.yaml.

Новые термины

  • AWS Elastic Container Registry (ECR): место для загрузки образов Docker в AWS для использования в Lambda и других сервисах AWS.
  • Инфраструктура как код (IaC): определение инфраструктуры с помощью кода, а не нажатия кнопок в пользовательском интерфейсе.
  • AWS Cloud Development Kit (CDK): инструменты и библиотеки AWS для инфраструктуры в виде кода, который можно определить на Javascript или Python.
  • AWS S3: Файловое хранилище в AWS.
  • Пользователь AWS IAM: учетная запись компьютера или человека, который может выполнять действия в вашей учетной записи AWS в соответствии с соответствующими разрешениями. Обычно доступ осуществляется с помощью ключа доступа и секрета. Ключ доступа и секрет аналогичны имени пользователя и паролю, но для программного доступа.
  • Секреты действий Github: конфиденциальная конфигурация, которая хранится на Github, связанная с репозиторием и доступная для действия Github, например ключ доступа или пароль.
  • Рабочий процесс Github Action: серия шагов для запуска Github Actions, связанных с триггером, например изменением ветки. На каждый рабочий процесс приходится один файл yaml.
  • Pull request (PR): в Github принято разрабатывать новую ветку, а затем отправлять pull request (PR), который представляет собой запрос на слияние кода с другой веткой (обычно основной). Обычно партнер просматривает измененный код в PR, прежде чем одобрить или запросить изменения. Компании также часто ограничивают свои репозитории Github, чтобы запросы на вытягивание не могли быть объединены, пока кто-то не одобрит их.

Дополнительная литература

Автоматическое тестирование сервиса из Github Actions

Мы не хотим развертывать плохой код, поэтому у нас также есть автоматизированное тестирование.

Тесты, которые у меня есть, очень просты: 1) Проверка того, что конечная точка может работать без сбоев 2) Проверка того, что конечная точка падает, как и ожидалось, если входной текст отсутствует. Они находятся в serve/tests/test_lambda_handler.py:

class BasicTests(unittest.TestCase):
    def test_basic_path(self):
        lambda_handler({"body": json.dumps({"text": "example input"})}, None)

    def test_crash(self):
        self.assertRaises(BaseException, lambda_handler, {"body": json.dumps({"not_the_right_one": "example input"})}, None)

В этих тестах я вызываю lambda_handler напрямую без установки context, потому что функция все равно его не использует.

Они запускаются из .github/workflows/test_service.yml:

      - name: Run tests
        run: make test-service

«make test-service» определен в Makefile в корне репозитория:

test-service:
   PYTHONPATH=serving/app/ python serving/tests/test_lambda_handler.py

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

Действие Github запускается в верхней части файла test_service.yml:

on:
  push:
    branches-ignore:
      - main
    paths:
    - 'serving/**'
    - '.github/workflows/test_service.yml'

Это переводится как «выполнять это действие каждый раз, когда код отправляется в ветку, кроме основной, в которой есть изменение в процессе обслуживания или есть шанс на test_service.yml». Это будет работать с запросами на вытягивание, а также с ветками без PR. Github настроен так, что если тесты не пройдут, он заблокирует любые PR для этой ветки.

Тесты запускаются, если что-либо в разделе serving изменяется в запросе на вытягивание. Это включает в себя изменения в файле модели!

Переобучение модели из Github Actions

Теперь давайте посмотрим, как обучается модель. Ключевые части обучения находятся в training/src/main.py:

model = make_pipeline(
    TfidfVectorizer(min_df=30, ngram_range=(1, 2), sublinear_tf=True),
    LogisticRegressionCV()
)

model.fit(training_data.data, training_data.target)

joblib.dump(model, os.path.join(args.output_dir, "model.joblib.gz"), compress=9)

Это биграммная модель, использующая логистическую регрессию с перекрестной проверкой для оптимизации веса регуляризации. Параметр min_df игнорирует нечастые ngram, что помогает нам сохранить модель маленькой, не теряя при этом большой точности. Sublinear_tf уменьшает влияние повторений одного и того же ngram, и я считаю, что это делает модели немного более устойчивыми к странным входным данным.

Я использую помощник make_pipeline от scikit-learn для создания объекта Pipeline. Я считаю, что конвейеры scikit-learn 1) упрощают загрузку и сохранение 2) делают настройку гиперпараметров более эффективной 3) уменьшают случайные утечки данных тестирования в обучение.

Если вы посмотрите на main.py, вы также увидите оценку модели и сравнение с базовым уровнем.

main.py вызывается из Makefile командой DVC:

train:
   dvc repro train

Как и прежде, мне нравится использовать Makefile, чтобы убедиться, что я запускаю одни и те же команды на своем локальном компьютере и на сервере. В этом примере он вызывает DVC для воспроизведения конвейера «обучения», который определен в dvc.yaml:

stages:
  train:
    cmd: python training/src/main.py serving/app/data/ serving/app/requirements.txt
    deps:
    - training/
    outs:
    - serving/app/data/model.joblib.gz
    metrics:
    - serving/app/data/metrics.json

Это конвейерная функция DVC, похожая на make, за исключением того, что она также автоматически запускает dvc add для наших выходных файлов, чтобы гарантировать их отправку на S3 в двк push. Часть метрик сохраняет метрики в указанном файле, и команда repro покажет нам обновленные значения.

Обучение запускается из действий Github в .github/workflows/train_model.yml:

# build when a branch other than main changes training or this file:
on:
  push:
    branches-ignore:
      - main
    paths:
    - 'training/**'
    - '.github/workflows/train_model.yml'

jobs:
  train:
    runs-on: ubuntu-latest
    steps:
      # ... for full steps see github ...
      # train the model
      - name: Train model
        run: make train
      # run the web service tests to make sure it still works!
      - run: pip install -r serving/app/requirements.txt
      - name: Run web service tests
        run: make test-service
      # commit the model, which needs the IAM user to access S3 on dvc push
      - name: Commit model
        # email address from https://github.community/t/github-actions-bot-email-address/17204/5
        run: |
          git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com"
          git config --local user.name "github-actions[bot]"
          git commit -am "Automated model build"
          dvc push
          git push
        env:
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}

В этом примере я прокомментировал встроенный код и вырезал шаблонный код. Я запускаю тесты веб-сервиса перед фиксацией модели в репозитории, потому что 1) это действие не может вызвать действие test_service и 2) я не хочу фиксировать модели, не прошедшие тесты.

Настройка AWS для DVC и действий Github

Я настраиваю DVC на использование корзины S3 в качестве хранилища, которым управляет код Terraform в разделе инфраструктура. Он создает корзину S3 для использования DVC, а также пользователя IAM для действий Github для доступа к DVC и развертывания CDK.

Код местами многословен, но я приведу обзор инфраструктуры/resources.tf с добавленными комментариями:

// create the bucket. In this file it's referenced as aws_s3_bucket.b.bucket
resource "aws_s3_bucket" "b" {
  bucket_prefix = "trnka-dvc-"
  acl = "private"
}

// create the IAM user for Github Actions to use
resource "aws_iam_user" "github_actions" {
  name = "github_actions_lambda_ml"
  force_destroy = true
}

// give the IAM user permissions to list files in the bucket and read/write/delete files
resource "aws_iam_user_policy" "dvc_policy" {
  name = "dvc_policy"
  user = aws_iam_user.github_actions.name

  policy = <<EOF
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": ["s3:ListBucket"],
      "Resource": ["arn:aws:s3:::${aws_s3_bucket.b.bucket}"]
    },
    {
      "Effect": "Allow",
      "Action": [
        "s3:PutObject",
        "s3:GetObject",
        "s3:DeleteObject"
      ],
      "Resource": ["arn:aws:s3:::${aws_s3_bucket.b.bucket}/*"]
    }
  ]
}
EOF
}

// this policy is long but ensures the IAM user can run CDK and upload ECR images
resource "aws_iam_user_policy" "cdk_policy" {
  name = "cdk_policy"
  user = aws_iam_user.github_actions.name

  // ...
}

// make sure there's an access key
resource "aws_iam_access_key" "github_actions" {
  user = aws_iam_user.github_actions.name
}

// when we run terraform apply, it'll show the secret on the command line
// which we can copy into Github Actions Secrets
output "secret" {
  value = aws_iam_access_key.github_actions.secret
}

Этот код создает корзину S3 для DVC, пользователя IAM для действий Github, предоставляет пользователю IAM соответствующие разрешения и создает ключ доступа, чтобы действия Github могли «входить» в AWS.

Одним из преимуществ инфраструктуры как кода является то, что мы можем попросить экспертов проверить ее перед запуском.

Новые термины

  • Terraform: Terraform — это инструмент для инфраструктуры как кода, который работает со многими популярными облачными провайдерами.
  • IAM-политика AWS: я не умею это объяснять, но это список разрешений, которые можно присвоить пользователю или роли IAM.
  • Роль AWS IAM: это как пользователь IAM, но вы не можете войти под ним. Он предназначен только для использования сервисами. Если вы того же возраста, что и я, вы можете думать об этом как о пользователе службы с большей безопасностью.

Что возможно могло пойти не так? Критика

Трудно оценивать свою работу, но я попробую! У меня есть три раздела:

  1. Обзор общих принципов
  2. Обзор потребностей отрасли, которые не были упомянуты иначе.
  3. Обсуждение приоритетов, которые нужно исправить

Общие принципы

Правильно ли сервис справляется с задержкой и затратами?

  • AWS Lambda масштабируется вверх и вниз автоматически, поэтому, если возникнет всплеск пользователей, он справится с этим, а затем уменьшит масштаб для экономии средств. Для этого не требуется никаких специальных настроек.
  • Когда Lambda масштабируется и прогревает новый «экземпляр», ей необходимо загрузить образ Docker и загрузить модель. Мы называем это холодным стартом. Если изображение и модель даже умеренно велики, время ожидания запроса API истечет во время загрузки. Это особенно плохо в нашем репозитории, потому что там нет активных пользователей, поэтому параллелизм будет равен 0, пока не будет сделан запрос во время тестирования, а затем время ожидания при загрузке модели, пока не появится теплая лямбда.
  • Что касается стоимости, Lambda будет масштабироваться по мере необходимости с учетом входящих запросов до ограничения параллелизма в масштабах всего региона. Таким образом, кто-то может спамить ваш API, чтобы масштабировать вашу Lambda и значительно увеличить ваш счет AWS.
  • Репо полностью реализовано в us-west-2. Если API вызывается из любой точки мира или даже из США, у многих пользователей будет медленная реакция просто потому, что они находятся далеко от сервера. С другой стороны, если его вызывают только серверы или пользователи рядом с us-west-2, это нормально.

Высокодоступна ли служба?

  • Из-за холодного запуска при развертывании службы происходит полное отключение, пока не будет загружена новая модель. В профессиональной ситуации это неприемлемо, но может быть приемлемо для любительской работы.
  • Можем ли мы быстро и безопасно вернуться, если ошибка попадет в рабочую среду? Да, в этом репозитории мы бы вернулись, объединив PR в Github. Конвейер развертывания занимает 4–5 минут. Вероятно, потребуется больше времени, чтобы получить одобрение вашего PR.
  • Можем ли мы быстро обнаружить производственные проблемы? Нет, в этом репозитории нет никаких сигналов тревоги, поэтому, если он выйдет из строя, мы, вероятно, не узнаем сразу. Тем не менее, AWS предоставляет несколько панелей мониторинга по умолчанию для шлюза API и Lambda.
  • Можем ли мы перейти на PyTorch без простоев? Да, это репозиторий реализован для того, чтобы мы могли внести серьезные изменения в машинное обучение, например переключить фреймворки, протестировать их и развернуть без простоев и с возможностью отката, если изменение фреймворка представляет собой один PR.

Есть ли способ развертывания непроверенной работы?

В моем тестировании есть несколько пробелов:

  • Компоненты между запросом API и Lambda не тестировались: упаковка Docker, взаимодействие между кодом Lambda и шлюзом API и конфигурация шлюза API. Часть Docker можно легко протестировать, запустив модульные тесты внутри Docker, хотя это немного замедлит тестирование. Конфигурацию AWS можно было бы лучше протестировать, используя AWS SAM для запуска локальной версии стека для тестирования. Эта проблема также влияет на производительность разработчиков, например, если разработчик меняет способ упаковки зависимостей, ему может потребоваться отредактировать Dockerfile, и лучше всего протестировать его на своем компьютере.
  • Еще один пробел связан со временем: зависимости для модульных тестов устанавливаются в другое время, чем зависимости для образа Docker. Поскольку зависимости не закреплены полностью (см. предыдущий пост, почему), это означает, что теоретически новая версия библиотеки может быть развернута между тестированием и развертыванием, что может привести к простою в производственной среде. Один из способов решить эту проблему — создать образ Docker в конвейере PR, использовать его для модульного тестирования, а затем загрузить в ECR. Это также сэкономит время при развертывании. Другой альтернативой является большее закрепление зависимостей.
  • Теоретически кто-то может перезаписать файл модели в S3 и обойти нашу автоматизацию тестирования, хотя это и раздражает. Если бы они это сделали, это фактически не вызвало бы развертывание, потому что это не было бы изменением в Github. Это должен быть точно такой же путь к файлу на S3.

Легко ли учиться новым разработчикам?

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

  • На мой взгляд, для полного новичка это сложно изучить, потому что есть так много инструментов. Большая часть сложности связана с автоматизацией, которую разработчику может не понадобиться полностью понимать. Но DVC, например, вводит новую концепцию — вместо того, чтобы просто делать клонирование git, им также нужно извлекать файлы DVC, чтобы заставить репозиторий работать. Это можно улучшить с помощью git hooks.
  • Связанная с DVC проблема заключается в том, что для этого требуются учетные данные AWS — теперь вашим разработчикам необходимо настроить AWS для работы в репозитории, и вам нужно убедиться, что у них есть соответствующие разрешения для корзины S3.
  • Для разработчика с предыдущим опытом работы с AWS это может быть проще изучить, чем репозиторий со многими инструментами или платформами, специфичными для машинного обучения.
  • Terraform добавляет еще один язык программирования и способ мышления. Если бы я мог сделать это снова, я бы попробовал заменить эту часть на CDK, чтобы разработчикам не нужно было изучать другой язык инфраструктуры.
  • Шлюз API и Lambda могут быть сложными для понимания и, по моему опыту, не являются отличными технологиями для младших разработчиков.
  • Использование «dvc repro» может не стоить усилий по его изучению.
  • Также, на мой взгляд, недостаточно документировано для младшего разработчика.
  • На мой взгляд, параметры обработчика Lambda сложно выучить, потому что нет автозаполнения для структуры данных. Lambda Powertools может помочь с этим, но добавляет еще одну зависимость.

Сложатся ли разработчикам значительные усилия при использовании этого репозитория?

В то время как предыдущий подраздел был посвящен новым разработчикам, этот — о повседневном опыте опытных разработчиков.

  • Существуют ли ситуации, в которых может потребоваться несколько PR в репозиториях? Обучение, обслуживание и инфраструктура находятся в одном репозитории, поэтому основные изменения можно вносить в одном, хорошо протестированном PR. С другой стороны, вполне вероятно, что этот код вызывается другим репозиторием, который не показан, и изменения, связанные, например, с формой ввода JSON, могут потребовать PR в репозиториях.
  • Будет ли этот репозиторий хорошо работать со сканерами безопасности, такими как depandabot? В прошлом у меня были проблемы с простым обновлением пакетов машинного обучения, помеченных зависимым ботом. С помощью этой настройки репозитория можно настроить зависимого бота для открытия PR с новыми зависимостями, создания обновленных моделей и проверки их работы в службе. Если PR проходит, мы можем просто объединить его, чтобы обновить зависимости.
  • Хотя развертывание занимает 4–5 минут, было бы лучше, если бы оно заняло около 1 минуты. К счастью, Github Actions позволяет отслеживать время по этапам, чтобы мы могли продолжить расследование. Например, установка DVC и CDK каждый раз занимает чуть более 1 минуты. Запуск конвейера CDK, включая сборку Docker, занимает около 3 минут.

Есть проблемы с безопасностью?

Я не профессионал в области безопасности, но я участвовал в достаточном количестве обзоров, чтобы проверить основы.

  • Безопасность IAM для роли Lambda? CDK автоматически создает для нас минимальную роль IAM. Даже если злоумышленник найдет способ запустить произвольный код внутри нашей Lambda, у этой роли не будет прав на то, чтобы что-то делать.
  • Безопасность IAM для пользователя Github Actions? Этот пользователь имеет достаточное количество разрешений, и злоумышленник может причинить некоторый вред, если у него будет доступ. Однако секреты Github Actions довольно безопасны. Самая большая типичная проблема заключается в том, что мне пришлось бы менять учетные данные IAM вручную, если бы они просочились.
  • Если вы адаптируете это для работы с конфиденциальными данными обучения, Github Actions может не подойти.
  • API полностью открыт для всего мира. Если модель очень уникальна и ценна, вы не хотите, чтобы другие исследовали ее без разбора. Может помочь фильтрация аутентификации и/или IP.
  • pickle позволяют выполнять произвольный код. Но чтобы написать эти файлы pickle, кому-то потребуется доступ на запись к вашей корзине S3 и запуск повторного развертывания из Github. Файлы Pickle являются маловероятным вектором атаки в этой системе.
  • Если в образе Docker есть уязвимости, он не будет обновлен до следующего развертывания. Это один из недостатков использования Docker для лямбда-выражений — мы сами управляем этими системными зависимостями, а не AWS управляет ими, как они делают для лямбда-выражений в zip-файлах.

Проблемы в реальной компании

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

  • Безопасность конечной точки (аутентификация, управление секретами, ограничения IP и т. д.)
  • Стабильность конечной точки (DNS)
  • Управление версиями конечной точки. Если структура входных или выходных данных будет изменяться таким образом, что не будет обратной совместимости, вам может потребоваться поддержка нескольких структур данных и версия самой конечной точки.
  • CORS, когда он интегрируется с внешним интерфейсом, обслуживаемым из другого домена, а также оптимизация задержки, связанная с CORS.
  • Приватный PyPI внутри Docker, если вы используете какие-либо внутренние модули
  • Несколько этапов развертывания: dev, staging, prod
  • Интеграционное тестирование, сквозное тестирование и модульное тестирование для любой бизнес-логики
  • Предусмотренный параллелизм, настройка для прогрева Lambda перед переходом шлюза API на новую версию, чтобы у нас не было времени простоя.
  • Приборные панели и сигналы тревоги
  • Версии данных для обучения, обновления данных и тестирование данных. Если вы получаете новые данные с течением времени, перед обучением необходим целый конвейер.

Теперь, когда я думаю о бизнесе, самое время упомянуть общую картину. Когда вы пишете API, это часть большой системы. Я видел значительный вред, вызванный тем, что слишком много внимания уделялось только одному репо и слишком мало — коду, вызывающему API. Например, мы можем изменить наш API, никому не сообщая об этом, и это может вывести из строя наш продукт.

Кроме того, полезно учитывать опыт разработчиков, использующих наш API, например, легко ли его освоить и использовать. Другие освещали тему опыта разработчиков для API лучше, чем я:

Если вы развертываете сервис в той же учетной записи AWS, что и другие сервисы, есть некоторые дополнительные соображения:

  • Назван ли и помечен ли сервис, чтобы вы могли легко проверить свой счет за AWS? Я бы посоветовал хотя бы пометить все ресурсы командой, которая их создала.
  • Если у вас нет ограничений параллелизма Lambda, имейте в виду, что все Lambda в одной учетной записи и регионе имеют общий лимит параллелизма. Таким образом, если ваша Lambda использует параллелизм 999, а ограничение составляет 1000, одновременно может работать только 1 другая Lambda в компании, а другие будут регулироваться. (Я говорю это на основании реального опыта работы с лямбда-выражениями, которые регулируются из-за того, что служба ведения журналов поглощает наш параллелизм.)

Приоритеты для исправления

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

Приоритизация должна определяться потребностями бизнеса, потребностями пользователей и тем, как ваш сервис интегрирован в продукт. Если бы это был критически важный сервис, работающий с личными данными, это сильно отличалось бы от удобного сервиса, который не может вывести продукт из строя. Если это API, используемый внешними разработчиками, который сильно отличается от внутреннего API. Если это сервис, который поддерживает 100 000 запросов в час, это отличается от 100 запросов в час. И так далее.

Ниже приведен пример расстановки приоритетов, который может иметь смысл для небольшого стартапа:

Высокие приоритеты

  • Доменное имя, чтобы мы случайно не вызвали сбой при смене шлюза API.
  • Предусмотренный параллелизм, чтобы сократить время простоя при развертывании обновлений.
  • Поговорите с людьми, интегрирующими API, если это не вы!
  • Любое необходимое соответствие

Средние приоритеты

  • IP-фильтрация, аутентификация и/или безопасность по вашему выбору — хотя это может варьироваться от высокого до низкого в зависимости от приложения.
  • Аварийные сигналы о сбоях — это может варьироваться от высокого до низкого в зависимости от приложения.
  • Реальные развертывания с нулевым временем простоя

Низкие приоритеты

  • Все остальное

Альтернативы

Я также попробовал несколько альтернатив, и я кратко упомяну, что я нашел.

Развертывание в AWS ECS

AWS Elastic Container Service (ECS) — лучший вариант, чем Lambda, для большинства людей. Он всегда включен, поэтому вам не нужно беспокоиться о времени, которое требуется для загрузки вашей модели при загрузке. Недостаток ECS в том, что сложно правильно настроить автомасштабирование, и это может быть дороже.

Развертывание с Cortex.dev

Я нашел Cortex.dev восхитительно быстрым и простым. Само развертывание было намного быстрее, чем что-либо еще, и не намного сложнее, чем Heroku. Я помню, как тогда подумал, что моей команде безопасности могут не понравиться некоторые аспекты развертывания.

Управление версиями модели с помощью git-lfs на Github

Я начал с использования git-lfs для управления версиями модели и бесплатного уровня Heroku для развертывания. Мне было любопытно использовать Github в качестве поставщика git-lfs после борьбы с альтернативой на работе.

Итак, я понял, почему мы не использовали git-lfs на Github! Когда вы достигаете квоты LFS, ваше репо блокируется до тех пор, пока вы не купите больше квоты или не удалите репо. Последнее, что я проверял, у Github нет способа автоматически увеличить вашу квоту, поэтому вы полагаетесь на людей, периодически обновляющих веб-сайт вручную.

Вещи, до которых я не дошел

В наши дни существует так много инструментов MLOps! Для каждой библиотеки, инструмента или платформы, которые я использовал, есть десятки альтернатив. Например, я слышал хорошие отзывы о kubeflow, и сейчас самое время переоценить Sagemaker.

Я также видел, как некоторые люди развертывали модели с кодом, используя упаковку Python. Кажется, это хороший способ упаковать код, данные и зависимости вместе.

Выводы

Надеюсь, мне удалось прояснить общие понятия веб-сервисов машинного обучения!

Хотя в этом примере я использовал AWS Lambda, в основном из-за любопытства. Пожалуйста, не интерпретируйте этот пост как одобрение Lambda для сервисов машинного обучения. Является ли Lambda хорошим вариантом, будет зависеть от ваших потребностей.

Также имейте в виду, что это может служить руководством для небольшого стартапа, но я сомневаюсь, что он готов к работе в более крупном масштабе. И я, конечно же, упустил некоторые части типичной работы MLOps, такие как поддержка экспериментов.

Спасибо за прочтение! Если какие-то части статьи показались вам запутанными, сообщите мне об этом.