Оптимальный Power Limit для deep learning задач на RTX 3090
Недавно я купил б/у RTX 3090 для экспериментов с обучением нейронных сетей и выяснил, что карта сильно нагревается и потребляет много энергии.
Стоковый power limit составляет 390 Вт, но может быть увеличен до 480 Вт. GPU выделяет так много тепла, что начинает нагревать CPU.
Основным методом снижения нагрева является понижение напряжения (undervolting):
меньше напряжение => меньше мощность => меньше нагрев.
К сожалению, драйверы NVIDIA под Linux не поддерживают понижение напряжения. Поэтому единственным вариантом остается понижение лимита мощности (power limit).
Power limit — это максимальное количество энергии, которое может потреблять GPU. Этот предел поддерживается автоматически путем регулировки частот и напряжений, чтобы энергопотребление оставалось ниже указанного предела. Я снизил power limit до 250 Вт, и производительность упала не так сильно, как я ожидал. Я решил исследовать, как ограничение мощности влияет на различные DL задачи:
Training:
fp32
tf32
amp fp16
fp16 (.half ())
Inference:
fp32
tf32
amp fp16
fp16 (.half ())
TensorRT fp16
Все тесты проводились на vit_base_patch16_224 из библиотеки timm.
fp32 training batch_size=160 approx. 20GB of VRAM
280 Вт => 80 процентов от пиковой производительности (при 480Вт)
330 Вт => 90 процентов
380 Вт => 95 процентов
tf32
Написав
torch.backends.cuda.matmul.allow_tf32 = True
мы можем указать pytorch использовать режим вычислений TF32. По сути, мы можем пожертвовать частью точности fp32, чтобы получить более быстрые матричные операции. Изображения ниже описывают, как это работает, и взяты из статьи в блоге NVIDIA. Операции TF32 могут быть ускорены Тензорными ядрами (Tensor Cores).
Сравнение различных форматов хранения чисел
Операция dot product (скалярное произведение) с использованием TF32
tf32 training batch_size=160 approx. 20GB of VRAM
230 Вт => 80 процентов от пиковой производительности (при 480Вт)
280 Вт => 90 процентов
320 Вт => 95 процентов
Как мы видим, тензорные ядра не только быстрее, но и гораздо более энергоэффективны.
amp (fp16)
С помощью torch.autocast
мы можем указать pytorch использовать fp16 вместо fp32 для некоторых операций. Список операций можно посмотреть здесь. И снова мы обмениваем точность на производительность. Мы видим, что некоторые операции, такие как matmul, conv2d, автокастятся в fp16, но операции, требующие большей точности, такие как sum, pow, функции потерь и нормализации, автокастятся fp32.
С использованием fp16 есть одна проблема: иногда градиенты настолько малы, что не могут быть представлены в fp16 и округляются до нуля.
нулевой градиент =>нулевое обновление весов => обучение не происходит.
Поэтому мы умножаем значения функции потерь на коэффициент масштабирования (который оценивается автоматически), чтобы после backward pass получить ненулевые значения градиента.
Прежде чем использовать градиенты для обновления весов, мы конвертируем их в fp32 и убираем ранее примененное масштабирование, чтобы оно не влияло на скорость обучения (градиенты умножаются на learning rate, и если мы не уберем масштабирование, то реальный learning rate будет отличаться от заданного).
Если соблюдены определенные критерии, такие как версия cuBLAS/cuDNN и размерности матриц, операции будут выполняться тензорными ядрами. Критерии можно найти на слайдах от NVIDIA (стр. 18–19).
for images, labels in zip(data, targets):
optimizer.zero_grad()
with autocast(device_type='cuda', dtype=torch.float16):
outputs = model.forward(images)
loss = criterion(outputs,labels)
scaler.scale(loss).backward()
scaler.step(optimizer)
scaler.update()
amp (fp16) training batch_size=160 approx. 13GB of VRAM
250 Вт => 80 процентов от пиковой производительности (при 480Вт)
300 Вт => 90 процентов
350 Вт => 95 процентов
Энергоэффективность немного хуже, чем у tf32, потому что некоторые операции выполняются в fp32 и не используют тензорные ядра.
fp16 (.half ())
Используя .half()
на наших данных и модели, мы преобразуем их в fp16. Это быстрее, чем amp, но приводит к нестабильному обучению и может привести к большому количеству NaN’ов.
fp16 (.half ()) training batch_size=160 approx. 11GB of VRAM
200 Вт => 80 процентов от пиковой производительности (при 480Вт)
230 Вт => 90 процентов
270 Вт => 95 процентов
Это наиболее энергоэффективное решение: мы вообще не используем fp32.
Теперь рассмотрим inference.
fp32 inference batch_size=2048 approx. 21GB of VRAM
290 Вт => 80 процентов от пиковой производительности (при 480Вт)
350 Вт => 90 процентов
390 Вт => 95 процентов
tf32 inference batch_size=2048 approx. 21GB of VRAM
250 Вт => 80 процентов от пиковой производительности (при 480Вт)
300 Вт => 90 процентов
350 Вт => 95 процентов
amp (fp16) inference batch_size=2048 approx. 16GB of VRAM
260 Вт => 80 процентов от пиковой производительности (при 480Вт)
310 Вт => 90 процентов
360 Вт => 95 процентов
fp16 (.half ()) inference batch_size=2048 approx. 11GB of VRAM
260 Вт =>80 процентов от пиковой производительности (при 480Вт)
310 Вт => 90 процентов
360 Вт => 95 процентов
TensorRT
Мы можем использовать фреймворк TensorRT для дальнейшей оптимизации нашего инференса.
В следующих экспериментах мы конвертируем нашу модель в onnx, оптимизируем его, преобразуем onnx в TensorRT и затем преобразуем модель TensorRT в pytorch jit. На данный момент преобразование модели pytorch сразу в TensorRT не работает для ViT, подробнее в этом issue.
TensorRT fp16 vs pytorch fp16 (.half ()) batch_size=512
Batch size = 512, потому что не хватило VRAM для генерации TensorRT модели, которая поддерживает бОльший батч.
.half ()
260 Вт => 82 процента от пиковой производительности (при 480Вт)
310 Вт => 90 процентов
360 Вт => 95 процентов
TensorRT fp16
290 Вт => 81 процент от пиковой производительности (при 480Вт)
340 Вт => 90 процентов
390 Вт => 95 процентов
Инференс. Время на 1 батч
Инференс. 100 процентов для каждого из режимов индивидуально
Инференс. 100 процентов это TensorRT fp16
Общие графики
Инференс. 100 процентов для каждого из режимов индивидуально
Инференс. 100 процентов это fp16 .half ()
Train. 100 процентов для каждого из режимов индивидуально
Train. 100 процентов это fp16 .half ()
Выводы
Если хотите чтобы GPU нагревался меньше:
используйте более низкую точность. Это не только быстрее, но и кривая производительность/мощность насыщается быстрее, а значит, вы можете снизить power limit без особого ущерба для производительности.
330–360 Вт — хороший диапазон для power limit. Может показаться, что отличие от стокового power limit незначительно, но даже такое уменьшение может заставить GPU работать немного прохладнее (3–5 градусов Цельсия) и уменьшить RPM вентиляторов.
В реальности прирост может быть еще больше. Например, сейчас я делаю инференс DDPM модели, и снижение power limit с 390 Вт до 300 Вт снижает скорость лишь на 8 процентов, причем утилизация GPU 100%.
Код для всех экспериментов можно найти на Github