Практическое руководство с использованием кода по масштабированию глубокого обучения на разных машинах — от групп процессов NCCL до синхронизации градиентов.
Делиться

1. Введение
У вас есть модель. У вас есть один графический процессор. Обучение занимает 72 часа. Вы запрашиваете вторую машину с четырьмя дополнительными графическими процессорами — и теперь вам нужно, чтобы ваш код действительно их использовал. Именно в этот момент большинство специалистов сталкиваются с трудностями. Не потому, что распределенное обучение концептуально сложно, а потому, что необходимые для его правильной реализации инженерные решения — группы процессов, логирование с учетом ранжирования, начальное значение сэмплера, барьеры контрольных точек — разбросаны по десяткам руководств, каждое из которых охватывает лишь один элемент головоломки.
Эта статья — руководство, которое мне очень пригодилось бы, когда я впервые масштабировал обучение за пределы одного узла. Мы создадим с нуля полноценный, готовый к использованию в производственной среде многоузловой конвейер обучения, используя DistributedDataParallel (DDP) из PyTorch. Каждый файл модульный, каждое значение настраиваемое, и каждая концепция распределенного обучения явно описана. В итоге у вас будет кодовая база, которую вы сможете установить в любой кластер и немедленно начать обучение.
Что мы рассмотрим: ментальную модель, лежащую в основе DDP, чистую модульную структуру проекта, распределенное управление жизненным циклом, эффективную загрузку данных по рангам, цикл обучения со смешанной точностью и накоплением градиента, логирование и контрольные точки с учетом ранга, скрипты запуска многоузловых систем, а также проблемы производительности, с которыми сталкиваются даже опытные инженеры.
Полный исходный код доступен на GitHub. Каждый блок кода в этой статье загружен непосредственно из этого репозитория.
2. Как работает DDP — Ментальная модель
Прежде чем писать какой-либо код, нам необходима четкая мысленная модель. Распределенная параллельная обработка данных (DDP) — это не магия, а хорошо определенная модель взаимодействия, построенная на основе коллективных операций.
Настройка проста. Вы запускаете N процессов (по одному на каждый графический процессор, потенциально на нескольких машинах). Каждый процесс инициализирует группу процессов — канал связи, поддерживаемый NCCL (NVIDIA Collective Communications Library) для передачи данных между графическими процессорами. Каждому процессу присваиваются три идентификационных номера: его глобальный ранг (уникальный на всех машинах), его локальный ранг (уникальный внутри своей машины) и общий размер глобальной среды.
Каждый процесс содержит идентичную копию модели. Данные распределяются между процессами с помощью распределенного выборщика (DistributedSampler) — каждый ранг получает свой собственный фрагмент набора данных, но веса модели изначально (и остаются) одинаковыми.
Ключевой механизм заключается в том, что происходит во время выполнения функции backward(). DDP регистрирует обработчики для каждого параметра. Когда для параметра вычисляется градиент, DDP объединяет его с близлежащими градиентами и запускает операцию all-reduce по всей группе процессов. Эта операция all-reduce вычисляет средний градиент по всем рангам. Поскольку теперь каждый ранг имеет одинаковый усредненный градиент, последующий шаг оптимизатора производит идентичные обновления весов, поддерживая синхронизацию всех реплик — без какого-либо явного кода синхронизации с нашей стороны.
Именно поэтому DDP однозначно превосходит более старую технологию DataParallel: в ней нет единого «главного» узкого места на графическом процессоре, отсутствуют избыточные прямые проходы, а обмен градиентами пересекается с обратными вычислениями.

Ключевые термины
| Срок | Значение |
| Классифицировать | Глобально уникальный идентификатор процесса (от 0 до world_size – 1) |
| Локальный рейтинг | Индекс графического процессора в пределах одной машины (от 0 до nproc_per_node – 1) |
| Размер мира | Общее количество процессов на всех узлах |
| Группа процессов | Канал связи (NCCL), объединяющий все звания. |
3. Обзор архитектуры
Конвейер обучения в производственной среде никогда не должен представлять собой единый монолитный скрипт. Наш разделен на шесть специализированных модулей, каждый из которых выполняет одну единственную функцию. Граф зависимостей ниже показывает, как они связаны между собой — обратите внимание, что файл config.py находится внизу, выступая в качестве единственного источника достоверной информации для каждого гиперпараметра.

Вот структура проекта:
pytorch-multinode-ddp/ ├── train.py # Entry point — training loop ├── config.py # Dataclass configuration + argparse ├── ddp_utils.py # Distributed setup, teardown, checkpointing ├── model.py # MiniResNet (lightweight ResNet variant) ├── dataset.py # Synthetic dataset + DistributedSampler loader ├── utils/ │ ├── logger.py # Rank-aware structured logging │ └── metrics.py # Running averages + distributed all-reduce ├── scripts/ │ └── launch.sh # Multi-node torchrun wrapper └── requirements.txtТакое разделение означает, что вы можете заменить модель реальным набором данных, отредактировав только файл dataset.py, или заменить модель, отредактировав только файл model.py. Цикл обучения при этом не нужно менять.
4. Централизованная конфигурация
Жестко заданные гиперпараметры — враг воспроизводимости. В качестве единственного источника конфигурации мы используем класс данных Python. Каждый второй модуль импортирует TrainingConfig и считывает из него данные — ничего не задано жестко.
Класс данных также выполняет функцию парсера командной строки: метод класса from_args() анализирует имена и типы полей, автоматически формируя флаги argparse со значениями по умолчанию. Это означает, что вы получаете –batch_size 128 и –no-use_amp бесплатно, без необходимости писать вручную ни одной строки парсера.
@dataclass class TrainingConfig: """Immutable bag of every parameter the training pipeline needs.""" # Model num_classes: int = 10 in_channels: int = 3 image_size: int = 32 # Data batch_size: int = 64 # per-GPU num_workers: int = 4 # Optimizer / Scheduler epochs: int = 10 lr: float = 0.01 momentum: float = 0.9 weight_decay: float = 1e-4 # Distributed backend: str = "nccl" # Mixed Precision use_amp: bool = True # Gradient Accumulation grad_accum_steps: int = 1 # Checkpointing checkpoint_dir: str = "./checkpoints" save_every: int = 1 resume_from: Optional[str] = None # Logging & Profiling log_interval: int = 10 enable_profiling: bool = False seed: int = 42 @classmethod def from_args(cls) -> "TrainingConfig": parser = argparse.ArgumentParser( formatter_class=argparse.ArgumentDefaultsHelpFormatter) defaults = cls() for name, val in vars(defaults).items(): arg_type = type(val) if val is not None else str if isinstance(val, bool): parser.add_argument(f"--{name}", default=val, action=argparse.BooleanOptionalAction) else: parser.add_argument(f"--{name}", type=arg_type, default=val) return cls(**vars(parser.parse_args()))Почему именно dataclass, а не YAML или JSON? Три причины: (1) подсказки типов обеспечиваются IDE и mypy, (2) отсутствует зависимость от сторонних библиотек конфигурации, и (3) каждый параметр имеет видимое значение по умолчанию прямо рядом с его объявлением. Для производственных систем, требующих иерархической конфигурации, всегда можно использовать Hydra или OmegaConf поверх этого шаблона.
5. Распределенное управление жизненным циклом
Жизненный цикл распределенной системы состоит из трех фаз: инициализация, выполнение и завершение. Любая из этих ошибок может привести к скрытым зависаниям, поэтому мы используем явную обработку ошибок для всего процесса.
Инициализация группы процессов
Функция setup_distributed() считывает три переменные среды, которые torchrun устанавливает автоматически (RANK, LOCAL_RANK, WORLD_SIZE), закрепляет нужный графический процессор с помощью torch.cuda.set_device() и инициализирует группу процессов NCCL. Она возвращает замороженный класс данных — DistributedContext — который остальная часть кода передает дальше, вместо повторного чтения os.environ.
@dataclass(frozen=True) class DistributedContext: """Immutable snapshot of the current process's distributed identity.""" rank: int local_rank: int world_size: int device: torch.device def setup_distributed(config: TrainingConfig) -> DistributedContext: required_vars = ("RANK", "LOCAL_RANK", "WORLD_SIZE") missing = [v for v in required_vars if v not in os.environ] if missing: raise RuntimeError( f"Missing environment variables: {missing}. " "Launch with torchrun or set them manually.") if not torch.cuda.is_available(): raise RuntimeError("CUDA is required for NCCL distributed training.") rank = int(os.environ["RANK"]) local_rank = int(os.environ["LOCAL_RANK"]) world_size = int(os.environ["WORLD_SIZE"]) torch.cuda.set_device(local_rank) device = torch.device("cuda", local_rank) dist.init_process_group(backend=config.backend) return DistributedContext( rank=rank, local_rank=local_rank, world_size=world_size, device=device)Контрольные точки с использованием охранников ранга
Наиболее распространённая ошибка распределённого контрольного сохранения — это одновременная запись всех рангов в один и тот же файл. Мы обеспечиваем сохранение через функцию is_main_process(), а загрузку — через dist.barrier() — это гарантирует, что ранг 0 завершит запись до того, как другие ранги попытаются её прочитать.
def save_checkpoint(path, epoch, model, optimizer, scaler=None, rank=0): """Persist training state to disk (rank-0 only).""" if not is_main_process(rank): return Path(path).parent.mkdir(parents=True, exist_ok=True) state = { "epoch": epoch, "model_state_dict": model.module.state_dict(), "optimizer_state_dict": optimizer.state_dict(), } if scaler is not None: state["scaler_state_dict"] = scaler.state_dict() torch.save(state, path) def load_checkpoint(path, model, optimizer=None, scaler=None, device="cpu"): """Restore training state. All ranks load after barrier.""" dist.barrier() # wait for rank 0 to finish writing ckpt = torch.load(path, map_location=device, weights_only=False) model.load_state_dict(ckpt["model_state_dict"]) if optimizer and "optimizer_state_dict" in ckpt: optimizer.load_state_dict(ckpt["optimizer_state_dict"]) if scaler and "scaler_state_dict" in ckpt: scaler.load_state_dict(ckpt["scaler_state_dict"]) return ckpt.get("epoch", 0)6. Разработка модели для DDP
Мы используем облегченный вариант ResNet под названием MiniResNet — три остаточных этапа с возрастающим количеством каналов (64, 128, 256), два блока на этап, глобальное усредняющее пулинг и полностью связанный головной узел. Он достаточно сложен, чтобы быть реалистичным, но достаточно легок, чтобы работать на любом оборудовании.
Критически важное требование DDP: модель должна быть перемещена на соответствующий графический процессор перед обертыванием. DDP не перемещает модели автоматически.
def create_model(config: TrainingConfig, device: torch.device) -> nn.Module: """Instantiate a MiniResNet and move it to device.""" model = MiniResNet( in_channels=config.in_channels, num_classes=config.num_classes, ) return model.to(device) def wrap_ddp(model: nn.Module, local_rank: int) -> DDP: """Wrap model with DistributedDataParallel.""" return DDP(model, device_ids=[local_rank])Обратите внимание на двухэтапную схему: create_model() → wrap_ddp(). Это разделение сделано намеренно. При загрузке контрольной точки вам нужна развернутая модель (model.module) для загрузки словарей состояний, а затем для повторной упаковки. Если вы объедините создание и упаковку, загрузка контрольной точки станет неудобной.
7. Распределенная загрузка данных
DistributedSampler гарантирует, что каждый графический процессор получит уникальный срез данных. Он распределяет индексы по рангам world_size и возвращает непересекающееся подмножество для каждого из них. Без него каждый графический процессор обучался бы на идентичных пакетах данных, расходуя вычислительные ресурсы без какой-либо пользы.
Есть три момента, которые сбивают людей с толку:
Во-первых, в начале каждой эпохи необходимо вызывать метод `sampler.set_epoch(epoch)`. Сэмплер использует номер эпохи в качестве начального значения для случайной генерации. Если этого не сделать, каждая эпоха будет перебирать данные в одном и том же порядке, что ухудшит обобщающую способность.
Во-вторых, параметр pin_memory=True в DataLoader предварительно выделяет заблокированную на уровне страниц память хоста, что позволяет осуществлять асинхронную передачу данных между ЦП и ГП при вызове tensor.to(device, non_blocking=True). Именно это перекрытие и обеспечивает реальный прирост пропускной способности.
Во-третьих, параметр persistent_workers=True позволяет избежать перезапуска рабочих процессов каждую эпоху — это значительно снижает накладные расходы, когда num_workers > 0.
def create_distributed_dataloader(dataset, config, ctx): sampler = DistributedSampler( dataset, num_replicas=ctx.world_size, rank=ctx.rank, shuffle=True, ) loader = DataLoader( dataset, batch_size=config.batch_size, sampler=sampler, num_workers=config.num_workers, pin_memory=True, drop_last=True, persistent_workers=config.num_workers > 0, ) return loader, sampler8. Цикл обучения — где все сходится воедино
Это сердце конвейера. Приведенный ниже цикл объединяет все компоненты, которые мы создали к настоящему моменту: модель, обернутую DDP, распределенный загрузчик данных, смешанную точность, накопление градиента, логирование с учетом ранга, планирование скорости обучения и контрольные точки.

Смешанная точность (AMP)
Автоматическая смешанная точность (AMP) сохраняет основные веса в формате FP32, но выполняет прямой проход и вычисление функции потерь в формате FP16. Это вдвое снижает требования к пропускной способности памяти и позволяет использовать ускорение Tensor Core на современных графических процессорах NVIDIA, часто обеспечивая увеличение пропускной способности в 1,5–2 раза при незначительном влиянии на точность.
Для прямого прохода мы используем torch.autocast, а для масштабирования функции потерь — torch.amp.GradScaler. Небольшая деталь: мы создаём GradScaler с параметром enabled=config.use_amp. Если он отключен, масштабировщик становится неактивным — тот же путь выполнения кода, нулевые накладные расходы, отсутствие ветвлений.
Накопление градиента
Иногда требуется больший эффективный размер пакета данных, чем позволяет память вашего графического процессора. Накопление градиента имитирует это, выполняя несколько проходов вперед-назад перед пошаговым запуском оптимизатора. Ключевым моментом является деление функции потерь на grad_accum_steps перед вызовом backward(), чтобы накопленный градиент был правильно усреднен.
def train_one_epoch(model, loader, criterion, optimizer, scaler, ctx, config, epoch, logger): model.train() tracker = MetricTracker() total_steps = len(loader) use_amp = config.use_amp and ctx.device.type == "cuda" autocast_ctx = torch.autocast("cuda", dtype=torch.float16) if use_amp else nullcontext() optimizer.zero_grad(set_to_none=True) for step, (images, labels) in enumerate(loader): images = images.to(ctx.device, non_blocking=True) labels = labels.to(ctx.device, non_blocking=True) with autocast_ctx: outputs = model(images) loss = criterion(outputs, labels) loss = loss / config.grad_accum_steps # scale for accumulation scaler.scale(loss).backward() if (step + 1) % config.grad_accum_steps == 0: scaler.step(optimizer) scaler.update() optimizer.zero_grad(set_to_none=True) # memory-efficient reset # Track raw (unscaled) loss for logging raw_loss = loss.item() * config.grad_accum_steps acc = compute_accuracy(outputs, labels) tracker.update("loss", raw_loss, n=images.size(0)) tracker.update("accuracy", acc, n=images.size(0)) if is_main_process(ctx.rank) and (step + 1) % config.log_interval == 0: log_training_step(logger, epoch, step + 1, total_steps, raw_loss, optimizer.param_groups[0]["lr"]) return trackerСтоит отметить две детали. Во-первых, параметр zero_grad(set_to_none=True) освобождает память для тензоров градиента вместо заполнения их нулями, экономя память пропорционально размеру модели. Во-вторых, данные перемещаются на графический процессор при использовании параметра non_blocking=True — это позволяет центральному процессору продолжать заполнение следующего пакета данных, пока текущий передается, используя эффект перекрытия pin_memory.
Основная функция
Функция main() управляет всем конвейером обработки данных. Обратите внимание на шаблон try/finally, гарантирующий завершение группы процессов даже в случае возникновения исключения — без этого сбой на одном уровне может привести к зависанию других уровней на неопределенное время.
def main(): config = TrainingConfig.from_args() ctx = setup_distributed(config) logger = setup_logger(ctx.rank) torch.manual_seed(config.seed + ctx.rank) model = create_model(config, ctx.device) model = wrap_ddp(model, ctx.local_rank) optimizer = torch.optim.SGD(model.parameters(), lr=config.lr, momentum=config.momentum, weight_decay=config.weight_decay) scheduler = CosineAnnealingLR(optimizer, T_max=config.epochs) scaler = torch.amp.GradScaler(enabled=config.use_amp) start_epoch = 1 if config.resume_from: start_epoch = load_checkpoint(config.resume_from, model.module, optimizer, scaler, ctx.device) + 1 dataset = SyntheticImageDataset(size=50000, image_size=config.image_size, num_classes=config.num_classes) loader, sampler = create_distributed_dataloader(dataset, config, ctx) criterion = nn.CrossEntropyLoss() try: for epoch in range(start_epoch, config.epochs + 1): sampler.set_epoch(epoch) tracker = train_one_epoch(model, loader, criterion, optimizer, scaler, ctx, config, epoch, logger) scheduler.step() avg_loss = all_reduce_scalar(tracker.average("loss"), ctx.world_size, ctx.device) if is_main_process(ctx.rank): log_epoch_summary(logger, epoch, {"loss": avg_loss}) if epoch % config.save_every == 0: save_checkpoint(f"checkpoints/epoch_{epoch}.pt", epoch, model, optimizer, scaler, ctx.rank) finally: cleanup_distributed()9. Запуск на нескольких узлах
Функция torchrun из PyTorch (представленная в версии 1.10 в качестве замены torch.distributed.launch) отвечает за запуск одного процесса на каждый графический процессор и установку переменных окружения RANK, LOCAL_RANK и WORLD_SIZE. Для многоузлового обучения каждый узел должен указывать адрес главного узла, чтобы все процессы могли установить соединение NCCL.
Вот наш скрипт запуска, который считывает все настраиваемые параметры из переменных окружения:
#!/usr/bin/env bash set -euo pipefail NNODES="${NNODES:-2}" NPROC_PER_NODE="${NPROC_PER_NODE:-4}" NODE_RANK="${NODE_RANK:-0}" MASTER_ADDR="${MASTER_ADDR:-127.0.0.1}" MASTER_PORT="${MASTER_PORT:-12355}" torchrun --nnodes="${NNODES}" --nproc_per_node="${NPROC_PER_NODE}" --node_rank="${NODE_RANK}" --master_addr="${MASTER_ADDR}" --master_port="${MASTER_PORT}" train.py "$@"Для быстрого тестирования на одном узле с использованием одной видеокарты:
torchrun --standalone --nproc_per_node=1 train.py --epochs 2Для обучения на двух узлах с четырьмя графическими процессорами на каждом, запустите программу на узле 0:
MASTER_ADDR=10.0.0.1 NODE_RANK=0 NNODES=2 NPROC_PER_NODE=4 bash scripts/launch.shА на узле 1:
MASTER_ADDR=10.0.0.1 NODE_RANK=1 NNODES=2 NPROC_PER_NODE=4 bash scripts/launch.sh 
10. Ошибки и советы по повышению эффективности работы
После создания сотен распределенных задач обучения я чаще всего встречаю следующие ошибки:
Забыли про sampler.set_epoch(). Без него порядок данных остается одинаковым в каждой эпохе. Это самая распространенная ошибка DDP, и она незаметно препятствует сходимости.
Узкое место в передаче данных между ЦП и ГП. Всегда используйте pin_memory=True в вашем DataLoader и non_blocking=True в вызовах .to(). Без них ЦП блокируется при каждой пакетной передаче.
Ведение логов со всех рангов. Если логирование выполняется со всех рангов, вывод будет представлять собой перемешанный мусор. Вся логировка должна осуществляться с проверкой rank == 0.
Функция zero_grad() без параметра set_to_none=True. Функция zero_grad() по умолчанию заполняет тензоры градиента нулями. Параметр set_to_none=True вместо этого освобождает их, уменьшая пиковый объем памяти.
Сохранение контрольных точек со всех рангов. Запись одного и того же файла несколькими рангами приводит к повреждению. Сохраняться должен только ранг 0, а перед загрузкой необходимо создать барьер для всех рангов.
Без использования смещения ранга при инициализации. torch.manual_seed(seed + rank) гарантирует, что аугментация данных для каждого ранга будет разной. Без смещения аугментация будет одинаковой на всех графических процессорах.
Когда НЕ следует использовать DDP
DDP реплицирует всю модель на каждом графическом процессоре. Если ваша модель не помещается в память одного графического процессора, одного DDP будет недостаточно. В таких случаях обратите внимание на Fully Sharded Data Parallel (FSDP), который распределяет параметры, градиенты и состояния оптимизатора по рангам, или на такие фреймворки, как DeepSpeed ZeRO.
11. Заключение
Мы перешли от подхода, ориентированного на обучение с использованием одной видеокарты, к полностью распределенному конвейеру производственного уровня, способному масштабироваться на разных машинах — без ущерба для ясности и удобства сопровождения.
Но что еще важнее, речь шла не просто о том, чтобы заставить DDP работать. Речь шла о том, чтобы правильно его построить.
Давайте выделим самые важные выводы:
Основные выводы
- DDP — это детерминированное проектирование, а не магия.
Как только вы разберетесь с группами процессов, рангами и алгоритмом AllReduce, распределенное обучение станет предсказуемым и поддающимся отладке. - Структура важнее масштаба.
Чистый, модульный код (конфигурация → данные → модель → обучение → утилиты) — вот что делает возможным масштабирование от 1 до 100 графических процессоров. - Правильное сегментирование данных не подлежит обсуждению.
DistributedSampler + set_epoch() — это разница между истинным масштабированием и неэффективными вычислительными затратами. - Успех зависит от мельчайших деталей.
Параметры pin_memory, non_blocking, set_to_none=True и AMP в совокупности обеспечивают значительный прирост пропускной способности. - Знание рангов имеет важное значение.
Ведение журналов, создание контрольных точек и случайность должны учитывать ранг — иначе возникнет хаос. - DDP масштабирует вычисления, а не память.
Если ваша модель не помещается на одном графическом процессоре, вам потребуется FSDP или ZeRO, а не дополнительные графические процессоры.
Более широкая картина
То, что вы здесь создали, — это не просто скрипт для обучения, это шаблон для реальных систем машинного обучения .
Этот же шаблон используется в:
- Производственные конвейеры машинного обучения
- Исследовательские лаборатории обучают большие модели
- Стартапы, масштабирующиеся от прототипа до инфраструктуры.
А самое приятное?
Теперь вы можете:
- Введите реальный набор данных.
- Замените трансформатор или используйте собственную архитектуру.
- Масштабирование на несколько узлов без каких-либо изменений в коде.
Что исследовать дальше
Как только вы освоите эту конфигурацию, следующим шагом станет эффективное использование памяти и масштабное обучение :
- Полностью сегментированная параллельная обработка данных (FSDP) → сегментированная модель + градиенты
- DeepSpeed ZeRO → состояния оптимизатора сегментов
- Параллелизм конвейера → разделение моделей между графическими процессорами
- Тензорный параллелизм → разделение слоев.
Эти методы лежат в основе самых крупных современных моделей, но все они построены на той самой базе DDP, которую вы теперь понимаете.
Дистанционное обучение часто вызывает опасения — не потому, что оно по своей природе сложное, а потому, что его редко представляют как целостную систему.
Теперь вы видите всю картину целиком.
А когда вы увидите это от начала до конца…
Масштабирование становится инженерным решением, а не исследовательской проблемой.
Что дальше?
Этот конвейер обрабатывает параллельное обучение данных — наиболее распространенный распределенный подход. Когда ваши модели превышают объем памяти одной видеокарты, рассмотрите возможность использования Fully Sharded Data Parallel (FSDP) для разделения параметров или DeepSpeed ZeRO для разделения состояния оптимизатора. Для действительно масштабных моделей необходимы конвейерный параллелизм (послойное разделение модели между видеокартами) и тензорный параллелизм (разделение отдельных слоев).
Однако для подавляющего большинства задач обучения — от ResNet до трансформеров среднего масштаба — созданный нами конвейер DDP в точности соответствует тому, что используют производственные команды. Масштабируйте его, добавляя узлы и графические процессоры; код позаботится обо всем остальном.
Полный, готовый к использованию код этого проекта доступен здесь: pytorch-multinode-ddp
Ссылки
[1] Обзор распределенных вычислений PyTorch, Документация PyTorch (2024), https://pytorch.org/tutorials/beginner/dist_overview.html
[2] С. Ли и др., PyTorch Distributed: Опыт ускорения параллельного обучения данных (2020), VLDB Endowment
[3] API DistributedDataParallel в PyTorch, https://pytorch.org/docs/stable/generated/torch.nn.parallel.DistributedDataParallel.html
[4] NCCL: Оптимизированные примитивы для коллективной связи между несколькими графическими процессорами, NVIDIA, https://developer.nvidia.com/nccl
[5] PyTorch AMP: Автоматическая смешанная точность, https://pytorch.org/docs/stable/amp.html
SM Navin Nayer Anik Посмотреть все товары SM Navin Nayer Anik
Источник: towardsdatascience.com























