Ваш код на 8 GPU работает медленнее, чем на одной карте. Знакомо?
Вы купили вторую RTX 4090, подключили её, запустили обучение модели — и ничего. Скорость не удвоилась. Добавили третью — стало ещё хуже. Четвёртая GPU вообще простаивает. Знакомая ситуация? 90% разработчиков AI сталкиваются с этой проблемой, потому что не понимают фундаментальное различие между хостом (CPU) и девайсом (GPU).
Сегодня разберём архитектуру Host-Device на костях. Без воды, без академических определений. Только то, что влияет на скорость вашего кода прямо сейчас.
Важно: Эта статья актуальна на 15 февраля 2026 года. Все примеры используют PyTorch 3.1+, CUDA 13.5+ и учитывают изменения в архитектуре NVIDIA Hopper (H200) и Blackwell. Если вы работаете с устаревшими версиями — обновитесь или готовьтесь к несовместимости.
CPU vs GPU: не братья, а начальник и рабочий
Представьте строительную площадку. CPU — это прораб. Умный, может делать всё: читать чертежи, звонить поставщикам, считать смету. Но у него только две руки. GPU — это бригада из 10,000 рабочих. Каждый тупой как пробка (умеет только складывать и умножать), но работает синхронно.
Когда вы пишете tensor.to('cuda'), вы говорите прорабу: "Отнеси эти кирпичи рабочим". Прораб идёт через мост (PCIe), несёт кирпичи, возвращается. Пока он ходит — стройка стоит.
| Операция | Где выполняется | Скорость (примерная) | Что происходит в этот момент |
|---|---|---|---|
| Загрузка данных | CPU (RAM → CPU cache) | ~50 GB/s | GPU простаивает |
| Копирование CPU→GPU | PCIe bus | ~16 GB/s (PCIe 4.0) | Обе стороны заняты передачей |
| Матричное умножение | GPU cores | ~3000 GB/s (RTX 4090) | CPU свободен |
Видите проблему? GPU в 200 раз быстрее CPU в вычислениях, но данные к нему ползут как черепаха. Добавьте сюда задержки ядра Linux, аллокацию памяти, синхронизацию — и получите ситуацию, когда GPU занят только 30% времени.
Типичная ошибка новичка: синхронный ад
Вот как НЕ надо делать (видел этот код в десятках проектов):
# АНТИПАТТЕРН - НЕ КОПИРУЙТЕ ЭТОТ КОД
import torch
# 1. Загружаем данные на CPU
data = load_huge_dataset() # 10GB в RAM
# 2. Копируем ВСЁ на GPU
for batch in data:
gpu_data = batch.to('cuda') # Прораб таскает кирпичи 10 секунд
result = model(gpu_data) # Рабочие работают 0.1 секунду
cpu_result = result.cpu() # Прораб таскает обратно 10 секунд
save(cpu_result)
Что здесь происходит? Прораб 99% времени таскает кирпичи, рабочие стоят без дела. GPU utilization покажет смешные 5%.
nvidia-smi -l 1. Если видите скачки от 0% до 100% и обратно — у вас проблема с pipeline. GPU работает рывками, потому что ждёт данных от CPU.Правильный паттерн: конвейер вместо очереди
Решение — overlap compute and data transfer. Пока GPU обрабатывает batch N, CPU готовит batch N+1. В PyTorch это делается через DataLoader с pin_memory и non_blocking:
import torch
from torch.utils.data import DataLoader, Dataset
class FastDataset(Dataset):
def __init__(self):
self.data = torch.randn(10000, 512, 512)
def __len__(self):
return len(self.data)
def __getitem__(self, idx):
return self.data[idx]
# Ключевые параметры:
dataloader = DataLoader(
dataset=FastDataset(),
batch_size=32,
num_workers=4, # 4 процесса загружают данные параллельно
pin_memory=True, # Фиксируем память для быстрого DMA
prefetch_factor=2 # Готовим 2 батча заранее
)
model = torch.nn.Linear(512, 256).cuda()
for batch in dataloader:
# non_blocking=True позволяет CPU не ждать завершения копирования
batch_gpu = batch.cuda(non_blocking=True)
# Пока GPU вычисляет, CPU может готовить следующий batch
output = model(batch_gpu)
# Если результат не нужен на CPU сразу - оставляем на GPU
loss = output.sum()
loss.backward()
# Только в конце синхронизируем
print("Готово, все вычисления завершены")
1 Почему pin_memory работает быстрее?
Обычная RAM может перемещаться операционной системой (page swapping). Когда вы копируете данные из такой памяти в GPU, драйвер сначала должен "зафиксировать" страницы, потом скопировать. pin_memory сразу выделяет "неподвижную" память, которую GPU может читать напрямую через DMA (Direct Memory Access).
2 Сколько workers ставить?
Золотое правило: num_workers = количество CPU ядер - 1. Но есть нюансы:
- На 8-ядерном процессоре ставьте 6-7 workers
- Если загрузка данных простая (читаем из RAM) — workers могут мешать друг другу
- Если данные на диске (HDD) — добавьте больше workers, потому что ждём IO
- Слишком много workers (>16) создадут contention на GIL и замедлят всё
Несколько GPU: когда одна проблема становится восемью
Добавляем вторую GPU. Теперь у нас два отряда рабочих (GPU 0 и GPU 1) и один прораб (CPU). Прораб должен кормить обе бригады. Если он будет бегать от одной к другой — обе будут простаивать.
Популярная ошибка — копировать все данные на GPU 0, потом на GPU 1:
# ЕЩЁ ОДИН АНТИПАТТЕРН
batch = next(dataloader)
gpu0_batch = batch.to('cuda:0') # Ждём...
gpu1_batch = batch.to('cuda:1') # Снова ждём...
# GPU 1 простаивает пока данные идут на GPU 0
Правильный подход — использовать асинхронные потоки CUDA:
import torch
# Создаём отдельные потоки для каждой GPU
stream0 = torch.cuda.Stream(device='cuda:0')
stream1 = torch.cuda.Stream(device='cuda:1')
with torch.cuda.stream(stream0):
data0 = batch[:half].to('cuda:0', non_blocking=True)
output0 = model0(data0)
with torch.cuda.stream(stream1):
data1 = batch[half:].to('cuda:1', non_blocking=True)
output1 = model1(data1)
# Синхронизируем оба потока
torch.cuda.synchronize('cuda:0')
torch.cuda.synchronize('cuda:1')
Внимание на архитектуру: На 15 февраля 2026 года актуальны GPU с NVLink 4.0 (до 900 GB/s между картами) и PCIe 6.0 (256 GB/s). Если у вас старые карты без NVLink — меж-GPU коммуникация будет через PCIe, что в 30 раз медленнее. Проверьте nvidia-smi topo -m. Если видите "PHB" вместо "NV#" — готовьтесь к bottleneck.
Реальная архитектура: от 2 до 8 GPU
Рассмотрим типичную конфигурацию для AI-фермы на 8x RTX 3090:
| Компонент | Роль | Типичная проблема | Решение |
|---|---|---|---|
| CPU (Threadripper) | Дирижёр оркестра | Не хватает PCIe линий | AMD Threadripper Pro с 128 линиями |
| RAM (256GB) | Буфер обмена | Медленная DDR4 | DDR5 или HBM (если бюджет) |
| GPU 0-7 | Вычислители | Перегрев, троттлинг | Прямой обдув, жидкостное охлаждение |
| NVLink/PCIe | Дороги между GPU | Узкое место (bottleneck) | Группировка GPU по 4 с NVLink |
Самая частая ошибка при сборке такой системы — экономия на материнской плате. Вы покупаете 8 дорогих GPU, но вставляете их в плату с 48 PCIe линиями. Каждая карта получает только x4 вместо x16. Пропускная способность падает в 4 раза. Все ваши вычисления упрутся в передачу данных.
Практика: распределяем модель по 4 GPU
Допустим, у нас есть огромная модель, которая не помещается в память одной GPU. Разделим её слои между устройствами:
import torch
import torch.nn as nn
device_count = torch.cuda.device_count()
print(f"Доступно GPU: {device_count}")
class DistributedModel(nn.Module):
def __init__(self):
super().__init__()
# Создаём части модели на разных GPU
self.part1 = nn.Sequential(
nn.Linear(1024, 4096),
nn.ReLU(),
nn.Dropout(0.1)
).to('cuda:0')
self.part2 = nn.Sequential(
nn.Linear(4096, 4096),
nn.ReLU(),
nn.Dropout(0.1)
).to('cuda:1')
# И так далее для всех GPU
def forward(self, x):
# x начинается на CPU
x = x.to('cuda:0')
# Проходим через часть на GPU 0
x = self.part1(x)
# Перемещаем на GPU 1
x = x.to('cuda:1')
x = self.part2(x)
# Возвращаем на CPU для лосса
return x.cpu()
# Но это НЕПРАВИЛЬНО! Почему?
# Каждый .to() — это синхронная операция.
# GPU 1 ждёт, пока GPU 0 закончит вычисления.
# GPU 0 ждёт, пока данные переедут на GPU 1.
# Все стоят в очереди.
Правильный подход — pipeline parallelism. Пока GPU 0 обрабатывает batch N, GPU 1 обрабатывает batch N-1:
# Упрощённый пример pipeline
from torch.distributed.pipeline.sync import Pipe
model = DistributedModel()
# PyTorch сам разделит модель и создаст конвейер
model_pipe = Pipe(model, chunks=8) # 8 микробатчей
# Теперь GPU работают параллельно, а не последовательно
output = model_pipe(input)
Когда всё летит в тартарары: отладка распределённого кода
Вы написали "идеальный" код, но он работает медленнее, чем на одной GPU. Куда смотреть?
- nvidia-smi dmon — показывает активность PCIe и NVLink в реальном времени. Если PCIe utilization постоянно 100% — у вас bottleneck в передаче данных.
- Nsight Systems — профилировщик от NVIDIA. Покажет timeline: где GPU ждёт, где CPU ждёт, какие операции блокируют конвейер.
- PyTorch Profiler — встроенный инструмент. Запустите с
with torch.profiler.profile(...)и получите flame graph.
Типичные находки при профилировании:
- cudaMemcpyAsync занимает 80% времени — вы слишком часто копируете данные туда-сюда. Оставляйте тензоры на GPU между итерациями.
- cudaStreamSynchronize после каждой операции — вы вызываете
.cpu()или.item()слишком часто. Эти операции блокируют весь конвейер. - GPU utilization скачет 0% → 100% → 0% — batch size слишком маленький. GPU быстро обрабатывает батч и ждёт следующего.
Специальный случай: инференс больших LLM
Для инференса (генерации текста) архитектура меняется. Здесь нельзя использовать pipeline parallelism в чистом виде, потому что каждый токен зависит от предыдущего. Но можно распределить слои модели по GPU с помощью tensor parallelism:
# Используем FullyShardedDataParallel (FSDP) из PyTorch
from torch.distributed.fsdp import FullyShardedDataParallel as FSDP
from torch.distributed.fsdp import ShardingStrategy
# Модель остаётся целой логически, но тензоры шардятся по GPU
model = HugeTransformer()
fsdp_model = FSDP(
model,
sharding_strategy=ShardingStrategy.FULL_SHARD, # Шардим параметры, градиенты и оптимизатор
device_id=torch.cuda.current_device()
)
# Теперь модель распределена по всем доступным GPU
# Каждый GPU хранит только кусок параметров
output = fsdp_model(input_ids)
FSDP особенно эффективен для моделей типа Llama 3 405B (да, на 15 февраля 2026 года это уже не самая большая модель). Если у вас арендованные H200 с 141GB памяти каждая — можно уместить довольно крупные модели.
Гибридные системы: когда у вас разные GPU
Реальность такова, что редко у кого есть 8 одинаковых GPU. Чаще встречается зоопарк: RTX 4090 + A100 + старый V100. Или, что ещё хуже, смесь NVIDIA и AMD.
Правило простое: самая медленная GPU определяет скорость всей системы. Если у вас одна карта с PCIe 3.0 x4, а остальные с PCIe 4.0 x16 — система будет работать на скорости PCIe 3.0 x4.
torch.cuda.get_device_capability(device_id) вернёт (compute capability major, minor). Карты с разной compute capability (например, 8.9 для Ada и 7.0 для Volta) могут не работать вместе в одном процессе.Мифы и правда о Host-Device архитектуре
| Миф | Правда | Что делать |
|---|---|---|
| "Больше GPU = быстрее обучение" | Только если данные успевают за GPU. Иначе добавление карт бесполезно. | Сначала оптимизируйте data pipeline, потом добавляйте GPU. |
| "NVLink решает все проблемы" | NVLink ускоряет только GPU↔GPU обмен. CPU↔GPU всё равно идёт через PCIe. | Используйте CUDA graphs для минимизации CPU участия. |
| "Можно использовать любую материнскую плату" | Дешёвые платы делят PCIe линии. 4 карты × x4 = все карты на x1 скорости. | Берите серверные платы или Threadripper с большим числом линий. |
| "Docker/K8s прозрачны для GPU" | Контейнеры добавляют overhead на системные вызовы. Может быть 5-10% потерь. | Используйте nvidia-container-toolkit и избегайте виртуализации GPU. |
Чеклист перед запуском распределённого кода
- Проверьте PCIe топологию:
nvidia-smi topo -m. Идеально, если все GPU связаны NVLink. - Убедитесь, что CUDA видит все карты:
torch.cuda.device_count()должно показывать правильное число. - Настройте окружение:
export CUDA_VISIBLE_DEVICES="0,1,2,3"если хотите использовать только часть карт. - Включите benchmark mode:
torch.backends.cudnn.benchmark = Trueдля автоматической оптимизации convolutions. - Проверьте память:
torch.cuda.empty_cache()перед началом, чтобы очистить кэш. - Настройте DataLoader:
pin_memory=True, num_workers=4, prefetch_factor=2. - Используйте mixed precision:
torch.autocast('cuda')для FP16/BF16 где возможно. - Избегайте синхронных операций: Не вызывайте
.cpu()или.item()в цикле.
Что будет дальше? Эволюция архитектур
К 2026 году тенденции ясны:
- CPU становится узким местом — поэтому NVIDIA и AMD интегрируют CPU и GPU на одном чипе (Grace-Hopper, MI300X).
- Память унифицируется — HBM на CPU и GPU позволяет обмениваться данными без копирования.
- Программный стек упрощается — PyTorch 3.x обещает автоматическое распределение без ручных
.to('cuda').
Но пока эти технологии не стали мейнстримом, вам придётся разбираться с ручным распределением. Хорошая новость: понимание Host-Device архитектуры — это 80% успеха в распределённом ML. Остальные 20% — это знание конкретных фреймворков (DDP, FSDP, DeepSpeed), о которых мы поговорим в следующих статьях.
Самый важный урок: GPU — не волшебные чёрные ящики. Они требуют правильного питания данными. Если вы кормите их с ложечки (синхронно, маленькими порциями), они будут голодать. Настройте конвейер — и они заработают на полную.
Проверьте себя: Запустите nvprof --print-gpu-trace python your_script.py. Если видите большие gaps между kernel launches — у вас bottleneck в Host-Device коммуникации. Пора оптимизировать.