Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 34

Mixed-Precision Training: Balancing Speed and Accuracy

Recommended resources

In our last lesson, we explored how gradient accumulation allows us to simulate large, stable batch sizes, even on memory-constrained hardware. We tackled the memory bottleneck caused by storing activations for many data samples at once. Now, we turn our attention to another, equally critical dimension of memory usage: the numerical precision of the numbers themselves. This lesson will show how we can often get away with using half the memory for every single number in our model, and often get a significant speed-up in the process.

Today's learning objective is to explain mixed-precision training (e.g., bfloat16) and how it reduces memory usage while maintaining numerical stability. We'll investigate why naively reducing precision can cause training to fail spectacularly and then uncover the elegant set of techniques that modern frameworks use to get the best of both worlds: the speed and memory efficiency of low precision and the stability of high precision.

The Promise of Lower Precision

At its heart, a neural network is a massive collection of floating-point numbers—weights, biases, activations, and gradients. By default, these are stored in a 32-bit single-precision format, commonly known as float32 or FP32. Your computer science background gives you an innate understanding that data representation is a series of trade-offs. Let's make that concrete for floating-point numbers.

A floating-point number is typically composed of three parts: a sign bit, an exponent, and a fraction (or mantissa). The number of bits allocated to the exponent determines the range of numbers that can be represented (how large or small they can be), while the bits for the mantissa determine the precision (how many significant digits can be stored).

To see exactly how this works for FP32 and its lower-precision counterparts, the following video provides a superb from-the-ground-up explanation.

Mixed Precision Training | Explanation and PyTorch Implementation from Scratch

This video, from the channel ExplainingAI, provides an excellent deep dive into floating-point representation.

Please watch the first part of the video, from the beginning. The narrator methodically deconstructs the float32 format, explaining the role of the sign, exponent, and mantissa bits, and how they define the range and precision of representable numbers. Focus on the distinction between the exponent's role in defining range and the mantissa's role in defining precision.

The idea behind mixed-precision training is simple: if we can use fewer bits, say 16, we can cut our memory usage in half. This applies to the model's weights, the gradients, and the memory-hungry activations we discussed in the last lesson. There are two main 16-bit formats used in deep learning: float16 (FP16) and bfloat16 (BF16).

A comparison of the bit allocation in float16 ("half" precision) and bfloat16 ("brain" float). Note the critical difference in exponent and fraction bits.

A comparison of the bit allocation in float16 ("half" precision) and bfloat16 ("brain" float). Note the critical difference in exponent and fraction bits.

As the image shows, they make a different trade-off:

  • float16 (FP16): Allocates 5 bits for the exponent and 10 for the fraction. Compared to FP32, it has a drastically smaller range but retains reasonable precision.
  • bfloat16 (BF16): Allocates 8 bits for the exponent (the same as FP32!) and 7 for the fraction. It has the same massive range as FP32 but much lower precision.

This reduction in bits yields two major benefits:

  1. Reduced Memory Footprint: Halving the bits per number roughly halves the memory required for the model, its gradients, and optimizer states. This enables training larger models or using larger batch sizes, synergizing perfectly with gradient accumulation.
  2. Increased Computational Speed: Moving fewer bits around reduces memory bandwidth bottlenecks. More importantly, modern GPUs (like NVIDIA's A100 and H100 series) have specialized hardware units called Tensor Cores that execute 16-bit matrix multiplications far faster than 32-bit operations.

The following segment of the video explains these benefits in more detail.

Mixed Precision Training | Explanation and PyTorch Implementation from Scratch

This segment continues the explanation by comparing the 16-bit formats and detailing the advantages of using them.

Watch from the comparison of the different float types, paying attention to the measuring tool analogy, then continue through the explanation of the advantages of lower precision, focusing on memory savings and the speedup from Tensor Cores.


The Peril: Numerical Instability

Given these advantages, an obvious question arises: why not just cast everything to FP16 and enjoy the benefits? The answer lies in its limited range.

Gradient Underflow

The biggest problem is gradient underflow. During training, especially in deep networks, it's common for gradient values to become extremely small. The smallest positive normalized number FP16 can represent is approximately . Any gradient smaller than this gets "flushed to zero." The update for that weight becomes zero, and the model simply stops learning.

This isn't a theoretical concern. The NVIDIA documentation for mixed precision provides a stark visualization of this problem.

Train With Mixed Precision

This document from NVIDIA, the creator of the hardware that pioneered this technique, clearly illustrates the problem.

Read the section "Considering When Training With Mixed Precision". The key element here is Figure 2, the histogram of gradient magnitudes. Observe how a large portion of the gradient values fall into a range that is too small for FP16 to represent, effectively being lost.

The video we've been watching provides a brilliant low-level explanation for why even updates that don't underflow can be lost.

Mixed Precision Training | Explanation and PyTorch Implementation from Scratch

Let's return to the ExplainingAI video for a bit-level demonstration of the problem.

Watch the section from the explanation of instability. The walkthrough of adding 1 and 1e-4 in FP16 is a perfect illustration of how limited precision causes small updates to be lost. This is the root cause of training stagnation.


The Solution: The Mixed-Precision Recipe

To safely harness the power of FP16, we can't use it naively. Instead, we use a "mixed" approach, combining FP16 and FP32 in a clever recipe. This recipe has two main ingredients.

1. Maintaining an FP32 Master Copy of Weights

The first problem we saw was that even if a gradient survives, the update weight += learning_rate * gradient might result in a value too small to change the FP16 weight.

The solution is to perform the update in a higher-precision format. We keep a master copy of the model weights in FP32. The training loop then looks like this:

  1. For the forward and backward pass, use a temporary FP16 copy of the weights to get the speed and memory benefits.
  2. After computing the gradients, use them to update the FP32 master copy. In FP32, the tiny updates can be correctly accumulated over many steps.
  3. Copy the updated FP32 weights back to the FP16 format for the next iteration.

Mixed Precision Training | Explanation and PyTorch Implementation from Scratch

The video continues by explaining this first part of the solution.

Please watch the segment from explaining the master copy. It clearly outlines how this approach prevents the loss of small weight updates.

2. Loss Scaling

Maintaining an FP32 master copy solves the update problem, but it doesn't solve the initial gradient underflow issue. If the gradients are already zero before they even reach the optimizer, there's nothing to update.

The solution is loss scaling. The logic is as follows:

  1. Before the backward pass, multiply the loss (a single scalar) by a large scaling factor, say .
  2. By the chain rule of calculus, all the gradients computed during the backward pass will automatically be scaled by this same factor .
  3. This scaling pushes the tiny gradient values up into the representable range of FP16, preventing them from becoming zero.
  4. Before the optimizer updates the FP32 master weights, the gradients are unscaled by dividing them by . This returns them to their correct magnitude, ensuring the weight update is correct.

This process ensures that gradient information is preserved through the FP16 backpropagation. The final mixed-precision training loop combines both techniques.

The full mixed-precision training algorithm. This visualizes the interplay between FP16 and FP32 copies of weights and gradients, as well as the loss scaling and unscaling steps.

The full mixed-precision training algorithm. This visualizes the interplay between FP16 and FP32 copies of weights and gradients, as well as the loss scaling and unscaling steps.

The video explains this final, crucial piece of the puzzle.

Mixed Precision Training | Explanation and PyTorch Implementation from Scratch

This final segment from the video explains loss scaling and other stability considerations.

Watch from the section on loss scaling. Notice how it directly addresses the gradient underflow problem. Also, briefly watch the subsequent section on per-operation policies, which notes that some sensitive operations like SoftMax are often kept in FP32 for stability, giving the technique its "mixed" name.

In practice, the scaling factor is not fixed. Modern frameworks use dynamic loss scaling, where the training process automatically adjusts . It starts with a large , and if it detects an overflow (gradients becoming inf or NaN), it reduces and skips the update for that step. If training proceeds for many steps without overflow, it attempts to increase again, always trying to use the maximum dynamic range of FP16 without causing instability.


BFloat16: A Modern, Simpler Alternative

The complexity of FP16 training (master weights, dynamic loss scaling) stems entirely from its limited range. This led to the development of bfloat16 (BF16). By having the same 8-bit exponent as FP32, BF16 has the same dynamic range as FP32.

This means gradient underflow and overflow are no longer significant concerns. BF16 training is therefore much simpler:

  • Loss scaling is generally not required.
  • You still benefit from FP32 master weights to accumulate small updates with higher precision, but the most complex part of the process is removed.

The trade-off is BF16's lower precision (7 mantissa bits vs. FP16's 10). However, it turns out that deep neural networks are remarkably resilient to this kind of noise, but extremely sensitive to the range issues that plague FP16. For this reason, on modern hardware that supports it (e.g., NVIDIA A100/H100, Google TPUs), BF16 is the preferred format for mixed-precision training.

Numerical Stability: Why FP16 Training Breaks (And How ...

This Medium article gives an excellent, intuitive overview of the entire topic and provides a clear recommendation for modern practice.

Please read the sections on BFloat16, which explains why it's a superior format for deep learning. Then, review the practical advice in "When to Use What" and the summary in "The Deeper Insight". This will cement your understanding of the real-world trade-offs.


Conclusion

In this lesson, we have dissected mixed-precision training, a cornerstone of efficient, large-scale model pre-training. We've seen that it's not a simple switch, but a carefully engineered system that balances computational efficiency with numerical reality.

Here are the key takeaways:

  • The Goal: Reduce memory usage and increase speed by using 16-bit floating-point formats (FP16, BF16) instead of the standard 32-bit (FP32).
  • The Problem with FP16: Its limited dynamic range leads to gradient underflow (small gradients becoming zero) and lost weight updates, which stalls training.
  • The FP16 Solution (Mixed Precision):
    1. Use FP16 for forward/backward passes to save memory and leverage fast hardware.
    2. Maintain a master copy of weights in FP32 to accurately accumulate small updates.
    3. Use (dynamic) loss scaling to shift small gradients into the representable range of FP16, preventing underflow.
  • The Modern Solution (BF16): bfloat16 has the same range as FP32, largely eliminating the underflow/overflow problems. This makes training much simpler as loss scaling is typically unnecessary, making BF16 the preferred format on supporting hardware.

We have now covered the two primary techniques for managing computational resources on a single GPU: gradient accumulation for handling large batches, and mixed-precision training for optimizing memory and speed at the data-type level. With these tools, we can train much larger and more powerful models. However, even these techniques have their limits. To train truly massive models like GPT-3, we must scale beyond a single device. In our next lesson, we will begin exploring how to do this, starting with data parallelism.