Introduction
In our last lesson, we navigated the implementation of the Layer Normalization backward pass, transforming its gradient equations into high-performance CUDA code. We saw how fundamental parallel primitives, especially reduction, are key to optimizing core components of modern neural networks.
Today, we shift our focus to another essential operator in the Transformer architecture: the Softmax function. While it appears simple mathematically, a naive implementation harbors a critical flaw that can bring computations to a grinding halt. Our goal is to dissect this issue—numerical instability—and understand the elegant mathematical solution that ensures our calculations are robust. This is a crucial step before we translate this stable algorithm into a parallel CUDA kernel in our next lesson.
The Softmax Function: From Scores to Probabilities
The softmax function is typically found at the end of classification models or within the attention mechanism of Transformers. Its purpose is to take a vector of arbitrary real-valued numbers, often called "logits," and convert them into a probability distribution. This means the output values are all between 0 and 1, and their sum is 1.
The function is defined for each element of an input vector as:
In words, the softmax of an element is its exponential divided by the sum of the exponentials of all elements in the vector.
Let's walk through a quick example.
Writing a Softmax Kernel From Scratch - Benji Visser
This article by Benji Visser provides a clear, step-by-step calculation for a simple input.
Please read the section titled "Softmax for beginners" and follow the three calculation steps for the input logits [2.0, 1.0, 0.1]. This will solidify your understanding of the basic mechanics.
As you see, the process is straightforward. However, this simplicity hides a significant problem when we move from small, well-behaved numbers to the large-magnitude values frequently encountered in deep neural networks.
Numerical Instability: The Achilles' Heel of Naive Softmax
The core of the problem lies in the exponential function and the finite limits of computer floating-point arithmetic. Your interest in low-level systems makes this a perfect opportunity to connect high-level algorithms to hardware realities.
Floating-point numbers, such as the float (32-bit) or double (64-bit) types in C++, cannot represent infinitely large or infinitely small numbers. They have a defined range.

When a calculation produces a number larger than the maximum representable value, it results in an overflow, typically represented as infinity (inf). The exponential function grows extremely rapidly, making it a prime candidate for causing overflow.
To see this in action, let's explore what happens with larger inputs.
Numerically Stable Softmax and Cross Entropy | Jay Mody
Jay Mody's blog post provides an excellent demonstration of this failure case.
Start by reading the section on the softmax definition, and then focus on the code block that shows what happens for very large inputs. The explanation of overflow, underflow, and nan is key here.
As the resource highlights, np.exp(2000) overflows to inf. The subsequent calculation becomes inf / inf, which is an indeterminate form that results in nan (Not a Number). A single nan in your computations can poison the entire training process, as any further arithmetic involving it will also result in nan. This effectively destroys the model's ability to learn.
While overflow is the catastrophic failure, underflow can also be a problem. If an input is a large negative number (e.g., -1000), becomes a number so close to zero that the computer flushes it to 0.0. This leads to a loss of precision which, while less severe than nan, is still undesirable.
The Solution: The Max-Subtraction Trick
To make softmax robust, we need a way to prevent the arguments to the exponential function from becoming too large. The solution is a simple but powerful mathematical adjustment known as the max-subtraction trick.
We can subtract a constant from every element inside the exponentials without changing the final result. Watch this short video for a quick intuition.
How to write a fast Softmax kernel
This clip from Simon Oz concisely explains why we can subtract a constant and why subtracting the maximum is the best choice.
Watch from the explanation of the problem and its mitigation.
Now, let's formalize this. The mathematical proof is elegant and relies on basic properties of exponents.
Numerically Stable Softmax and Cross Entropy | Jay Mody
This section of Jay Mody's article provides the formal derivation.
Read the section that begins with the derivation. Follow the algebraic steps showing that multiplying the numerator and denominator by a constant C is equivalent to adding \log C to each exponent.
By choosing our constant such that , we arrive at the numerically stable softmax formula:
This simple change has profound benefits:
- Prevents Overflow: The largest argument to any exponential is now where is the maximum value in the vector. This evaluates to . Since all other inputs are smaller, every term is less than or equal to zero. The result of for is always in the range , completely avoiding overflow.
- Prevents Division by Zero: Because at least one exponentiated term is , the denominator is guaranteed to be at least 1. This prevents any division-by-zero errors that could arise if all terms underflowed to zero.
For a different perspective that also leads into implementing this logic, watch the following video.
Log Softmax Explained with Python!
This video from Deep Learning with Yacine provides a great walkthrough of the math, connecting it directly to an implementation plan.
First, watch the explanation of the normalization trick, which demonstrates the mathematical equivalence. Then, continue through the derivation using logarithms from this timestamp. While the video discusses LogSoftmax, the core stability trick is identical to what we need for softmax.
Visualizing Softmax Behavior
The softmax function is sometimes described as a "soft" version of the argmax function (which returns the index of the maximum element). As the difference between the largest value and all other values increases, the output of softmax becomes "harder," concentrating almost all the probability mass on that single largest value.

The max-subtraction trick is essential for reliably computing these "hard" distributions. Without it, the green line ([20, 30, 50, 60]) would likely result in [nan, nan, nan, nan] because exp(60) would overflow a standard 32-bit float. By shifting the inputs to [-40, -30, -10, 0], the calculation becomes stable while preserving the same output distribution.
Conclusion
In this lesson, we have moved from a naive understanding of the softmax function to a robust, production-ready formulation. This journey from a simple equation to a numerically stable algorithm is a perfect example of the challenges and responsibilities of a high-performance computing engineer. You cannot just implement the math; you must implement it in a way that respects the physical limitations of the hardware.
Key Takeaways:
- The standard softmax formula, , is numerically unstable due to the rapid growth of the exponential function.
- Large positive inputs can cause overflow, leading to
infandnanvalues that corrupt the computation. - The root cause is the finite range of floating-point number representations in hardware.
- The max-subtraction trick creates a mathematically equivalent and numerically stable formula: .
- This trick guarantees that the largest value passed to the exponential function is 0, thus preventing overflow and ensuring a stable computation.
Now that we have a solid mathematical and conceptual foundation, our next step is to implement this stable algorithm in CUDA. This will require a two-pass approach that leverages the parallel reduction techniques you have already mastered.