Почему ручная настройка сети — это ад, особенно когда у тебя три RTX и куча энтузиазма
Ты купил вторую видеокарту. Потом третью. Собрал кластер из двух старых игровых ПК. И уперся в стену: распределенное обучение большой языковой модели требует, чтобы узлы видели друг друга. Стандартный подход — прописывать IP-адреса в hosts-файлах, настраивать статические адреса в DHCP, или, что еще хуже, поднимать полноценный Kubernetes. Это убивает весь кайф от эксперимента.
А что если сеть сама находит все GPU-ноды? Именно это решают протоколы mDNS (multicast DNS) и ZeroConf (Zero Configuration Networking). Они позволяют устройствам в локальной сети автоматически регистрировать и находить сервисы — без DNS-сервера, без ручного вбивания адресов. Звучит как магия, но работает это на стеке, которому уже лет 20 (Bonjour от Apple и Avahi в Linux).
Суть проста: каждая машина с GPU публикует сервис (например, _pytorch._tcp) и слушает multicast-запросы. Другие узлы видят его по имени вроде gpu-node-2.local. Никаких IP, никаких файлов /etc/hosts.
В этой статье я покажу, как прикрутить mDNS+ZeroConf к вашему домашнему кластеру для тренировки LLM. Мы не будем обсуждать все тонкости распределенного обучения — для этого есть отдельный разбор мульти-нод конфигураций. Наша цель — сделать так, чтобы torchrun сам находил доступные GPU по всей сети.
Что нам нужно: маленькая магия Avahi
В мире Linux стандарт ZeroConf реализует демон Avahi. Он поставляется почти во всех дистрибутивах, но часто не установлен по умолчанию. Ставим одной командой:
sudo apt install avahi-daemon avahi-utils avahi-autoipd # Debian/Ubuntu
sudo systemctl enable --now avahi-daemonНа macOS mDNS уже встроен (Bonjour), на Windows — придется ставить Bonjour Print Services или использовать WSL2 с Avahi.
Теперь каждая машина должна объявить свой сервис. Создаем файл /etc/avahi/services/torch.service:
<?xml version="1.0" standalone='no'?>
<!DOCTYPE service-group SYSTEM "avahi-service.dtd">
<service-group>
<name>gpu-node-1</name>
<service>
<type>_torch._tcp</type>
<port>29500</port> <!-- стандартный порт NCCL -->
</service>
</service-group>Перезапускаем Avahi: sudo systemctl restart avahi-daemon. Теперь другие узлы могут найти этот сервис по имени gpu-node-1.local. Проверяем:
avahi-browse -a -tЕсли видите в выводе _torch._tcp — всё работает.
Внимание: mDNS работает только в пределах одного широковещательного домена (в одной подсети /24). Если ваши машины разбросаны по разным VLAN — ZeroConf не поможет, придется настраивать традиционный DNS.
Сравниваем: что мы выигрываем по сравнению с альтернативами
Давайте честно: вы можете обойтись и без mDNS. Например:
- Статические IP / DHCP резервация. Работает, но если вы переставляете железо или подключаете ноутбук — всё слетает. Нужно редактировать каждый узел.
- DNS-сервер (BIND, dnsmasq). Избыточно для 2-5 машин. Требует отдельной настройки и обслуживания.
- Kubernetes / Slurm. Мощно, но тяжеловесно. Если ваша цель — просто дообучить Llama 3 на паре домашних ПК, поднимать целый оркестратор — стрельба из пушки по воробьям.
mDNS даёт автоматическое обнаружение: вы включаете новый узел с GPU — кластер видит его через 2 секунды. Никаких конфигов, никаких рестартов DNS. Это уровень plug-and-play, которого так не хватает домашним кластерам.
Из недостатков: mDNS не масштабируется на десятки машин (трафик multicast может забить сеть), и не подходит для изолированных подсетей. Но для домашнего кластера из 2-6 GPU-узлов — идеально.
Как подключить это к PyTorch Distributed и DeepSpeed
Теперь самое интересное: заставить PyTorch использовать mDNS-имена. Допустим, у вас две машины: gpu-node-1.local и gpu-node-2.local. Стандартный способ запуска распределенного обучения — через torchrun:
torchrun --nnodes=2 --nproc_per_node=1 --rdzv_backend=c10d --rdzv_endpoint=gpu-node-1.local:29500 train.pyНо как быть, если вы не знаете заранее, кто главный? ZeroConf может динамически выбирать мастер-узел. Пишем небольшой Python-скрипт, который использует avahi-browse или библиотеку python-zeroconf:
from zeroconf import Zeroconf, ServiceBrowser
import socket
class MyListener:
def __init__(self):
self.services = []
def add_service(self, zc, type_, name):
info = zc.get_service_info(type_, name)
if info:
addr = socket.inet_ntoa(info.addresses[0])
port = info.port
self.services.append((addr, port))
zc = Zeroconf()
listener = MyListener()
browser = ServiceBrowser(zc, "_torch._tcp.local.", listener)
# ждем 2 секунды для обнаружения
import time
time.sleep(2)
print("Найдены узлы:", listener.services)
zc.close()Этот скрипт находит все машины с сервисом _torch. Теперь можно динамически выбрать мастер (первый в списке) и передать его адрес в torchrun. А если вы только собираете рабочую станцию — почитайте гайд по железу.
Для DeepSpeed конфиг deepspeed_config.json содержит параметры сети. Вместо жесткого IP укажите mDNS-имя: "master_addr": "gpu-node-1.local". DeepSpeed взаимодействует с NCCL, а NCCL умеет резолвить .local через mDNS. Единственная тонкость — убедитесь, что библиотека libnss-mdns установлена на каждом узле (она подключает mDNS к системному резолверу).
Живой пример: обучаем Llama 3.2-8B на двух RTX 3090
Допустим, у вас есть две машины с одним GPU каждая (RTX 3090, 24 ГБ). Вы хотите дообучить Llama 3.2 8B с LoRA. Полный запуск:
- На обеих машинах установлен Avahi, объявлен сервис
_torch._tcp. - На мастер-ноде (первой) запускаем скрипт обнаружения, получаем список узлов.
- Скрипт выбирает мастером себя, остальные подключаются к нему.
- Запускаем
torchrunсrdzv_endpoint=master.local:29500.
Фактически команда на всех узлах одна и та же (кроме роли мастера, которую определяет скрипт). Пример скрипта запуска:
#!/bin/bash
MASTER=$(python discover_master.py) # возвращает hostname
$MASTER_ADDR="$MASTER"
torchrun --nnodes=2 --nproc_per_node=1 --rdzv_backend=c10d --rdzv_endpoint=${MASTER}:29500 finetune_llama.pyЭтот же скрипт работает на любом узле. Если мастер упал — достаточно перезапустить, и новый мастер будет выбран автоматически. Кстати, о выборе LLM — обзор лучших моделей с Tool Calling.
c10d rendezvous backend — самый простой для домашних условий. Для продакшна есть etcd или consul, но здесь они избыточны.Грабли, на которые я наступил
ZeroConf выглядит красиво, но есть детали:
- Задержка обнаружения. Время жизни записей (TTL) в mDNS обычно 120 секунд. Если узел выключился, другие не узнают об этом мгновенно. Приходится использовать health-check через отдельные PING-сообщения.
- Имена .local конфликтуют с реальными доменами. Если в вашей сети есть домен .local — будет коллизия. Лучше использовать свою метку, например
gpu-cluster.local. - NCCL на некоторых драйверах не резолвит mDNS. Если
torchrunпадает с ошибкойHost not found— проверьтеnsswitch.confи установитеlibnss-mdns. Либо используйте IP-адреса, полученные через Python-скрипт. - Windows-узлы. Нативную поддержку mDNS в Windows подружить с PyTorch сложно. Проще запускать все на Linux или через WSL2.
Кому это реально нужно (а кому нет)
Подход с mDNS+ZeroConf — для тех, у кого от 2 до 6 машин с GPU, стоящих в одной локальной сети. Он идеален, если вы:
- Хотите быстро подключать новый GPU-сервер без правки конфигов.
- Строите домашний кластер из того, что есть под рукой (игровые ПК, ноутбуки с eGPU).
- Экспериментируете с распределенным обучением и не хотите тратить время на DevOps.
- Нужна минимальная точка отказа: если мастер упал, любой другой узел может стать мастером.
Но не стоит использовать этот метод, если у вас больше 10 узлов, узлы находятся в разных подсетях, или вам нужна полная изоляция от multicast-трафика. Для промышленных нагрузок лучше сразу смотреть на Slurm или Kubernetes — хотя для локального запуска моделей есть и более простые инструменты.
Лично я использую такую схему на четырех RTX 3090 (два ПК по 2 карты). Настройка заняла 30 минут, и с тех пор я забыл про /etc/hosts. Новую машину подключил — через 5 минут она уже участвовала в тренировке. Это тот случай, когда «просто работает» не требует жертв.
Альтернативный путь — использовать готовый LLM-сервер с поддержкой кластеризации, как Oobabooga. Но гибкость, которую дает прямой Avahi + PyTorch, того стоит.