Недавно я купил б/у 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
Операция 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 батч
Инференс. Время на 1 батч
Инференс. 100 процентов для каждого из режимов индивидуально
Инференс. 100 процентов для каждого из режимов индивидуально
Инференс. 100 процентов это TensorRT fp16
Инференс. 100 процентов это TensorRT fp16

Общие графики

Инференс. 100 процентов для каждого из режимов индивидуально
Инференс. 100 процентов для каждого из режимов индивидуально
Инференс. 100 процентов это  fp16 .half()
Инференс. 100 процентов это fp16 .half()
Train. 100 процентов для каждого из режимов индивидуально
Train. 100 процентов для каждого из режимов индивидуально
Train. 100 процентов это  fp16 .half()
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

Комментарии (14)


  1. kovserg
    08.01.2024 16:26
    +3

    А вариант улучшить теплоотвод не рассматривали?


    1. hardtop
      08.01.2024 16:26
      +1

      Корпус должен быть сильно продуваемым (сверзху вентиляторы на выдув), да и всё равно видюшка дует прямо в кулер проца. Так что часто надо ставить водянку на проц. Да и все равно вентили на 3090 будут шуметь, дабы отвести сво 400Вт тепла. Мне, например не сильна важно, за сколько там условный whisper сделает транскрибацию - за 2 мин или за 2.5 минут - зато тише.


    1. qwertyforce Автор
      08.01.2024 16:26

      В корпусе 3 вентилятора на вдув, 1 на выдув.
      Поменял термопрокладки на Fehonda, термопасту на Thermal Grizzly Kryonaut Extreme
      Стало лучше, но все равно слишком много тепла выделяется, решил понижать мощность


      1. taktike
        08.01.2024 16:26

        Имхо маловато, у меня 3080 под нагрузкой с такой конфигурацией как у вас (3 вдув - 1 выдув) грелась весьма хорошо, добавил сверху еще два вентилятора вдув-выдув (не уверен что оптимально) и стало сильно приятнее жить (на 10-15 градусов)


  1. melodictsk
    08.01.2024 16:26
    +2

    А изменение частоты памяти? В майнинге часто уменьшение частоты памяти даёт увеличение производительности. Связано с уменьшением задержек (таймингом). А алгоритмам, которые требуют больших последовательных обращений, наоборот необходим разгон.


  1. iiiytn1k
    08.01.2024 16:26
    +5

    Прогнал несколько тестов на 4090:

    Train fp32

    Train tf32

    Train fp16 amp

    Train fp16 half

    Интересно, что кривые tf32 и fp16 amp практически одинаковые.

    Текстовые результаты
    fp32_train = [(150, 1916.85302734375), (160, 1726.422119140625), (170, 1572.1138916015625), (180, 1442.38671875), (190, 1337.249755859375), (200, 1242.3446044921875), (210, 1172.5013427734375), (220, 1095.2509765625), (230, 1029.110595703125), (240, 971.7257690429688), (250, 921.0980224609375), (260, 875.9171752929688), (270, 826.3692626953125), (280, 786.3384399414062), (290, 752.35595703125), (300, 721.5491943359375), (310, 688.0049438476562), (320, 663.115478515625), (330, 647.8905029296875), (340, 627.601806640625), (350, 601.1165161132812), (360, 591.7486572265625), (370, 570.9195556640625), (380, 560.3616333007812), (390, 554.3623657226562), (400, 550.8558349609375), (410, 541.1676025390625), (420, 537.27685546875), (430, 533.3079223632812), (440, 534.1802368164062), (450, 525.8939208984375)]
    
    tf32_train =[(150, 1130.47998046875), (160, 1045.67822265625), (170, 958.404052734375), (180, 872.135498046875), (190, 801.3380737304688), (200, 733.6597290039062), (210, 674.9171752929688), (220, 630.9574584960938), (230, 591.8206787109375), (240, 557.5418090820312), (250, 529.1500244140625), (260, 503.8023986816406), (270, 482.4733581542969), (280, 464.09136962890625), (290, 453.2052917480469), (300, 446.3533630371094), (310, 442.28253173828125), (320, 438.00335693359375), (330, 433.1138610839844), (340, 430.9553527832031), (350, 430.3887023925781), (360, 423.78192138671875), (370, 421.5494384765625), (380, 418.23504638671875), (390, 417.9413757324219), (400, 417.5173034667969), (410, 416.3753662109375), (420, 414.67138671875), (430, 414.67596435546875), (440, 415.1285095214844), (450, 415.0179443359375)]
    
    fp16_amp_train = [(150, 713.5382080078125), (160, 644.8378295898438), (170, 582.9127807617188), (180, 531.542724609375), (190, 487.0350646972656), (200, 449.3820495605469), (210, 419.1130065917969), (220, 393.4111633300781), (230, 370.9094543457031), (240, 346.04071044921875), (250, 329.3534851074219), (260, 313.56951904296875), (270, 299.94134521484375), (280, 289.3700866699219), (290, 283.7044982910156), (300, 281.9547424316406), (310, 278.1085205078125), (320, 275.4595947265625), (330, 273.2633972167969), (340, 271.5821228027344), (350, 270.0470886230469), (360, 266.6712646484375), (370, 265.72222900390625), (380, 264.9597473144531), (390, 263.90924072265625), (400, 263.3031921386719), (410, 262.78692626953125), (420, 262.68572998046875), (430, 262.64483642578125), (440, 262.59173583984375), (450, 262.6723327636719)]
    
    fp16_half_train = [(150, 432.8243103027344), (160, 380.8467712402344), (170, 346.6760559082031), (180, 312.84552001953125), (190, 287.2230529785156), (200, 266.4410400390625), (210, 248.6437225341797), (220, 235.57911682128906), (230, 225.4227752685547), (240, 218.5994415283203), (250, 215.1365509033203), (260, 212.386962890625), (270, 210.07582092285156), (280, 208.22596740722656), (290, 206.6107177734375), (300, 205.4788055419922), (310, 204.30592346191406), (320, 203.26885986328125), (330, 202.8826904296875), (340, 202.81948852539062), (350, 202.8431396484375), (360, 202.87684631347656), (370, 202.7930450439453), (380, 202.87242126464844), (390, 202.78543090820312), (400, 202.8188934326172), (410, 202.78146362304688), (420, 202.84884643554688), (430, 202.83177185058594), (440, 202.92153930664062), (450, 203.05181884765625)]

    В прикладных задачах (инференс и обучение лор/dreambooth в SD, обучение моделей RVC (в большинстве случаев работа с fp16 и bf16)) работаю с лимитом мощности в 70%. Разница в производительности по сранению со стоковыми 450 Вт на уровне погрешности, но при этом тепловыделение меньше на 135 Вт, что даёт снижение температуры GPU на 10 градусов.


    1. qwertyforce Автор
      08.01.2024 16:26
      +1

      Спасибо за тесты!
      А когда fp16 amp тестили, там tf32 было выключено?


  1. hardtop
    08.01.2024 16:26
    +2

    Полезное исследование. Я даже свою 3060 переодически ограничиваю по ТДП. А тут столько мощи при 250 Вт. Тоже задумался о 3090.


  1. Slava715
    08.01.2024 16:26
    +2

    "К сожалению, драйверы NVIDIA под Linux не поддерживают понижение напряжения. Поэтому единственным вариантом остается понижение лимита мощности (power limit)."

    Можно снизить напряжение так:
    nvidia-settings -a /GPUGraphicsClockOffsetAllPerformanceLevels=100


    1. qwertyforce Автор
      08.01.2024 16:26
      +1

      конкретно эта команда увеличит clock на 100, но да, есть такой метод, но он довольно костыльный
      как я понял суть такая: сделать power limit, и затем разогнать частоты.
      Мне он не понравился, решил просто лимит понижать


      1. Slava715
        08.01.2024 16:26
        +1

        Да, так и есть, карта будет упираться в максимальные частоты.

        Забыл добавить что вместе с этим можно зафиксировать максимальные частоты:
        nvidia-smi -pm 1
        nvidia-smi -lgc 100, 2000


  1. snakers4
    08.01.2024 16:26
    +1

    Стоковый power limit составляет 390 Вт, но может быть увеличен до 480 Вт. GPU выделяет так много тепла, что начинает нагревать CPU.

    Правильно понимаю, что у вас карты не выбрасывают воздух наружу?

    subprocess.run(["nvidia-smi","-pl",str(pwr)])

    Это делать надо делать просто при каждой загрузке?


    1. qwertyforce Автор
      08.01.2024 16:26
      +1

      >Правильно понимаю, что у вас карты не выбрасывают воздух наружу?
      Выбрасывают, но окружение все равно нагревается


      >Это делать надо делать просто при каждой загрузке?
      ага, или можно какой-нибудь сервис сделать для systemd, чтобы он при загрузке запускался


      1. snakers4
        08.01.2024 16:26
        +1

        Прикольно. Проблемы со сбросом тепла у нас не стоит, но запомню, слышал про эту фишулю, но пока надобности не было тестить.