Как справиться с нагрузкой в черную пятницу? Автоскейлинг инференса в Kubernetes

px23aewx70211ejvjcl0w6sxwpq.png


У многих наступление осени вызывает разные чувства и эмоции: от ностальгии во время просмотра Гарри Поттера под теплым пледом до депрессивных мыслей у дождливого окна в кофейне. Именно в этот период компании проводят масштабные мероприятия, одно из них — известная черная пятница. Клиенты пытаются «урвать» по хорошей скидке товары и купить подарки к Новому году. В связи с этим «атакуют» своим трафиком популярные сайты e-commerce, которые могут быть не готовы к такой нагрузке.

Ни для кого не секрет, как сохранить и инфраструктуру в пиковые нагрузки, и трафик клиентов. При этом не понести большие убытки. Деплоим сервис в облако, скейлим ресурсы по требованию и радуемся продажам! Но это в e-commerce, мы же с вами интересуемся ML production.

Привет, Хабр! Я — Антон, DevOps-инженер в команде Data/ML-продуктов Selectel. В этой статье расскажу, зачем нужен автоскейлинг GPU-ресурсов, как настроить масштабирование реплик в Kubernetes по трафику, а также как сделать свой высоконагруженный ChatGPT.

Используйте навигацию, если не хотите читать текст полностью:

→ Нагрузка в ML Production
→ Как работает автоскейлинг нод в K8s
→ Автоскейлинг chatGPT2 на VLLM
→ Заключение

jargq5pdefecou6_ouzhfadcrr8.gifХочешь выиграть мерч? Попробуй решить IT-кроссворд! Более 256 вопросов, 7 кроссвордов на разные темы из мира IT — ежедневно с 23 по 29 сентября. Достаточно зарегистрироваться по ссылке.

Нагрузка в ML Production


Лучше всего рассказывать про нагрузку ML- или инференс-сервисов на примере ChatGPT. Чтобы поддерживать свою инфраструктуру, OpenAI использует 3 617 серверов HGX A100. Это позволяет обеспечить MAU от 100 до 500 млн активных пользователей в месяц.

Если посмотреть на статистику последних 90 дней работы ChatGPT, можно увидеть, что даже такой IT-мастодонт не всегда справляется с входящим трафиком — обратите внимание на красные черты доступности сервиса.

025108b3f1d6daf7454b34df1d52c55c.png


Статусная панель доступности сервисов OpenAI.

Сам инференс почти не отличается от обычного веб-сервиса. Пользователь отправляет на эндпоинт запрос, затем модель делает предположение по нашему запросу и возвращает ответ в том же формате — например, в JSON. Чтобы справиться с большой нагрузкой, необходимо развернуть больше реплик. Для больших реплик нужно больше свободных ресурсов. Облако отлично подходит под такие системы, так как именно там есть свободные ресурсы под дополнительные входящие нагрузки (но конечно не во всех кейсах).

И вот мы хотим реализовать инференс-продакшн, да еще и с использованием GPU в облаке, например, на основе Selectel Managed Kubernetes (MKS). Давайте разберемся, с чем нам предстоит столкнуться.

Как работает автоскейлинг нод в K8s


Начальное состояние нашей системы — развернутый кластер Managed Kubernetes с одной нодой и GPU. На ноде крутится инференс-сервис, на который можно отправлять HTTP-запросы, например, как на модель gpt2.

За поддержку сервисов для работы с видеокартами отвечает GPU-оператор. Подробнее про него читайте в моей предыдущей статье.

1af603b55570d768c261c1edff437aff.png


Начальное состояние системы при автоскейлинге.

Далее мы подаем трафик на нашу реплику и замечаем, что клиенты стали получать ответы от инференса с задержкой больше одной секунды. Что происходит дальше с нашей системой? Рассмотрим подробнее.

Horizontal Pod Autoscaler


В дело вступает HPA (Horizontal Pod Autoscaler). Мы заранее заложили в него требование, что задержка запросов не должна превышать секунду. Как только это происходит, система разворачивает еще одну реплику нашего сервиса.

985680c9d35bdade9b7b5cded28a00b8.png


Добавилась новая реплика.

Когда новая реплика поднимается, то требует ресурс nvidia.com/gpu=1, который отвечает за наличие GPU на ноде. В данном случае у нас нет доступной ноды с этим ресурсом.

K8s autoscaler


В облаке Selectel для реализации автоскейлинга нод используем форк этого репозитория. Автоскейлер проверяет наличие ресурсов — CPU, RAM и т. д. — и смотрит за ресурсом nvidia.com/gpu, которого не хватает для новой реплики.

59df0fb3554c92db12aff412bc194f3a.png


Появилась новая нода.

Автоскейлер поднимет ноду из базового образа в той группе, в которой разворачивается новая реплика. Время деплоя новой ноды зависит от размера выбранного флейвора, обычно до пяти минут. Далее начнется установка сервисов K8s.

Managed Kubernetes Services


На этом этапе на новую ноду ставятся необходимые для работы K8s-сервисы в виде systemd units: containerd, kubelet, mk-node-adm, mk-node-health. Это занимает до минуты.

GPU operator


Поскольку мы работаем с GPU, нам необходимо подготовить ноду. GPU-оператор устанавливает необходимые драйверы и тулкиты, настраивает плагин. Последнее как раз и выдает ресурс nvidia.com/gpu для нашей новой реплики.

e1dbe8c76cf507e43ac0bc532b2b17a6.png


Устанавливаются драйвера на ноду.

После установки всех драйверов нода готова — теперь на нее можно аллоцировать реплику. Это занимает примерно до трех минут.

Image pulling


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

1d5a874308d797a81af0fb93baf178ee.png


Аллоцируется инференс на новую ноду.

Для образа размером в 20 ГБ —, а такие в ML бывают достаточно часто — время пуллинга займет порядка шести минут (с каналом 1 Гбит/с).

Это достаточно длинный этап для обычного пуллинга, не правда ли? В своем Telegram-канале я описал возможные варианты оптимизации времени образа, так что залетайте на огонек. Также думаю подробнее разобрать варианты оптимизации в следующей статье.


Теперь перейдем к практике и попробуем построить свой высоконагруженный инференс-сервис.

Автоскейлинг chatGPT2 на VLLM


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

Какие компоненты нам понадобятся


Инфраструктура


В вебинаре я разворачивал кластер Managed Kubernetes с помощью Terraform. Если вы знакомы с этим инструментом, вам не составит труда использовать код из репозитория и развернуть кластер.

Мы же посмотрим, как через панель управления развернуть Managed Kubernetes в облаке с опцией автоскейлинга. В целом, это ничем не отличается от обычного флоу, поэтому покажу только особенности.

1. Создаем кластер и указываем дефолтные настройки:

343c4cd5af2380d7495dd4c5efa0c179.png


2. Указываем регион, версию K8s и отказоустойчивость кластера. При деплое выбираем группу нод и указываем автоскейлинг:

953b49faf8662d4a7ee44c77d9c33027.png


При включении опции Автомасштабирования можно выбрать от 2 до 20 нод в одной группе. При этом через поддержку можно увеличить квоты персонально.

3. В конфигурации ноды выбираем флейвор с GPU, например Tesla T4:

6ae63cd8eaeca30cd212dd085fd46607.png


Также при выборе конфигурации ноды указываем опцию Установить ноду без драйверов GPU, чтобы самостоятельно поставить GPU-оператор.

Итак, наш кластер готов! Теперь установим необходимые сервисы.

Системные сервисы


gpu-operator


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

1. Используем следующие значения для Helm-чарта:

driver: # поставит драйвер на ноду
  enabled: true
  version: "550.54.15" # версия устанавливаемого драйвера
toolkit: # перезапишит containerd config
  enabled: true
devicePlugin: # разметит наши GPU ресурсы в лейбл nvidia.com/gpu
  enabled: true
dcgmExporter: # нужен для экспорта метрик GPU в prometheus
  enabled: true
2. Ставим gpu-operator с помощью следующей команды:

helm upgrade --install gpu-operator -n gpu-operator --create-namespace nvidia/gpu-operator -f gpu-operator/values.yaml


prometheus-stack
1. Стек сервисов нужен для Prometheus и Grafana, чтобы отслеживать наш трафик по дашбордам. Ставим чарт со следующими значениями:

prometheus:
  prometheusSpec: # эти настройки нужны для автоматического подтягивания ServiceMonitor
    podMonitorSelectorNilUsesHelmValues: false
    probeSelectorNilUsesHelmValues: false
    ruleSelectorNilUsesHelmValues: false
    serviceMonitorSelectorNilUsesHelmValues: false
grafana: # дефолтные настройки графаны
  grafana.ini:
    analytics:
      check_for_updates: true
    grafana_net:
      url: https://grafana.net
    log:
      mode: console
    paths:
      data: /var/lib/grafana/
      logs: /var/log/grafana
      plugins: /var/lib/grafana/plugins
      provisioning: /etc/grafana/provisioning
helm upgrade --install prometheus-stack prometheus-community/kube-prometheus-stack -f prometheus-stack/values.yaml


2. Далее открываем Grafana через port forward и заходим в веб-интерфейс:

kubectl port-forward  3000:3000 --namespace=


prometheus-adapter
Нужен для преобразования метрик Prometheus в кастомные метрики K8s. Расскажем о нем подробнее чуть позже.

Манифесты нашего инференса


Для демонстрации работы инференса будем использовать фреймворк vLLM. Деплой моделей достаточно прост: достаточно указать название из списка доступных моделей, например Hugging Face. В нашем случае — gpt2, чтобы не тратить много времени на загрузку весов. Также vLLM хорош тем, что сразу под капотом имеет метрики инференса и Swagger для тестов.

Отправляем манифесты в одну папку vllm/ha. Деплоить их можно с помощью команды:

kubectl apply -f vllm/ha


Теперь рассмотрим каждый манифест.vLLM deployment
Манифест деплоймента нашего ChatGPT 2:

apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: vllm-app
  name: vllm
  namespace: default
spec:
  replicas: 1
  selector:
    matchLabels:
      app: vllm-app
  strategy:
    type: Recreate
  template:
    metadata:
      labels:
        app: vllm-app
    spec:
      containers:
      - command:
        - python3
        - -m
        - vllm.entrypoints.openai.api_server
        - --model
        - gpt2
        image: vllm/vllm-openai:latest
        name: vllm-openai
        ports:
        - containerPort: 8000
          protocol: TCP
        resources:
          limits:
            nvidia.com/gpu: "1"
        volumeMounts:
        - mountPath: /root/.cache/huggingface
          name: cache-volume
        readinessProbe:
          failureThreshold: 5
          httpGet:
            path: /health
            port: 8000
            scheme: HTTP
          initialDelaySeconds: 40
          periodSeconds: 5
          successThreshold: 1
          timeoutSeconds: 1
        livenessProbe:
          failureThreshold: 5
          httpGet:
            path: /health
            port: 8000
            scheme: HTTP
          initialDelaySeconds: 40
          periodSeconds: 5
          successThreshold: 1
          timeoutSeconds: 1
      volumes:
      - emptyDir: {}
        name: cache-volume


Service load balancer
Для доступа из интернета к инференсу будем использовать балансировщик нагрузки Selectel. Достаточно задеплоить следующий манифест:

apiVersion: v1
kind: Service
metadata:
  labels:
    app: vllm-app
  name: vllm-openai-svc
  namespace: default
spec:
  ports:
  - port: 8000
    protocol: TCP
    targetPort: 8000
  selector:
    app: vllm-app
  type: LoadBalancer


Service monitor
Нужен для сбора Prometheus-метрик с нашего инференса. После деплоя монитора в Prometheus система автоматически добавит новый таргет и начнет сбор данных.

apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  labels:
    serviceMonitorSelector: vllm-prometheus
  name: vllm-prometheus
spec:
  endpoints:
  - interval: 10s
    targetPort: 8000
    path: /metrics
  selector:
    matchLabels:
      app: "vllm-app"


HorizontalPodAutoscaler

Нужен для настройки автоскейлинга наших реплик. Указываем таргет в виде кастомной метрики и порог:

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
 name: vllm-hpa
spec:
 scaleTargetRef:
   apiVersion: apps/v1
   kind: Deployment
   name: vllm
 minReplicas: 1
 maxReplicas: 3
 metrics:
 - type: Pods
   pods:
     metric:
       name: vllm_request_latency_seconds
     target:
       type: AverageValue
       averageValue: 200m # 200ms задержки инференса


Думаю, многие могут задаться вопросом, что это за кастомная метрика K8s. Давайте разберем, как работает Prometheus-адаптер.

Делаем кастомные метрики с помощью Prometheus-адаптера


Зачем это нужно


Скейлинг происходит по метрикам K8s. Адаптер делает из метрик Prometheus кастомные метрики «куба» с помощью декларации API. Ранее я повторял это самостоятельно в статье про шеринг GPU. Prometheus-адаптер позволяет автоматизировать процесс через Helm-чарт.

Реализация


1. Используем следующие значения:

namespaceOverride: default
prometheus:
  url: http://prometheus-stack-kube-prom-prometheus
  port: 9090
rules:
  custom:
    - seriesQuery: 'vllm:e2e_request_latency_seconds_sum{namespace!="",pod!="",model_name="gpt2"}'
      resources:
        overrides:
          namespace:
            resource: "namespace"
          pod:
            resource: "pod"
      name:
        matches: "vllm:e2e_request_latency_seconds_sum"
        as: "vllm_request_latency_seconds"
      metricsQuery: 'rate(vllm:e2e_request_latency_seconds_sum{<<.LabelMatchers>>}[1m])/rate(vllm:e2e_request_latency_seconds_count{<<.LabelMatchers>>}[1m])'


2. Деплоим Helm-чарт с помощью такой команды:

helm upgrade --install prometheus-adapter prometheus-community/prometheus-adapter -f vllm/prometheus-adapter.yaml


Для составления кастомной метрики используется специальная формула metricsQuery. Принцип ее составления похож на выбор метрик в Prometheus с помощью promQL-запроса. Единственное, нужно дополнительно указать атрибут >, по которому фильтруются метрики по подам и пространствам имен. Этой формулой мы создадим кастомную метрику vllm_request_latency_seconds, по которой HPA будет производить скейлинг.

Проверяем инференс


После деплоя манифестов мы можем зайти в Swagger и отправить запрос в модель. Swagger будет доступен по IP-адресу балансировщика нагрузки по порту 8000.

edffbced84e846006c8acf0ae35fa60f.png


После всех манипуляций получим примерно такой ответ:

566a4dc1f33fc805782982884f378a8d.png


В результате у нас есть код 200 и ответ от gpt2 — не самый осознанный, как и модель, но инференс работает.

Подаем нагрузку


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

32d5c28f67479cbc432f94b0f6f37844.png


Нам нужен график E2E Request Latency, по нему будем отслеживать среднюю задержку запросов.

Нагрузку будем подавать с помощью инструмента gen ai perf client от NVIDIA. Его разработали на основе perf client специально для тестирования LLM.

Указываем и количество конкурентных пользователей --concurrency. Если менять --concurrency от 50 до 100, то средняя задержка будет варьироваться от 200 до 400 мс.

docker run --net host -it -v /tmp:/workspace nvcr.io/nvidia/tritonserver:24.05-py3-sdk
genai-perf   -m gpt2   --service-kind openai   --endpoint v1/completions   --concurrency 50 --url :8000 --endpoint-type completions --num-prompts 100 --random-seed 123 --synthetic-input-tokens-mean 20 --synthetic-input-tokens-stddev 0 --tokenizer hf-internal-testing/llama-tokenizer --measurement-interval 1000 -p 100000


Сам GenAI генерирует запросы для gpt2 и сохраняет их в файле artifacts/gpt2-openai-completions-concurrency50/llm_inputs.json.

Спустя время мы сможем увидеть, как HPA поднимает новую реплику, которая будет требовать nvidia.com/gpu. Далее начинается магия автоскейлинга, которую описывали выше.

Как только поднялась новая нода, поставились драйверы и аллоцировалась на ноду реплика, смотрим, как в Grafana меняется трафик.

Вот пример графика, в котором после появления новой реплики генерация токенов в старой сократилось почти в два раза:

54e5cf4c698c5542f8cfb1648d902217.png


Заключение


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

Что делать, если нет богатого парка GPU? Реализовать автоскейлинг можно и с одной GPU. Читайте в моих статьях про шеринг GPU, MIG, Timeslicing и MPS.

Зачем использовать K8s для ML production, если можно развернуть большие виртуалки? K8s — это продакшн для любых сервисов, как и для инференсов. Он избавляет вас от проблем с оркестрацией, обеспечивает деплой без даунтаймов, менеджмент ресурсов и изоляцию сервисов.

Как предусмотреть АБ-тестирование инференсов? Мы используем canary-деплой наших инференс-сервисов. Сначала тестируем на определенном проценте трафика новую модель, затем пересылаем на нее уже полный трафик. Делаем это с помощью Istio. Полноценный АБ-тест таким образом не реализовать, так как нет контроля над определенной группой пользователя, но потестировать на нагрузку новую версию инференса можно.

Можно ли использовать в одном поде две и более видеокарты? Вы можете выбрать флейвор для ноды в нашем облаке, которое использует более двух видеокарт. NVIDIA device plugin отметит наличие более одного ресурса nvidia.com/gpu на ноде. Стоит помнить, что две видеокарты под может использовать только на ноде, в которой они аллоцированы. Нельзя использовать две видеокарты с разных нод.

© Habrahabr.ru