От нуля до GPUaaS, или Как завести в Kubernetes технологии InfiniBand, RDMA и GPUDirect

На рынке мало экспертизы по запуску GPU-стека и InfiniBand именно внутри Kubernetes (обычно их настраивают на «голом железе» / bare-metal), а официальная документация NVIDIA имеет пробелы и в ру-комьюнити готовых кейсов нет. Под капотом я расскажу, как мы запустили GPUaaS (GPU как услуга) для ИИ-команд, которым нужны вычислительные мощности и благодаря этому за 3 месяца с нуля развернули и вывели в продакшен готовый стек для работы с GPU, решили проблему простоя дорогого оборудования и обеспечили команды разработчиков вычислительными мощностями в критически сжатые сроки.

Сроки. Запуск базовой инфраструктуры занял всего 3 месяца (вместо стандартных нескольких кварталов). Да, не идеально: за 3 месяца трудно сделать крутую инфру. Но базово все команды уже могли использовать эти технологии.

Изоляция команд. Мы настроили виртуальные функции и партиции InfiniBand, с помощью которых команды изолированы друг от друга на уровне видеопамяти и интерфейсов — доступ к чужим ресурсам исключен.

Автоматизация. Полностью избавились от рутины. Развертывание ОС автоматизировано через Ansible, а управление квотами и ресурсами — через Argo CD.

GPUaaS — это платформа с общим пулом ресурсов и единым способом их получения. Теперь вместо того, чтобы ждать закупки GPU под каждый проект (месяцы ожидания), мы можем выдавать командам мощности из готового пула — on-prem и облако.

В этой статье я расскажу про open-source-решения, которые вы можете использовать и перенять для реализации своей инфраструктуры.

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

Предыстория

Предыстория проста — командам был необходим доступ к мощным и высокопроизводительным GPU. Весь стек до вывода в продакшен мы развернули за три месяца.

  • Запустили базовые технологии, которыми команды уже могли пользоваться.

  • Мы не использовали лицензионные решения, а использовали только Open Source, что сэкономило деньги.

  • Мы изолировали команды с помощью партиций (Partition Key).

  • Автоматизировали рутину через GitOps, что снизило количество ошибок.

Загвоздка в том, что отказаться от Kubernetes мы не могли — платформа изначально задумывалась на нём. Поэтому мы стали изучать опыт других компаний: сами мы с такой задачей раньше не сталкивались.

Удивительно, но на рынке все уже слышали про кластеры Kubernetes, а все, кто администрирует и поддерживает их, знают, что у Kubernetes большое комьюнити и “плюшки” в виде декларативного управления ресурсами и устойчивости к сбоям, но ресерч рынка быстро выявил, что в RU-комьюнити информации по этой теме мало, так как в среднем по больнице все используют классические Linux-серверы, а оркестрацию и Kubernetes при работе с GPU и технологиями InfiniBand и GPUDirect не используют. Да, на текущий момент есть уже множество компаний, которые используют Kubernetes с GPU, но без технологий GPUDirect RDMA, не говоря уже об изоляции команд друг от друга по видеопамяти…
На момент развертывания платформы в документации NVIDIA некоторые части были не описаны, так что пришлось все проходить методом проб и ошибок.

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

№1. Настройка GPU Operator: MIG и Time Slicing

Начнем с азов — с операционной системы для работы GPU-оператора. 

Устанавливаем пакеты

Первоначально на хост необходимо предустановить пакеты для работы с сетевыми картами.

Луковица по пакетамЛуковица по пакетам

А минимально можно предустановить пакет DOCA-Networking, который будет в себя включать doca-ofed/roce/host-basic и нативно уже иметь большинство утилит, которые необходимы для работы с IB. Либо можно установить DOCA-ALL, который будет включать в себя все те пакеты, которые изображены на картинке (более подробно что находится в том или ином пакете можно ознакомиться тут).

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

  • nvidia-driver

  • nvidia-fabricmanager 

  • nvidia-utils

Примечание. В рамках Helm Chart самого GPU-Operator также можно использовать внутренний компонент драйвера, который может предустанавливать драйверы. Но на момент, когда мы только развертывали платформу в закрытом контуре, мы выбрали пойти по пути с предустановкой на нодах самостоятельно (далее можете приглядеться к инсталляции драйверов через GPU operator, чтобы он самостоятельно их устанавливал)
nvidia-fabricmanager нужен только на системах с NVSwitch (HGX/DGX) и должен строго совпадать по версии с драйвером.
После предустановки драйверов необходимо включить модуль nvidia_peermem — он нужен для GPUDirect RDMA (об RDMA и GPUDirtct расскажу ниже)

modprobe nvidia-peermemecho nvidia-peermem > /etc/modules-load.d/nvidia-peermem.conflsmod | grep nvidia_peermem

Примечание. nvidia_peermem является legacy, в более свежих версиях драйверов и лучшей практикой будет использовать DMA-BUF

Теперь перейдем к Helm. 

Уточняем values

Сам Chart GPU-оператора не такой уж и трудный, но есть нюанс работы с MIG. 

MIG — это технология, которая позволяет не программно, а аппаратно поделить одну физическую карту максимум на 7 инстансов. Каждый инстанс имеет свои вычислительные ядра, кэш и память, то есть они изолированы. Процессы внутри одного инстанса физически не могут залезть в память или забрать ядра у соседа — каждый инстанс работает как отдельная мини-видеокарта.

Карта делится максимум на 7 инстансов: например, у H100 7 вычислительных срезов и 8 срезов памяти, а каждому инстансу нужен хотя бы один вычислительный срез. Самый крупный профиль 7g занимает все 7 вычислительных срезов и все 8 срезов памяти. Если MIG на карте не нужен вообще, правильнее просить обычную цельную карту на ноде без MIG, а не занимать 7g — это не бесплатно: MIG-режим отключает NVLink P2P между картами.

Стратегии

Как именно нарезать видеокарты, задаётся в MIG Manager. Есть две стратегии: single и mixed.
При single device plugin публикует каждый MIG-инстанс как обычный ресурс nvidia.com/gpu. При mixed каждый инстанс публикуется отдельным ресурсом под свой профиль, например nvidia.com/mig-1g.10gb, а GPU с выключенным MIG остаются nvidia.com/gpu. 

Саму нарезку задают конфиг MIG Manager и label nvidia.com/mig.config на ноде. 

Конфиг с профилями нарезки подключается в values через migManager.config:

mig: strategy: mixedmigManager: enabled: true resources: {} config: default: all-disabled name: nvidia-config-mig-mixed

default: all-disabled — по умолчанию MIG отключен и лейбл на нодах выставлен all-disabled
strategy: mixed — выбранная стратегия

name: nvidia-config-mig-mixed — название конфигурационного файла (ConfigMap), где описано как делить карту
Пример конфига:

apiVersion: v1kind: ConfigMapmetadata: name: nvidia-config-mig-mixeddata: config.yaml: |- version: v1 mig-configs: all-disabled: - devices: "all" mig-enabled: false 1x4g.40gb-3x1g.10gb: - devices: "all" mig-enabled: true mig-devices: "4g.40gb": 1 "1g.10gb": 3

И чтобы применить профиль, для ноды достаточно изменить лейбл на nvidia.com/mig.config=1x4g.40gb-3x1g.10gb
Более подробно о том, как разбить видеокарту, можно прочитать здесь

Time Slicing

В «противовес» MIG у нас есть программный шеринг — Time-Slicing (разделение по времени). Time Slicing — это режим, когда в рамках одной физической GPU-карты несколько подов или процессов используют одну видеокарту. Вычисления делятся по очереди, квантами времени, а видеопамять у всех общая. Если один процесс сильно нагружает ядра, остальные ждут своего кванта и работают медленнее. Если он «забивает» всю память, остальные падают с ошибкой Out of Memory (OOM). 

Для Time Slicing тоже подключается свой ConfigMap, в котором задаётся, на сколько реплик делить карту. 

devicePlugin: config: name: nvidia-config-time-slicing

Пример ConfigMap

apiVersion: v1kind: ConfigMapmetadata: name: nvidia-config-time-slicing namespace: gpu-operatordata: time-slicing: |- version: v1 flags: migStrategy: mixed sharing: timeSlicing: failRequestsGreaterThanOne: true resources: - name: nvidia.com/gpu replicas: 2

№2. Network Operator: RDMA, InfiniBand, GPUDirect

Перейдем к Network Operator. Много кто использует GPU Operator с Kubernetes, но мало кто использует InfiniBand, RDMA и GPUDirect в Kubernetes. Поэтому я бы сначала хотел пройтись по базовым понятиям.

RDMA

Без RDMA данные между нодами идут через сетевой стек ядра. Сетевая карта кладёт пакет в буфер ядра, CPU копирует его в память приложения, затем приложение копирует данные в видеопамять. Каждый шаг нагружает CPU и добавляет задержку.

RDMA (Remote Direct Memory Access) позволяет сетевой карте писать данные прямо в память приложения на удалённой ноде, минуя ядро и CPU.

InfiniBand

RDMA работает поверх InfiniBand или RoCE (RDMA over Converged Ethernet). IB из коробки даёт сеть без потерь и низкие задержки, а RoCE требует аккуратной настройки PFC/ECN на всех свитчах. Кроме того, InfiniBand заложен в эталонную архитектуру NVIDIA DGX SuperPOD. RoCE тоже рабочий вариант для больших кластеров, просто не наш случай.

Итак, InfiniBand — это сетевая технология, которая позволяет добиться пропускной способности до 200, 400 и даже до 800 Гбит/с с крайне низкими задержками. В основном используется в суперкомпьютерах, а также в системах с ИИ. 

GPUDirect

Если RDMA может миновать CPU и обращаться к оперативной памяти, то GPUDirect RDMA идёт дальше: сетевая карта читает и пишет прямо в видеопамять GPU по шине PCIe, и данные вообще не попадают в оперативную память. GPUDirect — это семейство технологий NVIDIA (есть ещё GPUDirect P2P и GPUDirect Storage); в статье речь о GPUDirect RDMA. Работает она только поверх RDMA.

С базовыми понятиями определились. Перейдем к Network Operator и его настройке. 

Начнём непосредственно с настройки RDMA. 

Настройка RDMA

Ниже блок настройки плагина для предоставления подам прямого доступа к сетевым картам Mellanox через RDMA. 

rdmaSharedDevicePlugin: image: k8s-rdma-shared-dev-plugin repository: ghcr.io/mellanox version: v1.5.2 imagePullSecrets: [] config: | { "configList": [ { "resourcePrefix": "nvidia.com", "resourceName": "rdma", "rdmaHcaMax": 500, "selectors": { "vendors": ["15b3"], "drivers": ["mlx5_core"] } } ] }

При настройке RDMA в Network Operator конфигурация выносится отдельно через NicClusterPolicy.

Важные параметры для инициализации ресурсов в Kubernetes:

  • Указания ресурсов («resourcePrefix»), которые будут потом инициализироваться и отображаться в Kubernetes.

  • rdmaHcaMax — количество единиц ресурса nvidia.com/rdma.

  • Селекторы для поиска RDMA-устройства.

rdmaHcaMax

Если с resourcePrefix и селекторами в целом понятно, для чего они нужны, то что такое rdmaHcaMax? 

rdmaHcaMax — сколько единиц ресурса nvidia.com/rdma плагин объявит на ноде, то есть сколько подов могут одновременно получить доступ к RDMA-устройству. Это административный лимит: аппаратные ресурсы карты (QP, MR и т.д.) плагин не считает, поэтому значение подбирается так, чтобы поды не исчерпали их раньше (более подробно о QP/MR и как работают очереди можно ознакомиться по ссылке в блоке “Полезные ссылки”).

Настройка SR-IOV

SR-IOV — технология, позволяющая делить одну сетевую карту на несколько виртуальных функций (VF).

В рамках RDMA контексты выделяются на интерфейс. Если мы делим интерфейс, контексты распределяются между виртуальными функциями. Каждая виртуальная функция имеет свой GUID и изоляцию.

Для использования SR-IOV необходимо:

№1. Включить SR-IOV и VT-d в BIOS, а в параметры ядра добавить intel_iommu=on iommu=pt

№2. Настроить деление на хосте. 

echo 6 > /sys/class/infiniband/mlx5_0/device/sriov_numvfs# Для VF0 порт (type 01)echo "00:00:01:01:0a:00:00:01" > /sys/class/infiniband/mlx5_0/device/sriov/0/port# Для VF0 нода (type 02)echo "00:00:01:01:0a:00:00:02" > /sys/class/infiniband/mlx5_0/device/sriov/0/node# Установить политикуecho "Follow" > /sys/class/infiniband/mlx5_0/device/sriov/0/policy# Перезагрузить VFVF_PCI=$(basename "$(readlink -f /sys/class/infiniband/mlx5_0/device/virtfn0)") # например 0000:0a:00.2echo $VF_PCI > /sys/bus/pci/drivers/mlx5_core/unbindecho $VF_PCI > /sys/bus/pci/drivers/mlx5_core/bind

Объясню подробнее по порядку.

№1. Сначала прописываем деление на виртуальные функции. В документации NVIDIA описано деление на 8 виртуальных функций. Мы методом проб и ошибок выяснили, что на текущей инфраструктуре стабильно работают только 6 виртуальных функций, так как работать с 8 или 7 мы не могли — они просто не отображались (вероятная причина — лимит NUM_OF_VFS в прошивке (mlxconfig -d query), но на момент инсталляции платформы и с текущими нагрузками для нас было достаточно и 6).

№2. Далее для каждой виртуальной функции необходимо сгенерировать и прописать GUID ноды и порта (скрипт можно найти в полезных ссылках).

№3. Установить политику состояния виртуального порта. Доступны три режима: Down удерживает порт выключенным; Up разрешает его инициализацию, после чего OpenSM может перевести его в рабочее состояние; Follow разрешает инициализацию только тогда, когда физический порт находится в состоянии Active, а иначе удерживает виртуальный порт в Down. Мы используем Follow, чтобы состояние виртуального порта зависело от доступности физического подключения. Предварительно устанавливать Up не требуется.

№4. Перепривязать VF к драйверу (unbind/bind) по её PCI-адресу. 

Теперь посмотрим на конфигурационный файл для SR-IOV Network Device Plugin, который тоже задается в NicClusterPolicy (его можно задать следующим блоком после rdma).

sriovDevicePlugin: config: | { "resourceList": [ { "resourcePrefix": "nvidia.com", "resourceName": "sriov8001", "selectors": { "vendors": ["15b3"], "drivers": ["mlx5_core"], "pKeys": ["0x8001"], "isRdma": true } }, ... ] }

В целом, здесь нет ничего сверхъестественного, но я бы хотел обратить внимание на пару моментов. А именно на строку pKeys, которая в себе несет селектор на значение pKeys, которые мы самостоятельно формируем — о них ниже.

Управление топологией: OpenSM и Partition Key

Когда виртуальных функций много (например, 100 нод умножить на 6 функций = 600 VF), управлять ими сложно. На помощь приходит OpenSM — Subnet Manager InfiniBand, отвечающий за инициализацию оборудования InfiniBand и, в целом, его настройку. Партиции и их ключи (PKey) вы описываете в partitions.conf, а OpenSM прописывает эти ключи в PKey-таблицы портов. Так каждый порт получает доступ только к тем партициям, в которые он включён.

С помощью PKeys (Partition Keys) OpenSM управляет безопасностью и изоляцией. Когда прописан Partition Key, плагин отберет только те виртуальные сетевые интерфейсы, которые привязаны к определенной изолированной партиции, что облегчает менеджмент и обеспечивает изоляцию команд разработки (одна VF из партиции №1 не может обратиться к другой VF из партиции №2).

Настройка партиций

У OpenSM есть возможность объединения виртуальных функций в группы.

### Default_Partition ###Default = 0x7fff , indx2, ipoib :SELF=full,ALL_SWITCHES=full;### Partition_1 ###Partition_1 = 0x0001 , indx0 , ipoib :### ai-worker-one ###=full,=full,### ai-worker-two ###=full,=full;### Partition_2 ###...

Из основного что хотел бы тут подметить, так это партиции можно делить и указать виртуальные функции в этих партициях как угодно. В нашем случае мы сделали так, что у нас первая виртуальная функция попадает в первую партицию — Partition_1 = 0x0001 , indx0 , ipoib — , вторая во вторую партицию и так далее до 6. При добавлении новой worker node мы точно также делим на 6 виртуальных интерфейсов и добавляем в уже созданные партиции. Тогда у нас уже получается целых шесть партиций, которые легче администрировать и также партиции изолированы друг от друга.

При работе с OpenSM, обратите внимание на выбор роутинга. В нашем случае и в нашей топологии сети хорошо подходит адаптивный роутинг — это когда динамически балансируется нагрузка и выбирается более-менее свободный порт и канал (если грубо говоря, то можно сравнить с leastconn). Важный момент, необходимо задать в конфигурации virt_enabled 2, без этого VF не заработают.

И также в partitions.conf партиция объявляется как 0x0001, а в PKey-таблице порта она появляется как 0x8001: старший бит означает полное членство (full). Поэтому в конфигах Kubernetes будет фигурировать 0x8001.

№3. Kubernetes

Мы объявили ресурсы в Kubernetes, теперь нужно их использовать.

Стандартные CNI (Calico, Cilium) выделяют в поде только один сетевой интерфейс. Для работы с InfiniBand нам нужен второй сетевой интерфейс в под.

На помощь приходит Multus, который работает в паре с CNI и позволяет добавлять дополнительные сети.

  • Он работает не вместо основных CNI (например Calico/Cilium), а в паре с ними.

  • Основные CNI (Calico/Cilium) по-прежнему управляют первым интерфейсом, а Multus CNI «пробрасывает» в под второй интерфейс специально под тяжелый ИИ-трафик.

  • Через Multus можно прокинуть не только InfiniBand, но и любую другую физическую сеть (например, выделенный VLAN со статическими IP-адресами).

Важно отметить, что на нодах у вас обязательно должен быть бинарник CNI-плагина ib-sriov в директории /opt/cni/bin, так как он использует этот бинарь для пробрасывания интерфейса, если его не будет, то при созаднии пода у вас pod выпадет в ошибку.

В Multus используется ресурс Network Attachment Definition (NAD), который описывает, что мы будем закидывать в под Kubernetes. Здесь указывается название партиции (наш partition key, прописанный в partitions.conf OpenSM). Если партиций несколько, создаем NAD на каждую из них:

apiVersion: k8s.cni.cncf.io/v1kind: NetworkAttachmentDefinitionmetadata: name: infiniband-net-8001 annotations: k8s.v1.cni.cncf.io/resourceName: nvidia.com/sriov8001spec: config: | { "cniVersion": "0.3.1", "name": "infiniband-net-8001", "type": "ib-sriov", "pkey": "0x8001", "ipam": { "type": "static" } }

Все ресурсы поставили, все инициализировали, все работает. Что дальше?

Пришло время тестирования. 

№4. Тестирование

Для создания пода необходимо указать ресурсы (RDMA, SR-IOV), аннотацию с именем Network Attachment и права для работы с RDMA.

RDMA InfiniBand

resources: limits: cpu: 2 memory: 4Gi nvidia.com/gpu: 1 nvidia.com/sriov8001: 1 requests: cpu: 2 memory: 4Gi nvidia.com/gpu: 1 nvidia.com/sriov8001: 1

Для пода нужно указать ресурсы, которые мы инициализировали, — RDMA, SR-IOV. Также необходимо указать аннотацию с именем Network Attachment, который мы создали ранее.

annotations: k8s.v1.cni.cncf.io/networks: | [ { "name": "infiniband-net-8001" } ]

Примечание. IP RDMA-трафику не нужен, данные идут по LID/GID, а служебный обмен идёт по основной сети пода. IP понадобится, только если соединение устанавливается через rdma_cm (MPI/UCX, NVMe-oF, ib_write_bw с -R), нужен IPoIB-трафик или bootstrap обучения должен идти по IB.

И права для работы с RDMA. 

securityContext: capabilities: add: [ "IPC_LOCK" ]

Синтетический тест с помощью утилит IB . Мы создаем один pod с сервером:

ib_write_bw -d mlx5_2 -a -F --report_gbits -q 4

Второй — с клиентом:

ib_write_bw -n 5000 -d mlx5_8 -a -F --report_gbits -q 4 10.244.4.54

10.244.4.54 — IP пода в основной сети, по нему идёт только обмен параметрами по TCP. Адрес в ib_write_bw нужен только для TCP-рукопожатия, а данные в любом случае идут по RDMA через устройство из -d.

Получаем максимальную пропускную способность в кластере — это до 400 Гбит/с:

#bytes

#iterations

BW peak
[Gb/sec]

BW average
[Gb/sec]

MsgRate
[Mpps]

8,388,608

20,000

394.87

394.87

0.005584

По синтетическому тесту все. 

Чтобы проверить сеть на нагрузке, близкой к реальной, я запустил два пода на разных нодах. Каждый под получает одну GPU и одну VF из партиции 0x8001, а IB-интерфейсу (net1) назначается статический IP. Внутри контейнер NGC PyTorch 24.01 и небольшой скрипт на torch.distributed, который гоняет all_reduce через NCCL между двумя ранками.

Пропускную способность скрипт меряет на тензорах от 1 до 256 МБ, по 20 повторов на каждый размер. Задержку — на сообщениях от 8 байт до 16 КБ, по 100 повторов, с расчётом p50 и p99. Время засекается вокруг вызова all_reduce с синхронизацией CUDA, то есть это полное время операции, а не чистая задержка сети.

Тест я прогнал в трёх режимах, меняя только переменные окружения NCCL:

  • GPUDirect RDMA: NCCL_NET_GDR_LEVEL=SYS, NCCL_NET_GDR_READ=1 — данные идут по InfiniBand из видеопамяти одной ноды прямо в видеопамять другой;

  • RDMA без GPUDirect: NCCL_NET_GDR_LEVEL=LOC — данные идут по InfiniBand, но через оперативную память;

  • TCP: NCCL_IB_DISABLE=1 — NCCL переходит на обычные сокеты поверх интерфейса net1.

Служебный обмен NCCL во всех режимах идёт через net1 (NCCL_SOCKET_IFNAME=net1). Что GPUDirect действительно включился, видно по логу NCCL (NCCL_DEBUG=INFO): у сетевого канала стоит пометка GDRDMA.

Пропускную способность считаю как busbw по методике nccl-tests. Для all_reduce на двух ранках она равна размеру тензора, делённому на время операции, и её можно напрямую сравнивать со скоростью линка.

На графике можно увидеть, что:

  • Если используем технологии GPUDirect и RDMA, то можем достичь скорости 51 ГБ/с (400 Гбит/с).

  • Без GPUDirect максимальная скорость (которую я смог достичь) — ~42 ГБ/с (~336 Гбит/с).

  • Без этих технологий (обычный TCP/IP стек) выше ~2 ГБ/с (16 Гбит/с) не поднимается. 

Прирост в пропускной способности в 25 раз в случае с GPUDirect+RDMA против обычного стека TCP.

Дальше проверил latency.

График показывает, что при использовании GPUDirect p99-задержка составила 0,05 мс (50 мкс), что довольно-таки круто. Если сравнивать с обычным стеком без GPUDirect и RDMA, то в пике в районе 0,4-0,5 миллисекунд. Уменьшение задержек в 8 раз. 

В данной статье я постарался подробно рассказать о том, как строить кластеры с GPU, с такими технологиями как GPUDirect + RDMA. И также статья по сути является более подробным гайдом/пересказом моего выступления, которое можно посмотреть тут ))

P.S. В конце статьи я хочу отметить, что при использовании MIG сталкиваешься с кучей проблем, с такими как деление карты в бою под нагрузкой (необходимо идти в историю с согласованиями, выселять нагрузку с карты и только после этого делить, что не совсем удобно, как и то, что у MIG максимально всего 7 инстансов с определенным количеством видеопамяти, которые не дают динамику), мы решили смотреть в сторону более гибкого и динамического HAMi+DRA и уже реализовали весь функционал квотирования и его использования, но для этого необходим k8s >1.35, где DRA уже в stable версии, но об этом могу рассказать в следующей статье…))

Полезные ссылки

  • SRIOV – документация 

  • Настройка ноды для SRIOV – GitHub

  • Platform-Support – nvidia

  • Install Network-Operator – Settings

  • QP RDMA – работа очередей

  • Тестирование – RDMA/GPUDirect / Models

Источник: habr.com

0 0 голоса
Рейтинг новости
1
0
Подписаться
Уведомить о
0 комментариев