# Optimal power limit for deep learning tasks on RTX 3090

So I've recently bought a used RTX 3090 for my experiments with training some nets and found out how much power it consumes and how hot it is.

The stock power limit is 390 watts and can be increased up to 480 watts. The GPU generates so much heat that my CPU temperatures noticeably increase. Running the GPU at 80% fan usage with temperatures reaching 70°C is quite hot, so I began researching methods to reduce the heat. The main method for reducing heat is undervolting: less voltage => less power => less heat. Unfortunately linux drivers don't support undervolting. So the only option left is to reduce power limit.

Power limit is the maximum amount of power GPU can draw. This limit is maintained by adjusting frequencies and voltages to ensure that power consumption remains under the specified limit.
I reduced the power limit to 250 watts, and the performance didn't drop as much as I had expected. I've decided to explore how power limit affects different dl workloads:

- Training:
- fp32
- tf32
- amp fp16
- fp16 (.half())

- Inference:
- fp32
- tf32
- amp fp16
- fp16 (.half())
- TensorRT fp16

All tests were conducted with vit_base_patch16_224 from timm library.

# fp32 training batch_size=160 approx. 20GB of VRAM

280 watts => 80 percent of peak performance(480 watts)

330 watts => 90 percent

380 watts => 95 percent

# tf32

by using

```
torch.backends.cuda.matmul.allow_tf32 = True
```

we can tell pytorch to use TF32 computation mode. Basically, we can sacrifice some of the fp32 precision to get faster matrix operations. Images below describe how it works and were taken from a blog post by NVIDIA. TF32 operations can be accelerated by Tensor Cores.

# tf32 training batch_size=160 approx. 20GB of VRAM

230 watts => 80 percent of peak performance(480 watts)

280 watts => 90 percent

320 watts => 95 percent

as we can see, Tensor Cores are not only faster but also much more energy efficient.

# amp (fp16)

by using `torch.autocast`

we can tell pytorch to use fp16 instead of fp32 for some operations. List of operations. Once again we trade precision for performance. We can see, that some ops like matmul, conv2d are autocasted to fp16, but ops that require better precision, like sum, pow, loss functions and norms are autocasted to fp32.

There is one problem with using fp16: sometimes gradients are so small, they can't be represented in fp16 and are rounded to zero.

zero gradient => zero weight update => no learning.

This is why we multiply loss values by a scaling factor (which is estimated automatically), so that after backward pass we get non-zero gradient values. Before using gradients for weight updates, we convert them to fp32 and unscale them, so that our scaling doesn't interfere with learning rate.

If certain criteria are met, like cuBLAS/cuDNN version and dimensions of the matrices are right, operations will be carried out by tensor cores. NVIDIA slides 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 watt => 80 percent of peak performance(480 watts)

300 watt => 90 percent

350 watt => 95 percent

Power efficiency is a little bit worse than tf32, because some operations are fp32 and don't use Tensor Cores.

# fp16 (.half())

by using `.half()`

on our data and model, we convert them to fp16. This is faster than amp but leads to unstable training and can result in a lot of NaNs.

# fp16 (.half()) training batch_size=160 approx. 11GB of VRAM

200 watt => 80 percent of peak performance(480 watts)

230 watt => 90 percent

270 watt => 95 percent

This is the most power efficient solution: we don't use fp32 at all.

# fp32 inference batch_size=2048 approx. 21GB of VRAM

290 watt => 80 percent of peak performance(480 watts)

350 watt => 90 percent

390 watt => 95 percent

# tf32 inference batch_size=2048 approx. 21GB of VRAM

250 watts => 80 percent of peak performance(480 watts)

300 watts => 90 percent

350 watts => 95 percent

# amp (fp16) inference batch_size=2048 approx. 16GB of VRAM

260 watts => 80 percent of peak performance(480 watts)

310 watts => 90 percent

360 watts => 95 percent

# fp16 (.half()) inference batch_size=2048 approx. 11GB of VRAM

260 watts => 80 percent of peak performance(480 watts)

310 watts => 90 percent

360 watts => 95 percent

# TensorRT

We can use TensorRT framework to further optimize our inference. In the next experiments we convert our code to onnx, optimize it, convert onnx to TensorRT and then convert TensorRT model to pytorch jit. At the moment converting pytorch model straight to TensorRT is not working for vision transformer, more details in this issue.

# TensorRT fp16 vs pytorch fp16 (.half()) batch_size=512

Batch size is 512, because I don't have enough VRAM to convert model with bigger batch size to TensorRT.

.half()

260 watts => 82 percent of peak performance(480 watts)

310 watts => 90 percent

360 watts => 95 percent

TensorRT fp16

290 watts => 81 percent of peak performance(480 watts)

340 watts => 90 percent

390 watts => 95 percent

Here 100 percent is the maximum performance of TensorRT model:

# Some more charts

Here 100 percent is the maximum performance of each of the models:

Here 100 percent is the maximum performance of fp16 .half() model:

Train relative performance.

Here 100 percent is the maximum performance of fp16 .half() model:

# Conclusion

If you want your GPU to overheat less:

- use lower precision. Not only it's faster, but the performance/power curve saturates faster meaning that you can lower power limit without sacrificing much of performance
- 330-360 watts is a good range for the power limit. It might not seem significantly different from the stock limit, but it can make your GPU run a bit cooler (3-5 degrees Celsius) and reduce fan RPM.

Code for all the experiments is available on Github.