Делим неделимое в Kubernetes: шеринг GPU с помощью MIG и TimeSlicing

от автора

Привет, Хабр! На связи снова Антон, DevOps-инженер в отделе Data- и ML-продуктов Selectel. В предыдущей статье я рассказал о шеринге GPU и показал, как запустить несколько инстансов на одной видеокарте с помощью MIG. А в конце затронул тему с автомасштабированием инференс-серверов. Она оказалась актуальной, и я решил написать продолжение.

В этот раз посмотрим, как применять технологии шеринга в Kubernetes, а также разработаем прототип автомасштабируемой инференс-платформы за один вечер. Интересно? Тогда добро пожаловать под кат!

О чем поговорим:

Проблема шеринга ресурсов в Kubernetes
GPU-оператор
Подготовка рабочего окружения
Активируем MIG в Kubernetes
Автомасштабирование нашей платформы
Как обмануть GPU-ресурсы Kubernetes с помощью TimeSlicing
Заключение

Дисклеймер: для понимания данной статьи нужны навыки в работе с Helm-чартами и kubectl, а также знание Kubernetes. Если вы хотите погрузиться в эти технологии, изучайте курсы в Академии Selectel.

Проблема шеринга ресурсов в Kubernetes


Вспомним, что происходит, когда в Kubernetes мы хотим запустить под с приатаченной GPU.

Чтобы выделить GPU от Nvidia для конкретного пода, нужно использовать специальный ресурс — nvidia.com/gpu = N, где N — количество доступных видеокарт. По умолчанию это число нельзя поделить на проценты: нельзя написать, что поду нужна, например, половина видеокарты. Соответственно, один под забирает все ресурсы GPU.

Проблема заключается в том, что не всем подам нужна целая GPU. Например, если у вас A100, то выделять 40 ГБ на один инференс бывает излишне. Также не получится запустить несколько реплик одного сервиса, так как на каждую понадобится отдельный ресурс GPU — придется запастись партией видеокарт для такой имплементации.

Еще один кейс связан с обучением моделей на видеокартах. Допустим, у вас в команде несколько Data Science-специалистов, каждому из которых понадобилась GPU для обучения. При этом у вас дефицит видеокарт — если использовать нативный Kubernetes, то образуется очередь на использование ресурсов. Это связано с тем, что каждый инстанс JupyterLab захватывает ровно один ресурс GPU (если не указано больше).

Управлением ресурсами GPU занимается DaemonSet Nvidia Device Plugin. В целом, можно форкнуть этот проект и немного изменить логику присвоения ресурсов в специальном блоке кода. Но мы рассмотрим нативные решения от Nvidia.


GPU-оператор

Разбить GPU в Kubernetes можно вручную, но мы воспользуемся GPU-оператором. Это инструмент, который позволяет администраторам Kubernetes управлять узлами GPU точно так же, как узлами CPU в кластере. О том, как с ним работать, можно почитать в документации.

В нашем случае от оператора мы получаем возможность легко и просто делать разметку MIG в нодах с A100 и A30, а также дополнительно конфигурировать TimeSlicing, о котором я расскажу подробнее в конце статьи.

Принцип работы

Для нашей задачи рассмотрим основные демонсеты, которые используются оператором для работы с GPU в Kubernetes, — mig-manager и nvidia-device-plugin.

Mig-manager — это демонсет в Kubernetes, который следит за изменениями лейбла «nvidia.com/mig.config» на ноде, а затем применяет запрошенную пользователем конфигурацию MIG.

При изменении метки mig-manager сначала останавливает все модули графического процессора, включая плагин устройства, gfd и dcgm-экспортер. Затем останавливает все клиенты GPU на хосте, перечисленные в разделе client.yml (его можно расширять), если драйверы предварительно установлены. Наконец, mig-manager применяет реконфигурацию MIG и перезапускает модули GPU. Реконфигурация может включать перезагрузку узла, если это требуется для включения режима MIG.

Краткий алгоритм обработки запроса на деление GPU в K8S

1. Для конфигурирования MIG GPU-оператор использует два демонсета:

  • mig-manager — следит за лейблом nvidia.com/mig.config. Если его значение изменилось, то применяет конфигурацию MIG с помощью nvidia-smi на ноде с GPU. Конфигурацию (например, количество партиций, на которые нужно разбить видеокарту) берет из configmap.
  • nvidia-device-plugin — отвечает за ресурсы nvidia/gpu в Managed Kubernetes. Как только mig-manager применит новую конфигурацию MIG, а лейбл nvidia/mig.config.state станет success, nvidia-device-plugin изменит количество ресурсов на указанное в конфигурации.

Если стратегия single, то изменит ресурс nvidia.com/gpu. Если стратегия mixed — заменит nvidia.com/gpu на конкретную партицию.

2. Демонсеты следят за метками на нодах. При процессе миграции конфигурации MIG используется лейбл nvidia.com/mig.config.state для отслеживания статуса:

  • pending — миграция в процессе, нужно подождать.
  • succeed — миграция выполнена успешно.
  • failed — миграция остановлена из-за ошибки.

Ошибка может быть из-за наличия работающих процессов в GPU. Поэтому миграцию необходимо проводить на не занятой GPU: оператор не может оставить «чужие процессы». Подробнее можно ознакомиться здесь.

Подготовка рабочего окружения


Для нашего исследования необходимо снова использовать определенную линейку GPU — A100 или A30. Selectel предоставляет почасовую аренду таких видеокарт, чем мы и воспользуемся. Развернем Managed Kubernetes:

1. Переходим в раздел Облачная платформа в панели управления.

2. Выбираем Kubernetes и нажимаем Создать кластер.

3. Выбираем пул ru-9a, базовый тип кластера и создаем группу нод:

  • Выбираем flavor с GPU A100.
  • Указываем размер диска.
  • Сохраняем изменения.

4. Далее можно добавить свой SSH-ключ и указать необходимую сеть.

5. Ждем. Как только кластер задеплоиться, через панель можно будет достать kubeconfig.

Процесс создания кластера Managed Kubernetes.

Также необходимо позаботиться о том, где мы будем хранить наши модели. Kubernetes позволяет указывать для подов внешние хранилища, например, через протокол NFS. Для этого отлично подойдет файловое хранилище Selectel (Selectel File Storage, SFS). Теперь его можно создать прямо из панели управления.

  1. Переходим в раздел Облачная платформа внутри панели управления.
  2. Выбираем Файловое Хранилище и нажимаем Cоздать.
  3. Выбираем пул ru-9a, указываем размер диска, тип NFS.
  4. После создания можно скопировать команду для подключения к файловому хранилищу. Из этой команды нам понадобится IP-адрес и путь до файлового хранилища.

Процесс создания файлового хранилища.

Файловое хранилище

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

Почему мы вообще используем SFS, а не хранилище внутри ноды (например, монтирование PV в локальную папку на ноде)? Все просто: при создании масштабируемой системы, вероятно, понадобится горизонтально масштабировать наши ноды в кластере. При использовании сетевого хранилища инференсы на разных нодах получат доступ к нашим моделям и не будут дублировать данные внутри себя.

Подключимся к ноде Kubernetes по SSH, чтобы подцепить файловое хранилище по протоколу NFS и положить туда наши модели:

sudo mkdir -p /mnt/nfs && sudo mount -vt nfs "10.222.1.60:/shares/share-3010a65e-124c-4ac8-b08e-a7b2eae0b78c" /mnt/nfs 

Далее перейдем в смонтированную папку и скачаем наши модели. Загрузим репозиторий на виртуальную машину и подтянем заготовленные модели. Также скачаем ONNX densenet (с этой моделью работали в предыдущей статье) с помощью скрипта fetch_models:

git clone -b r23.05 https://github.com/triton-inference-server/server.git cd server/docs/examples ./fetch_models.sh 

Убедимся, что в FX есть необходимые файлы для загрузки весов моделей в наш инференс-сервер:

ls /mnt/nfs/server/docs/examples/ -l ls /mnt/nfs/server/docs/examples/model_repository/ -l 

Отлично — файловое хранилище готово для монтирования подов.

Устанавливаем чарт GPU-оператора

GPU-оператор устанавливает несколько компонентов, в том числе nvidia-device-plugin. По умолчанию в Managed Kubernetes плагин уже установлен на нодах с GPU, поэтому сначала удаляем дефолтный плагин:

kubectl delete daemonset/nvidia-device-plugin-daemonset -n kube-system 

Далее устанавливаем GPU-оператор с помощью Helm:

helm install --wait --generate-name \     -n kube-system --create-namespace \     nvidia/gpu-operator \     --set mig.strategy=single 

Небольшие пояснения по поводу выбора стратегии Mig:

  • mig.strategy=single — используется только в случае, если на одной ноде подключены GPU с возможностью включения MIG. При этом доступны только конфигурации, которые делят видеокарты на равные части. Примеры конфигураций для GPU A100 и A30 можно посмотреть здесь (конфигурации для стратегии single начинаются с префикса all).
  • mig.strategy=mixed — используется в случаях, когда на одной ноде хотя бы одна GPU поддерживает MIG. Притом доступны любые конфигурации, поддерживаемые видеокартой. Пример такой конфигурации показан здесь.

Доступные конфигурации MIG для видеокарт A100 и A30 можно изучить в предыдущей статье.

После установки оператора GPU посмотрим лейблы ноды, которые относятся к Nvidia:

kubectl get node <node name> -o json | jq '.metadata.labels' | grep nvidia

Нас интересуют следующие лейблы:

  • nvidia.com/gpu.count — количество ресурсов GPU (сейчас указано значение 1 — следовательно, поду достанется целая видеокарта),
  • nvidia.com/gpu.product — убеждаемся что нода с GPU A100,
  • nvidia.com/mig.config — текущая конфигурация MIG по умолчанию (all-disabled означает, что MIG выключен),
  • nvidia.com/mig.config.state — статус разбиения MIG (устанавливается mig-manager),
  • nvidia.com/mig.strategy — стратегия распределения партиций, single.

Следующим этапом рекомендую проверить, что при отключенном MIG действует правило «один под — одна GPU». Для этого напишем манифест для нашего triton server, который использовали в прошлой статье.

Также обязательно укажем монтирование файлового хранилища с моделями на удаленном SFS-сервере. Для этого запустим две реплики нашего инференс-сервера:

apiVersion: apps/v1 kind: Deployment metadata: name: tritonserver labels:   app: tritonserver spec: replicas: 2 selector:   matchLabels:     app: tritonserver template:   metadata:     labels:       app: tritonserver   spec:     volumes:     - name: models       nfs:         server: 10.222.1.60 # указываем ip адрес nfs сервера         path: /shares/share-3010a65e-124c-4ac8-b08e-a7b2eae0b78c/server/docs/examples/model_repository # указываем папку монтирования         readOnly: false     containers:       - name: tritonserver         ports:         - containerPort: 8000           name: http-triton         - containerPort: 8001           name: grpc-triton         - containerPort: 8002           name: metrics-triton         image: "nvcr.io/nvidia/tritonserver:23.05-py3"         volumeMounts:         - mountPath: /models           name: models         command: ["/bin/sh", "-c"]         args: ["/opt/tritonserver/bin/tritonserver --model-repository=/models --allow-gpu-metrics=false"]         resources:           limits:             nvidia.com/gpu: 1 

Теперь можем убедиться, что один из контейнеров встал в очередь:

На нашей ноде доступен только один ресурс GPU. Соответственно, второй под будет ждать в очереди с сообщением:

0/1 nodes are available: 1 Insufficient nvidia.com/gpu. preemption: 0/1 nodes are available: 1 No preemption victims found for incoming pod. 

Активируем MIG в Kubernetes


Пришло время активировать MIG в нашем кластере Kubernetes. На самом деле, в этом нет ничего сложного, так как мы используем GPU-оператор, который делает всю работу за нас. Для начала уберем мусор: удалим созданный triton server и перейдем к настройке MIG.

Подключимся к ноде по SSH и выведем nvidia-smi. Как видим, после установки оператора MIG выключен:

Каким образом можно включить MIG и сразу разделить видеокарту, согласно нужной конфигурацию? Все просто: необходимо в лейбл nvidia.com/mig.config прописать конфигурацию MIG, которую можно взять по ссылке.

Теперь попробуем разбить карту на семь равных частей:

kubectl label node <node name> nvidia.com/mig.config=all-1g.5gb --overwrite 

Зайдем в логи mig-manager. Как видим, он зафиксировал изменение лейбла и начал свою работу. По сути, зашел на ноду и применил 19 конфигурацию MIG:

Далее повторно подключимся к ноде по SSH и выведем nvidia-smi:

Наша GPU успешно разбилась на семь партиций. Но что произошло с ресурсами в Kubernetes? Посмотрим, сколько стало доступно ресурсов GPU:

kubectl get node <node name> -o json | jq '.status.allocatable 

Так как мы выбрали стратегию Single, в Kubernetes теперь доступно семь ресурсов GPU. Далее снова протестируем наш манифест с triton server. Попробуем запустить три реплики сразу и выведем их статус:

В логах пода видим, что сервисы успешно поднялись:

Посмотрим на занятые ресурсы GPU в ноде:

kubectl describe nodes/<node name> 

Как видим, система использует три пода из семи. Убедимся, что видеопамять действительно занята на отдельных партициях MIG:

Все три процесса распределены по разным партициям MIG. Таким образом, можно запускать на одной GPU до 7 подов, если вы используете A100.

Так как мы создали deployment, теперь мы можем отскалировать triton server до семи подов:

Следующим этапом попробуем повторить наш demo-стенд с балансировщиком нагрузки из прошлой статьи, добавив к нему функционал автомасштабирования.

Балансировка нагрузки

Необходимо создать сервис Kubernetes, чтобы клиенты могли отправлять запросы на triton server. За основу я взял статью от Nvidia, обновив манифесты до версий текущего года. Согласно схеме ниже, все запросы клиентов будут распределятся по нашим инференс-серверам через Load Balancer (балансировщик нагрузки):

Создадим сервис с типом Load Balancer. Также укажем порты, которые пробрасываем для протоколов HTTP и GRPC. Отдельно выделим порт для сбора метрик, показывающие насыщение наших сервисов (по ним мы сможем определить нагрузку на сервисы):

kubectl apply -f pod-sharing-example/service-triton.yaml 

apiVersion: v1 kind: Service metadata: name: tritonserver labels:   app: tritonserver spec: selector:   app: tritonserver ports:   - protocol: TCP     port: 8000     name: http     targetPort: 8000   - protocol: TCP     port: 8001     name: grpc     targetPort: 8001   - protocol: TCP     port: 8002     name: metrics     targetPort: 8002 type: LoadBalancer 

Проверим, что балансировщик поднялся (также балансировщик будет доступен в панели облака Selectel):

kubectl get svc 

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

docker run -it --rm nvcr.io/nvidia/tritonserver:23.05-py3-sdk /workspace/install/bin/image_client -m densenet_onnx -c 3 -s INCEPTION -i http -u 5.188.81.52:8000 /workspace/images/mug.jpg 

В результате, как и в предыдущей статье, моделька распознает, что изображено на картинке. Отправили картинку с кружкой кофе — получили такой результат:

# Inference should return the following Image '/workspace/images/mug.jpg':     15.346230 (504) = COFFEE MUG     13.224326 (968) = CUP     10.422965 (505) = COFFEEPOT 

Мониторинг

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

Для начала установим инсталляцию Prometheus Operator, которая подробно описана в отдельной статье:

helm repo add prometheus-community https://prometheus-community.github.io/helm-charts helm repo update helm search repo prometheus-community kubectl create namespace monitoring helm install kube-prom-stack prometheus-community/kube-prometheus-stack -n monitoring 

Далее создадим PodMonitor, чтобы метрики из наших инференс-серверов triton попадали в Prometheus:

kubectl apply -f monitoring/pod-monitor-triton.yaml 

apiVersion: monitoring.coreos.com/v1 kind: PodMonitor metadata: name: kube-prometheus-stack-tritonmetrics namespace: monitoring labels:     release: kube-prom-stack spec:  selector:     matchLabels:        app: tritonserver  namespaceSelector:     matchNames:        - default  podMetricsEndpoints:    - port: metrics-triton      interval: 10s      path: /metrics 

Чтобы применить изменения, необходимо перезагрузить Prometheus:

kubectl rollout restart statefulset prometheus-kube-prom-stack-kube-prome-prometheus 

В интерфейсе Prometheus (его можно пробросить через kubectl port forward) в таргетах можно увидеть все наши реплики инференс-сервера:

Отправим несколько тестовых запросов на наш инференс-сервер и посмотрим на метрику NVIDIA Triton — nv_inference_request_success. Количество успешных запросов должно отличаться от 0. При этом можно заметить, что на каждом поде указано разное количество запросов. Это связано с тем, что балансировщик нагрузки распределяет запросы по разным подам, чтобы уменьшить нагрузку на всю нашу платформу:

Также можем запросить агрегированную метрику по следующей формуле (в течение 30 секунд я отправлял запросы на сервер). Формула показывает среднее значение отношения длительности запросов и количества успешных запросов за последние 30 секунд. В дальнейшем по этому соотношению мы и будем управлять автомасштабированием:

avg(delta(nv_inference_queue_duration_us[30s])/(1+delta(nv_inference_request_success[30s]))) 

Готово — сбор метрик работает, балансировщик нагрузок настроен, MIG в Kubernetes активирован.

Другие полезные материалы:

   

Автомасштабирование нашей платформы

Prometheus Adapter

Теперь следует развернуть Prometheus Adapter, который умеет взаимодействовать с Kubernetes и самим Prometheus. Он поможет нам использовать собранные показатели для принятия решений о масштабировании. Адаптер собирает названия доступных метрик из Prometheus через регулярные промежутки времени, а затем предоставляет только те метрики, которые соответствуют определенным формам. Эти показатели предоставляются сервисом API и могут быть легко использованы HPA.

Для своего прототипа я выдал расширенный доступ к Kubernetes для Prometheus Adapter, что не советую делать в продакшен-системе. Я это сделал с помощью специальной команды:

kubectl create clusterrolebinding permissive-binding --clusterrole=cluster-admin --user=admin --user=kubelet --group=system:serviceaccounts 

Для продакшен-системы вы можете настроить политику RBAC самостоятельно — наиболее подробно процесс описан в официальной документации Kubernetes и документации Selectel.

Определяем новую метрику, CustomMetric

Мы хотим мониторить агрегированную метрику наших сервисов, но для этого ее необходимо создать. Будем использовать среднее время запросов (avg_time_queue_us) в микросекундах, которое опишем в ConfigMap, чтобы HPA масштабировал реплики по этому таргету.

Составим формулу в нашем ConfigMap которая состоит из следующих метрик сервиса:

  • nv_inference_request_success[30] — число успешных запросов за последние 30 секунд,
  • nv_inference_queue_duration_us — длительность запросов в микросекундах.

Пользовательская метрика avg_time_queue_us, которую мы определим ниже, означает среднее время запроса за последние 30 секунд. На ее основе HPA решает, следует ли изменять количество реплик. В итоге наш ConfigMap выглядит следующим образом:

apiVersion: v1 kind: ConfigMap metadata: name: adapter-config namespace: monitoring data: triton-adapter-config.yml: |   rules:   - seriesQuery: 'nv_inference_queue_duration_us{namespace="default",pod!=""}'     resources:       overrides:         namespace: {resource: "namespace"}         pod: {resource: "pod"}     name:       matches: "nv_inference_queue_duration_us"       as: "avg_time_queue_us"     metricsQuery: 'avg(delta(nv_inference_queue_duration_us{<<.LabelMatchers>>}[30s])/(1+delta(nv_inference_request_success{<<.LabelMatchers>>}[30s]))) by (<<.GroupBy>>)' 

Деплоим Prometheus Adapter

Чтобы HPA мониторил кастомную метрику, мы должны создать Deployment, Service, и APIService для Prometheus Adapter. Далее будут представлены манифесты, которые необходимы для его работы. Ознакомимся сначала с каждым из них и далее задеплоим все вместе.

Ниже представлен код нашего Deployment, который ссылается на созданный ранее ConfigMap и Prometheus (укажите в нем правильный адрес --prometheus-url и название config-файла, который мы готовили выше):

apiVersion: apps/v1 kind: Deployment metadata:  name: triton-custom-metrics-apiserver  namespace: monitoring  labels:    app: triton-custom-metris-apiserver spec:  replicas: 1  selector:   matchLabels:     app: triton-custom-metrics-apiserver  template:   metadata:     labels:       app: triton-custom-metrics-apiserver   spec:     containers:     - name: custom-metrics-server       image: registry.k8s.io/prometheus-adapter/prometheus-adapter:v0.11.0       args:       - --cert-dir=/tmp       - --prometheus-url=http://kube-prom-stack-kube-prome-prometheus:9090/       - --metrics-relist-interval=30s       - --config=/etc/config/triton-adapter-config.yml       - --secure-port=6443       ports:       - name: main-port         containerPort: 6443       volumeMounts:       - name: config-volume         mountPath: /etc/config         readOnly: false     volumes:     - name: config-volume       configMap:         name: adapter-config 

Теперь создадим новый Service для нашего Deployment:

apiVersion: v1 kind: Service metadata:  name: triton-custom-metrics-api  namespace: monitoring spec:  selector:   app: triton-custom-metrics-apiserver  ports:  - port: 443    targetPort: 6443 

Далее создадим APIService, в котором определим API для доступа к новым метрикам. API будет доступно по адресу custom.metrics.k8s.io/v1beta1. Так HPA сможет достучаться до нашей кастомной метрики. Код ниже создаст сущность APIService, которую мы привяжем к нашему сервису:

apiVersion: apiregistration.k8s.io/v1 kind: APIService metadata:  name: v1beta1.custom.metrics.k8s.io spec:  groupPriorityMinimum: 100  group: custom.metrics.k8s.io  insecureSkipTLSVerify: true  service:    name: triton-custom-metrics-api    namespace: monitoring  version: v1beta1  versionPriority: 5 

Задеплоим наши сущности с помощью команды kubectl apply -f — и можно проверить вывод нашего API:

gpu-sharing-k8s git:(main) $ kubectl get --raw "/apis/custom.metrics.k8s.io/v1beta1" | jq                        {   "kind": "APIResourceList",   "apiVersion": "v1",   "groupVersion": "custom.metrics.k8s.io/v1beta1",   "resources": [     {       "name": "pods/avg_time_queue_us",       "singularName": "",       "namespaced": true,       "kind": "MetricValueList",       "verbs": [         "get"       ]     },     {       "name": "namespaces/avg_time_queue_us",       "singularName": "",       "namespaced": false,       "kind": "MetricValueList",       "verbs": [         "get"       ]     }   ] } 

Проверим, что наши поды действительно возвращают метрики:

gpu-sharing-k8s git:(main) $ kubectl get --raw /apis/custom.metrics.k8s.io/v1beta1/namespaces/default/pods/%2A/avg_time_queue_us | jq . {   "kind": "MetricValueList",   "apiVersion": "custom.metrics.k8s.io/v1beta1",   "metadata": {},   "items": [     {       "describedObject": {         "kind": "Pod",         "namespace": "default",         "name": "tritonserver-595c7df85f-bnplq",         "apiVersion": "/v1"       },       "metricName": "avg_time_queue_us",       "timestamp": "2023-08-17T15:21:53Z",       "value": "399300m",       "selector": null     },     {       "describedObject": {         "kind": "Pod",         "namespace": "default",         "name": "tritonserver-595c7df85f-gp5k9",         "apiVersion": "/v1"       },       "metricName": "avg_time_queue_us",       "timestamp": "2023-08-17T15:21:53Z",       "value": "0",       "selector": null     },     {       "describedObject": {         "kind": "Pod",         "namespace": "default",         "name": "tritonserver-595c7df85f-mpxhn",         "apiVersion": "/v1"       },       "metricName": "avg_time_queue_us",       "timestamp": "2023-08-17T15:21:53Z",       "value": "4275m",       "selector": null     }   ] } 

Как видим, под tritonserver-595c7df85f-mpxhn вернул метрику, после того, как я в течение 30 секунд посылал тестовые запросы. По этим метрикам мы и будем масштабировать наши реплики.

Horizontal Pod Autoscaling

HPA масштабирует наши реплики по следующей формуле:

  • R — необходимое количество реплик,
  • CR — текущее количество реплик,
  • CV — текущая метрика, среднее значение метрик со всех реплик,
  • DV — порог метрики.

Когда R отличается от CR, HPA увеличивает или уменьшает количество реплик, воздействуя на развертывание подов Kubernetes. По сути, всякий раз, когда соотношение между текущим значением метрики и таргетом больше одного, могут быть развернуты новые реплики.

Работа HPA заключается в том, что он говорит Deployment сколько нужно реплик в текущий момент времени.

Ниже представлен манифест HPA, где мы указываем наш Deployment, а также название метрики и ее порог, по которому HPA будет принимать решение об изменении количества реплик. Применим изменения также через команду kubectl apply -f.

apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler metadata:  name: tritonserver-hpa spec:  scaleTargetRef:    apiVersion: apps/v1    kind: Deployment    name: tritonserver  minReplicas: 1  maxReplicas: 7  metrics:  - type: Pods    pods:      metric:        name: avg_time_queue_us      target:        type: AverageValue        averageValue: 50 

Если клиенты начнут спамить запросами на инференс-сервер, HPA развернет больше реплик, а балансировщик автоматически начнет перенаправлять запросы на новые реплики. Тем самым мы снизим нагрузку на существующие.

Теперь запустим тест на пропускную способность из прошлой статьи, начиная с 60 конкурентных клиентов:

perf_client -i http -u triton-server -m densenet_onnx --concurrency-range 60:100 

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

kubectl describe hpa tritonserver-hpa 

Но с MIG мы ограничены только масштабированием в семь реплик, так как сейчас мы поделили нашу GPU A100 на максимальное количество партиций. При этом важно помнить, одна реплика — это один ресурс GPU.

Но каким образом мы можем запускать больше реплик в Kubernetes? Давайте посмотрим, как немного обмануть его с помощью TimeSlicing.

Как обмануть GPU-ресурсы Kubernetes с помощью TimeSlicing


Рассмотрим деплой нашего сервиса на ноду с GPU Tesla T4. Такая видеокарта не поддерживает MIG, то есть разделить ее на несколько кусочков так, как мы делали ранее, не получится. Но Nvidia предоставляет еще один способ шеринга GPU в Kubernetes — TimeSlicing. С этой технологией мы не ограничены только GPU A100 и A30.

Добавим через панель управления еще одну группу нод с GPU Tesla T4. Для этого достаточно выбрать необходимый flavor и указать размер хранилища:

Далее увидим, что в нашем кластере добавилась новая группа нод:

Новая группа нод доступна через kubectl:

Официальная документация по настройке TimeSlicing с помощью оператора GPU доступна по ссылке.

Теперь необходимо создать ConfigMap, в котором мы укажем количество партиций для разбиения GPU. Для каждой из них TimeSlicing выделит свой квант времени (то есть конкурентные процессы будут сменять друг друга на разных процессах), при этом будет использоваться общая память GPU. Если один из процессов превысить лимит по видеопамяти и случится OutOfMemory Error (OOM), ошибка затронет все сервисы на GPU.

Далее интегрируем конфигурацию в наш Helm-релиз. Для этого проверим, что на Tesla T4 доступен только один ресурс nvidia.com/gpu:

kubectl get node <node name> -o json | jq '.status.allocatable 

Вторым этапом применим следующий ConfigMap, в котором укажем, что нужно поделить GPU на 14 частей:

apiVersion: v1 kind: ConfigMap metadata:  name: time-slicing-config-fine  namespace: kube-system data:  tesla-t4: |-    version: v1    flags:      migStrategy: none    sharing:      timeSlicing:        resources:        - name: nvidia.com/gpu          replicas: 14 

Теперь обновляем наш релиз GPU-оператора, указав новый параметр:

helm upgrade --install --wait \     -n kube-system <release-name> \     nvidia/gpu-operator \     -set devicePlugin.config.name=time-slicing-config-fine 

Чтобы активировать TimeSlicing, необходимо добавить лейбл на ноду:

kubectl label node <node-name> nvidia.com/device-plugin.config=tesla-t4 

В логах nvidia-device-plugin можно увидеть следующее:

"deviceListStrategy": [ "envvar" ], "deviceIDStrategy": "uuid", "cdiAnnotationPrefix": "cdi.k8s.io/", "nvidiaCTKPath": "/usr/bin/nvidia-ctk", "containerDriverRoot": "/host" } }, "resources": { "gpus": [ { "pattern": "*", "name": "nvidia.com/gpu" } ], "mig": [ { "pattern": "*", "name": "nvidia.com/gpu" } ] }, "sharing": { "timeSlicing": { "resources": [ { "name": "nvidia.com/gpu", "devices": "all", "replicas": 14 } ] } } 

Как видим из логов, плагину выданы новые инструкции на изменение количества реплик (sharing[‘timeslicing’]). Теперь можно проверить, сколько ресурсов nvidia.com/gpu стало доступно:

Супер — мы немного обманули Kubernetes. Теперь можно запускать до 14 инференсов на ноде с GPU Tesla T4. Однако мы все еще не застраховали себя от ошибки OOM.

Попробуем запустить наш Deployment на этой ноде с четырьмя репликами:

apiVersion: apps/v1 kind: Deployment metadata: name: tritonserver-teslat4 labels:   app: tritonserver-teslat4 spec: replicas: 4 selector:   matchLabels:     app: tritonserver-teslat4 template:   metadata:     labels:       app: tritonserver-teslat4   spec:     volumes:     - name: models       nfs:         server: 10.222.1.60         path: /shares/share-3010a65e-124c-4ac8-b08e-a7b2eae0b78c/server/docs/examples/model_repository         readOnly: false     nodeSelector:       nvidia.com/gpu.product: Tesla-T4-SHARED     containers:       - name: tritonserver-teslat4         ports:         - containerPort: 8000           name: http-triton         - containerPort: 8001           name: grpc-triton         - containerPort: 8002           name: metrics-triton         image: "nvcr.io/nvidia/tritonserver:23.05-py3"         volumeMounts:         - mountPath: /models           name: models         command: ["/bin/sh", "-c"]         args: ["/opt/tritonserver/bin/tritonserver --model-repository=/models --allow-gpu-metrics=false"]         resources:           limits:             nvidia.com/gpu: 1 

kubectl get deployments 

Видим, что поды поднялись — заглянем в логи одного из них, чтобы убедиться, что сервер работает. А также посмотрим загрузку GPU через nvidia-smi на ноде:

Видно, что поднялись четыре разных процесса на одной GPU, но они используют общую память. Если мы начнем увеличивать количество подов, то в какой то момент превысим лимит и все наши сервисы получат OOM.

Как решить проблему OOM?

Думаю, этот вопрос достоин отдельной статьи. Но могу дать краткую пару вариантов по решению проблемы с OOM при использовании TimeSlicing:

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

2. Ограничьте потребление памяти на уровне runtime. Можно конвертировать модели в onnx runtime. Подробности ищите по ссылке.

3. Используйте CUDA Multi-Process Service (MPS). Скажу сразу: нативной поддержки от Nvidia еще нет. Но можно использовать форк nvidia-device-plugin.

4. Используйте MIG, тогда OOM будет ограничена конкретной партицией. Да, у вас будет ограничение в семь частей GPU, но при желании его можно обойти. Давайте познакомимся с этим решением поближе.

Коллаборация века, или TimeSlicing в MIG

Сейчас мы посмотрим, каким образом можно «впихнуть» в шеринг GPU по MIG шеринг GPU по TimeSlicing.

Попробуем разбить нашу ноду A100 через mixed-стратегию. Например, возьмем десятую конфигурацию, в которой две части 1g.5gb и по одной 2g.10gb и 3g.20gb. И с помощью TimeSlicing увеличим количество ресурсов (replicas) в каждой партиции.

Для этого необходимо снова обратиться к ConfigMap, который мы использовали для TimeSlicing с Tesla T4. Суть точно такая же: мы вводим разбиение по количеству реплик на каждый ресурс, при этом явно указываем ресурсы для каждой партиции MIG.

apiVersion: v1 kind: ConfigMap metadata:  name: time-slicing-config-mig  namespace: kube-system data:  a100-40gb: |-    version: v1    flags:      migStrategy: mixed    sharing:      timeSlicing:        resources:        - name: nvidia.com/gpu          replicas: 3        - name: nvidia.com/mig-1g.5gb          replicas: 4        - name: nvidia.com/mig-2g.10gb          replicas: 3        - name: nvidia.com/mig-3g.20gb          replicas: 4        - name: nvidia.com/mig-7g.40gb          replicas: 7 

Теперь обновим оператор GPU, настроив стратегию MIG и добавив новый ConfigMap:

helm upgrade --install --wait \                                                                      -n kube-system gpu-operator-1691669985 \     nvidia/gpu-operator \     --set mig.strategy=mixed --set devicePlugin.config.name=time-slicing-config-mig 

Укажем для nvidia-device-plugin, что мы будем использовать config a100-40gb из нашего ConfigMap:

kubectl label node <node name> nvidia.com/device-plugin.config=a100-40gb --overwrite 

Применим десятую конфигурацию MIG, разбив A100 на разные партиции, которые указывали выше:

kubectl label node <node name> nvidia.com/mig.config=all-balanced --overwrite 

Далее выведем доступные ресурсы и посмотрим на результат:

kubectl get node <node name> -o json | jq '.status.allocatable 

Как видим, нам стало доступно больше ресурсов, так как мы добавили к каждой партиции TimeSlicing. Удалось получить восемь реплик 1g.5gb, три 2g.10gb и четыре 3g.20gb.

Проверим, что теперь мы можем деплоить больше семи реплик нашего инференса на видеокарту A100 с включенным MIG. Задеплоим triton server, слегка модифицировав уже знакомый манифест Deployment. Обратите внимание на секцию с ресурсами, на партицию 1g.5gb:

apiVersion: apps/v1 kind: Deployment metadata: name: tritonserver-timeslicing-mig labels:   app: tritonserver-timeslicing-mig spec: replicas: 8 selector:   matchLabels:     app: tritonserver-timeslicing-mig template:   metadata:     labels:       app: tritonserver-timeslicing-mig   spec:     volumes:     - name: models       nfs:         server: 10.222.1.60         path: /shares/share-3010a65e-124c-4ac8-b08e-a7b2eae0b78c/server/docs/examples/model_repository         readOnly: false     containers:       - name: tritonserver-timeslicing-mig         ports:         - containerPort: 8000           name: http-triton         - containerPort: 8001           name: grpc-triton         - containerPort: 8002           name: metrics-triton         image: "nvcr.io/nvidia/tritonserver:23.05-py3"         volumeMounts:         - mountPath: /models           name: models         command: ["/bin/sh", "-c"]         args: ["/opt/tritonserver/bin/tritonserver --model-repository=/models --allow-gpu-metrics=false"]         resources:           limits:             nvidia.com/mig-1g.5gb: 1 

Готово — на партиции nvidia.com/mig-1g.5gb запустилось ровно восемь реплик:

Таким образом, обойти ограничение в семь реплик можно. Однако важно помнить про OOM, потому что используя TimeSlicing мы рискуем превысить лимит, но в рамках уже одной партиции.

Заключение


В этой статье мы изучили, как можно использовать MIG и TimeSlicing в Kubernetes, а также показали реальный пример инференс-сервера с автомасштабированием. Мы познакомились с оператором GPU, который позволяет достаточно просто динамически управлять шерингом видеокарт в Kubernetes. И напоследок — разобрали варианты решения проблемы с OOM.

Готовая платформа с шерингом GPU

Недавно мы имплементировали автоматическую разметку MIG и TimeSlicing в нашей ML-платформе, что позволило запускать дополнительные параллельные Jupyter-инстансы на одной видеокарте, а также очереди экспериментов в ClearML. Но об этом уже в следующей статье — следите за обновлениями в нашем блоге на Хабре!

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


ссылка на оригинал статьи https://habr.com/ru/articles/756934/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *