Ускорение машинного обучения с помощью графических процессоров в PyTorch
Введение
PyTorch поддерживает вычисления на графических процессорах NVIDIA через библиотеку CUDA. Это позволяет ускорять обучение и инференс моделей в десятки раз по сравнению с центральным процессором. В данном разделе рассматриваются основные приемы работы с CUDA, различные варианты настройки, типичные проблемы и пути их решения.
Основной подход к использованию CUDA
Для задействования GPU необходимо проверить доступность устройства, переместить тензоры и модель на нужное устройство, после чего выполнять тренировку обычным образом. Пример базового цикла обучения на GPU:
import torch
import torch.nn as nn
# Проверка доступности CUDA
if torch.cuda.is_available():
device = torch.device('cuda')
print('CUDA доступен')
else:
device = torch.device('cpu')
print('CUDA не найден, используется CPU')
# Простая модель
model = nn.Linear(10, 2).to(device)
# Данные на GPU
x = torch.randn(32, 10, device=device)
y = torch.randn(32, 2, device=device)
# Оптимизатор
optimizer = torch.optim.SGD(model.parameters(), lr=0.01)
for epoch in range(10):
pred = model(x)
loss = nn.functional.mse_loss(pred, y)
optimizer.zero_grad()
loss.backward()
optimizer.step()
print(f'Epoch {epoch}: loss = {loss.item():.4f}')
Python torch cuda (использование cuda с pytorch)
Все операции с тензорами и моделью происходят на GPU, что ускоряет вычисления. Главное правило: все входные данные и параметры модели должны находиться на одном устройстве.
Как проверить доступность CUDA и выбрать устройство?
Для проверки используется функция torch.cuda.is_available(). Она возвращает True, если система содержит совместимую видеокарту с установленными драйверами и CUDA Toolkit. Дополнительно можно получить количество устройств и их имена:
import torch
print('CUDA доступен:', torch.cuda.is_available())
if torch.cuda.is_available():
print('Количество GPU:', torch.cuda.device_count())
print('Имя устройства:', torch.cuda.get_device_name(0))
Torch python dll (ошибка dll при использовании pytorch)
Типичная ошибка:
При запуске кода на системе без NVIDIA GPU или без драйверов возникает ошибка AssertionError: Torch not compiled with CUDA enabled. Решение: переустановить PyTorch с поддержкой CUDA через официальный сайт (например, команда pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu121).
Как переместить тензор на GPU?
Существует несколько способов: метод .to(device), .cuda(), а также явное указание устройства при создании тензора. Рекомендуется использовать единый подход через объект device, чтобы код легко переключался между CPU и GPU:
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
# Создание на устройстве
t1 = torch.tensor([1, 2, 3], device=device)
# Перемещение после создания
t2 = torch.tensor([4, 5, 6]).to(device)
# Альтернатива
t3 = torch.tensor([7, 8, 9]).cuda() # только если CUDA доступен
Как переместить модель на GPU?
Метод model.to(device) копирует все параметры и буферы модели на указанное устройство. После этого вызов модели автоматически работает на GPU. Важно, чтобы входные данные тоже находились на том же устройстве:
model = nn.Sequential(nn.Linear(10, 5), nn.ReLU(), nn.Linear(5, 2))
model.to(device)
input_data = torch.randn(16, 10, device=device)
output = model(input_data) # выполняется на GPU
Частая ошибка:
Если модель перемещена на GPU, а данные остались на CPU, PyTorch выдаст RuntimeError: Expected all tensors to be on the same device. Решение: следить, что данные перед передачей в модель тоже переведены на устройство.
Как обрабатывать сценарий, когда CUDA отсутствует?
Код должен быть устойчивым к отсутствию GPU. Рекомендуется всегда определять устройство динамически и не использовать жестко прописанное 'cuda':
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
Такой подход позволяет запускать скрипт как на сервере с несколькими GPU, так и на ноутбуке без дискретной графики.
Как задействовать несколько графических процессоров?
Для распределения вычислений на несколько GPU применяется torch.nn.DataParallel. Он автоматически делит батч данных и передает части на разные видеокарты. Пример:
if torch.cuda.device_count() > 1:
model = nn.DataParallel(model)
model.to(device)
# Дальнейший код не меняется
for data, target in dataloader:
data, target = data.to(device), target.to(device)
output = model(data)
Более продвинутый и эффективный вариант DistributedDataParallel рекомендуется для продакшена, но его настройка сложнее и требует запуска с помощью torchrun.
Как использовать полуточность (float16) для ускорения?
Автоматическая смешанная точность (AMP) позволяет экономить память и ускорять вычисления на GPU с поддержкой Tensor Cores. В PyTorch это реализовано через torch.cuda.amp:
scaler = torch.cuda.amp.GradScaler()
for batch in dataloader:
inputs, labels = batch[0].to(device), batch[1].to(device)
optimizer.zero_grad()
with torch.cuda.amp.autocast():
outputs = model(inputs)
loss = loss_fn(outputs, labels)
scaler.scale(loss).backward()
scaler.step(optimizer)
scaler.update()
Использование AMP особенно эффективно для больших моделей (например, трансформеров).
Как освободить занятую GPU память?
Python автоматически освобождает память, когда объекты выходят из области видимости, но иногда необходимо принудительно очистить кэш для освобождения неиспользуемых блоков:
torch.cuda.empty_cache()
Этот вызов не высвобождает память, занятую живыми тензорами, но может помочь при фрагментации. Также полезно отслеживать состояние памяти:
print(torch.cuda.memory_summary()) # подробный отчет
print('Занято:', torch.cuda.memory_allocated() / 1024**2, 'MB')
print('Зарезервировано:', torch.cuda.memory_reserved() / 1024**2, 'MB')
Как избежать ошибки CUDA out of memory?
Ошибка возникает, когда размер батча или модели превышает объем видеопамяти. Решения:
- Уменьшить размер батча.
- Использовать градиентный аккумулятор (несколько шагов без обновления весов).
- Включить AMP (смешанную точность).
- Проверить, нет ли утечек памяти (например, сохранять ссылки на тензоры в циклах).
- Использовать torch.cuda.empty_cache() между эпохами.
Критическая ошибка:
При работе с большими данными можно столкнуться с CUDA error: device-side assert triggered. Эта ошибка часто связана с некорректными индексами в слоях (например, nn.Embedding с выходами за границы словаря). Решение: проверить входные данные, установить torch.backends.cuda.deterministic = True для воспроизводимости.
Расширенные примеры использования CUDA в PyTorch
Пример 1: Сравнение скорости CPU и GPU на умножении матриц
Демонстрирует значительное преимущество GPU при работе с большими матрицами.
import torch
import time
size = 3000
A_cpu = torch.randn(size, size)
B_cpu = torch.randn(size, size)
# Замер на CPU
start = time.time()
C_cpu = torch.matmul(A_cpu, B_cpu)
time_cpu = time.time() - start
print(f'CPU: {time_cpu:.3f} сек')
if torch.cuda.is_available():
A_gpu = A_cpu.cuda()
B_gpu = B_cpu.cuda()
torch.cuda.synchronize() # дождаться завершения
start = time.time()
C_gpu = torch.matmul(A_gpu, B_gpu)
torch.cuda.synchronize()
time_gpu = time.time() - start
print(f'GPU: {time_gpu:.3f} сек')
print(f'Ускорение: {time_cpu/time_gpu:.1f}x')
CPU: 2.145 сек GPU: 0.025 сек Ускорение: 85.8x
Пример 2: Обучение сверточной сети на GPU с замером времени
Используется простой классификатор MNIST.
import torch
import torch.nn as nn
import torchvision.datasets as dsets
import torchvision.transforms as transforms
from torch.utils.data import DataLoader
import time
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
transform = transforms.ToTensor()
train_dataset = dsets.MNIST(root='./data', train=True, transform=transform, download=True)
train_loader = DataLoader(train_dataset, batch_size=64, shuffle=True)
model = nn.Sequential(
nn.Conv2d(1, 16, kernel_size=3, stride=1, padding=1),
nn.ReLU(),
nn.MaxPool2d(2),
nn.Flatten(),
nn.Linear(16*14*14, 10)
).to(device)
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
start = time.time()
for epoch in range(3):
for images, labels in train_loader:
images, labels = images.to(device), labels.to(device)
outputs = model(images)
loss = criterion(outputs, labels)
optimizer.zero_grad()
loss.backward()
optimizer.step()
print(f'Epoch {epoch+1} завершена')
total = time.time() - start
print(f'Общее время обучения: {total:.2f} сек')
Epoch 1 завершена Epoch 2 завершена Epoch 3 завершена Общее время обучения: 14.23 сек
Пример 3: Использование автоматической смешанной точности (AMP) с обучением
Показывает, как применить AMP для уменьшения расхода памяти и ускорения.
scaler = torch.cuda.amp.GradScaler()
for epoch in range(5):
for images, labels in train_loader:
images, labels = images.to(device), labels.to(device)
optimizer.zero_grad()
with torch.cuda.amp.autocast():
outputs = model(images)
loss = criterion(outputs, labels)
scaler.scale(loss).backward()
scaler.step(optimizer)
scaler.update()
(Вывод аналогичен примеру 2, но время может быть на 20-30% меньше)
Пример 4: Распределение вычислений с DataParallel на 2 GPU
Модель копируется на все доступные GPU, батч делится поровну.
if torch.cuda.device_count() > 1:
print(f'Используется {torch.cuda.device_count()} GPU')
model = nn.DataParallel(model)
model.to(device)
# Цикл обучения без изменений
for images, labels in train_loader:
images, labels = images.to(device), labels.to(device)
outputs = model(images)
loss = criterion(outputs, labels)
optimizer.zero_grad()
loss.backward()
optimizer.step()
Используется 2 GPU (обучение происходит быстрее, но нелинейно из-за накладных расходов)
Пример 5: Анализ использования памяти GPU с помощью memory_summary
Полезно для отладки утечек и оптимизации размера батча.
import torch
# Создадим большой тензор на GPU
t = torch.randn(1000, 1000, device='cuda')
print('После создания тензора:')
torch.cuda.memory_summary(device='cuda', abbreviated=True)
del t # удалим ссылку
torch.cuda.empty_cache()
print('После очистки кэша:')
torch.cuda.memory_summary(device='cuda', abbreviated=True)
После создания тензора: | alloc. requests | 1 | | current memory | 4.00 MB | | peak memory | 4.00 MB | | ... После очистки кэша: | current memory | 0.00 MB |
Пример 6: Использование CUDA streams для параллельного выполнения операций
Streams позволяют перекрыть передачу данных и вычисления.
s1 = torch.cuda.Stream()
s2 = torch.cuda.Stream()
with torch.cuda.stream(s1):
# Вычислительная операция A
a = torch.randn(2000, 2000, device='cuda')
b = torch.matmul(a, a)
with torch.cuda.stream(s2):
# Операция B может выполняться одновременно, если ресурсы позволяют
c = torch.randn(2000, 2000, device='cuda')
d = c.t()
# Синхронизация
torch.cuda.synchronize()
print('Все потоки завершены')
Все потоки завершены
Пример 7: Использование pinned memory для ускорения загрузки данных
При передаче на GPU из CPU-памяти можно использовать закрепленные страницы (pinned memory), чтобы ускорить копирование.
from torch.utils.data import DataLoader, TensorDataset
# Используем pin_memory=True в DataLoader
dataloader = DataLoader(dataset, batch_size=64, pin_memory=True)
for batch in dataloader:
# Данные уже могут быть в pinned memory, .to(device) работает быстрее
batch = [t.to(device, non_blocking=True) for t in batch]
(Улучшение производительности на 5-15% в зависимости от размера данных)