Мы закупили шестьдесят четыре ускорителя A100, распихали их по стандартным шасси с обычным 100G Ethernet и решили, что сейчас порвём рынок файнтюном тяжёлых LLM. Спустя неделю кластер показал 18% эффективности масштабирования (scaling efficiency), превратив кремний на пару миллионов долларов в очень дорогие обогреватели. Тема, о которой почему-то молчат продавцы железа — NCCL и сеть кластера: почему распределённое обучение упирается не в GPU, а в то, как эти карты общаются между собой.
Если при проектировании кластера для AI ваш бюджет на сеть составляет меньше тридцати процентов от бюджета на видеокарты, вы строите не суперкомпьютер, а дорогую гирлянду.
Обучение на нескольких узлах — это не независимая работа. Как только мы выходим за пределы одной машины, в игру вступает NVIDIA Collective Communications Library, она же NCCL. Это слой, который отвечает за синхронизацию данных. В стандартном Data Parallel обучении каждая карта считает градиенты на своём куске батча, а потом они должны сложиться и усредниться, чтобы веса модели обновились синхронно. Операция называется all-reduce. И вот здесь начинается физика. GPU молотят матрицы с чудовищной скоростью, выплёвывая гигабайты градиентов за миллисекунды. Затем они останавливаются и ждут, пока эти гигабайты пролезут через межузловой линк. В нашем случае это был классический TCP/IP поверх витой пары.
Если копнуть в топологию, NCCL строит логические кольца (Ring) или деревья (Tree) для передачи данных. В алгоритме Ring All-Reduce каждая видеокарта передаёт кусок данных следующей по кругу. Если у вас восемь карт на шине NVLink внутри сервера, этот круг замыкается со свистом на пропускной способности в 600 гигабайт в секунду. Карты общаются друг с другом практически на скорости видеопамяти. Но когда кольцо выходит за пределы сервера, пакеты падают в узкое горлышко межузловой сети.
Сначала данные должны спуститься с видеокарты по шине PCIe. Без аппаратного RDMA вы не можете использовать прямой доступ к памяти соседей. Данные копируются в оперативку, центральный процессор берёт их, заворачивает в TCP-пакеты, считает контрольные суммы и только потом отдаёт сетевой карте (NIC). Здесь срабатывает закон слабого звена. Скорость всего логического кольца определяется самым медленным линком. Если хотя бы один коммутатор по пути решил буферизовать трафик или отбросить пакет, все шестьдесят четыре карты в кластере синхронно сбрасывают обороты.
Мы наивно рассчитывали на communication/computation overlap — перекрытие вычислений и передачи данных. Идея проста: пока считается следующий слой бэкпропа, градиенты предыдущего уже летят по сети. В теории это позволяет спрятать сетевые задержки за вычислительной нагрузкой. Но перекрытие работает только тогда, когда сеть способна прожевать данные быстрее, чем железо их генерирует. В профайлере PyTorch мы видели чудовищную картину. Вместо красивой диаграммы, где блоки связи идут параллельно с математикой, мы получили строгую последовательность. Сеть была настолько медленной, что очередь передачи забивалась мгновенно. Слой градиентов не успевал улететь до того, как был готов следующий. График утилизации чипов напоминал редкий штакетник: короткий пик вычислений, а затем долгая полка ожидания завершения синхронизации.
Попытка затащить на эту инфраструктуру Fully Sharded Data Parallel (FSDP) обернулась катастрофой. Чтобы уместить 70-миллиардную модель в память, FSDP режет веса, градиенты и состояния оптимизатора на куски и раздаёт разным узлам. Звучит логично, пока вы не смотрите на сетевой профиль. Для того чтобы сделать форвард-пасс, каждой видеокарте нужны полные веса текущего слоя. Она запрашивает недостающие куски у соседей — это операция all-gather. Как только слой посчитан, эти куски весов удаляются из памяти. На бэкворд-пассе всё повторяется: снова all-gather для весов, вычисление градиентов, а затем reduce-scatter, чтобы размазать посчитанные градиенты обратно по кластеру.
Вы променяли ограничения по памяти на сетевой трафик. Объем пересылаемых данных взлетает в небеса. И вот тут наша стогигабитная сеть просто умерла. Стек ядра Linux начал захлёбываться прерываниями. Процессоры ушли в стопроцентную загрузку, обрабатывая входящие пакеты, потому что ядро физически не успевало парсить TCP-заголовки на такой скорости. Задержки (latency) улетели в сотни миллисекунд. Распределённое обучение жёстко синхронно. Хвосты задержек p99 в Ethernet убивают всё: вы можете иметь средний пинг в доли миллисекунды, но если один пакет летит десять миллисекунд, эффективная производительность всего суперкомпьютера падает до этого уровня.
Если с Data Parallel мы просто ждали синхронизации градиентов, то попытка запустить Pipeline Parallelism показала нам, как выглядит настоящая деградация. В пайплайн-параллелизме мы режем саму модель на слои и раскладываем их по разным узлам. Первая машина считает первые десять слоёв трансформера и передаёт активации на вторую машину по сети. Вторая считает свои слои и передаёт дальше. Задержка при передаче активаций заставляет последующие узлы сидеть в так называемом пузыре (pipeline bubble). Железо ждёт данных, чтобы начать работу. Чем выше latency вашей сети, тем больше этот пузырь, и тем меньше полезного времени работают транзисторы.
Пришлось остановить эксперименты и начать считать. Первое правило отладки кластера — никогда не верить заявленной скорости портов. Нам нужна была эффективная алгоритмическая пропускная способность на уровне приложения. Мы запустили стандартный бенчмарк nccl-tests, принудительно выставив отладочные флаги для сетевой подсистемы.
NCCL_DEBUG=INFO NCCL_DEBUG_SUBSYS=INIT,NET \
NCCL_ALGO=Ring,Tree \
mpirun -np 64 -hostfile nodes.txt \
-x NCCL_SOCKET_IFNAME=eth0 \
/opt/nccl-tests/build/all_reduce_perf -b 8 -e 8G -f 2 -g 1
Когда вы смотрите на вывод nccl-tests, нужно понимать разницу между двумя ключевыми метриками. Первая — это busBandwidth, фактическая скорость передачи байт по физической шине. Вторая — algoBandwidth, полезная пропускная способность алгоритма. Из-за особенностей коллективных операций алгоритмическая пропускная способность всегда ниже физической. На идеальной сети 100G мы должны были видеть algoBandwidth в районе 10 гигабайт в секунду для крупных тензоров. Мы видели едва ли 4 гигабайта в секунду, и эти цифры непредсказуемо скакали.
В логах мы увидели, что NCCL честно пытался поднять RDMA-соединения, не находил соответствующих адаптеров, ругался в stderr и откатывался на сокеты. На мелких тензорах latency была неприличной из-за накладных расходов на системные вызовы. На крупных тензорах мы упирались в полосу пропускания, но графики прыгали из-за инкаст-трафика (incast traffic). Когда десятки узлов одновременно начинают передавать данные одному узлу, буферы на коммутаторах переполняются за микросекунды. Срабатывает механизм congestion control, окно передачи схлопывается, и пропускная способность падает в разы. Физику нельзя обмануть патчами конфигурации.
Мы пытались выжать максимум. Включили gradient bucketing — механизм, который собирает мелкие градиенты в большие куски памяти перед отправкой, чтобы снизить количество вызовов сетевого API. Тюнили размеры этих бакетов до посинения. Слишком маленький бакет — сеть захлёбывается мелкими пакетами. Слишком большой — вы убиваете overlap, потому что ускоритель ждёт накопления огромного массива данных. Мы настраивали CPU-аффинность для сетевых прерываний, прибивали процессы к нужным NUMA-нодам, чтобы избежать кросс-сокетного трафика. Это дало прибавку в жалкие несколько процентов.
Взрослые дяди не просто так строят кластеры на InfiniBand или на RoCEv2 (RDMA over Converged Ethernet). Технология RDMA позволяет сетевой карте одного сервера писать данные напрямую в память видеокарты другого сервера, вообще минуя процессор и ядро операционной системы. Задержки падают до микросекунд. Но RoCEv2 требует идеальной настройки сети без потерь (lossless network) — приоритетного управления потоком (Priority Flow Control) и явного уведомления о перегрузке (ECN). Настроить это в масштабах стойки — чёрная магия. Чуть ошибся с порогами срабатывания на коммутаторах, и сеть ловит шторм pause-фреймов, когда порты каскадно блокируют друг друга. InfiniBand в этом плане надёжнее, так как кредит-базированное управление потоком встроено в протокол аппаратно. Железо Mellanox стоит космических денег, но без него ускорители простаивают.
Мы пересчитали экономику провала. Выяснилось, что нам было бы дешевле и эффективнее купить в два раза меньше серверов, но упаковать каждый максимальным количеством карт с шиной NVSwitch, чем пытаться масштабироваться дешёвыми узлами. Внутри NVSwitch карты видят единое адресное пространство, там нет TCP-стека и нет потери пакетов. Трейд-офф предельно честный: без быстрой, низколатентной сети на базе RDMA 3D-параллелизм и FSDP не просто бесполезны, они парализуют инфраструктуру. В итоге мы разобрали этот кластер, перевели машины на инференс, где межузловая сеть почти не нагружена, и начали проектировать новый, но уже отталкиваясь от архитектуры коммутаторов, а не от терафлопсов.