От нуля до 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-peermem
echo nvidia-peermem > /etc/modules-load.d/nvidia-peermem.conf
lsmod | 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: mixed
migManager:
enabled: true
resources: {}
config:
default: all-disabled
name: nvidia-config-mig-mixeddefault: all-disabled - по умолчанию MIG отключен и лейбл на нодах выставлен all-disabled
strategy: mixed - выбранная стратегия
name: nvidia-config-mig-mixed - название конфигурационного файла (ConfigMap), где описано как делить карту
Пример конфига:
apiVersion: v1
kind: ConfigMap
metadata:
name: nvidia-config-mig-mixed
data:
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: v1
kind: ConfigMap
metadata:
name: nvidia-config-time-slicing
namespace: gpu-operator
data:
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
# Перезагрузить VF
VF_PCI=$(basename "$(readlink -f /sys/class/infiniband/mlx5_0/device/virtfn0)") # например 0000:0a:00.2
echo $VF_PCI > /sys/bus/pci/drivers/mlx5_core/unbind
echo $VF_PCI > /sys/bus/pci/drivers/mlx5_core/bindОбъясню подробнее по порядку.
№1. Сначала прописываем деление на виртуальные функции. В документации NVIDIA описано деление на 8 виртуальных функций. Мы методом проб и ошибок выяснили, что на текущей инфраструктуре стабильно работают только 6 виртуальных функций, так как работать с 8 или 7 мы не могли — они просто не отображались (вероятная причина — лимит NUM_OF_VFS в прошивке (mlxconfig -d <pci> 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 ###
<GUID>=full,
<GUID>=full,
### ai-worker-two ###
<GUID>=full,
<GUID>=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/v1
kind: NetworkAttachmentDefinition
metadata:
name: infiniband-net-8001
annotations:
k8s.v1.cni.cncf.io/resourceName: nvidia.com/sriov8001
spec:
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.5410.244.4.54 - IP пода в основной сети, по нему идёт только обмен параметрами по TCP. Адрес в ib_write_bw нужен только для TCP-рукопожатия, а данные в любом случае идут по RDMA через устройство из -d.
Получаем максимальную пропускную способность в кластере — это до 400 Гбит/с:
#bytes | #iterations | BW peak | BW average | MsgRate |
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
KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.