WattOps Start free
← Blog

fp16 mixed precision in training: about 70% less GPU energy per step on a T4

Measurement · 23 October 2026 · 6 min read

On this pageThe jobHow it was measuredResultsWhy the saving is all timeThe two sessions disagree on joules, and agree on the savingThe trap: fp16 on two GPUs that was really fp32What this does and does not tell youRun it yourself

Last week we measured a pure matrix multiply on a Tesla T4: fp16 used 81 to 84% less GPU energy than fp32. That is the best case. A training step does more than multiply matrices, so we measured a real training loop too: a BERT model in Hugging Face Trainer, fp32 against fp16 mixed precision, read from the GPU's own energy counter.

The result: fp16 mixed precision used 69 to 70% less GPU energy per training step and ran 3.2 to 3.4× as fast, in two separate Kaggle sessions. Still a large saving, and closer to what you would see in your own run than the matrix-multiply number.

To run it yourself, open the Kaggle notebook: it prints your own numbers on a Kaggle T4.

GPU energy per training step on a Kaggle Tesla T4: fp32 10.36 J and fp16 3.25 J in session 1, fp32 11.97 J and fp16 3.55 J in session 2

The job

How it was measured

One line adds the measurement to an existing Trainer script:

from joules.integrations.huggingface import attach

trainer = Trainer(model=model, args=TrainingArguments(output_dir="out", fp16=True), train_dataset=train)
energy = attach(trainer)
trainer.train()
print(energy.result["j_per_step"])

attach puts the callback first in the Trainer's list, so energy/joules, energy/kwh, energy/avg_watts and energy/joules_per_step appear in every log the Trainer sends to W&B, TensorBoard, MLflow or any other report_to, next to the loss.

Results

RunSession 1: J per stepms per stepSession 2: J per stepms per step
fp32, round 110.4615711.94182
fp16, round 13.27493.5855
fp32, round 210.2615412.00183
fp16, round 23.24483.5153
Median fp32 · fp1610.36 · 3.25156 · 4911.97 · 3.55183 · 54
fp16 saving69% less3.18× as fast70% less3.37× as fast
4-epoch warm-up run, fp32Session 1Session 2
Average power66 W66 W
Time held back by the 70 W power cap99%98%
Clock, share of its maximum60%51%
Busy97%96%
Temperature, start → end54 → 81 °C51 → 80 °C
Spread between epochs (energy)2.0%8.7%

Why the saving is all time

The two sessions disagree on joules, and agree on the saving

Session 2's numbers are 9% (fp16) to 16% (fp32) higher than session 1's. Its T4 held a lower clock under the same 70 W cap (51% of its maximum against 60%), and Kaggle can hand out a different card each session. In session 2 each epoch of the warm-up run also cost more than the one before (1,359 → 1,591 J) as the card heated, which is why its epochs spread 8.7% against 2.0%.

The saving still agreed to one point: 69% and 70%. That is the practical rule: compare runs inside one session, on one machine, never across. A number from last week's session is not a baseline for this week's.

The trap: fp16 on two GPUs that was really fp32

On Kaggle's GPU T4 x2, our first run used both GPUs. The Trainer then wraps the model in PyTorch DataParallel, and in that run (transformers 5.x) the fp16 runs logged exactly the same losses as the fp32 runs: mixed precision never reached the copies of the model on each GPU, so the "fp16" side was fp32. That run's comparison is void, and the numbers above all come from runs on one GPU.

If you train on more than one GPU, check that fp16 and fp32 losses differ, or launch with torchrun so the Trainer uses DistributedDataParallel. A measurement that shows no change at all is worth a second look before anyone concludes that mixed precision saves nothing.

What this does and does not tell you

Run it yourself

The quickest way: open the Kaggle notebook, click Copy & Edit, set the accelerator to GPU T4 x2 and click Run All. It trains on one T4, runs the four alternating runs and prints the comparison, with the loss check.

In your own training script:

pip install -U landauer-gap

then energy = attach(trainer) before trainer.train(), as above. The training guide covers Trainer and PyTorch Lightning, saving runs with track=, and multi-GPU runs. Nothing leaves the machine unless you ask it to.

On a GPU that several jobs share, the same counter can be split between them by each job's SM time: how that works, and a test of it.