CUDA Approximate Sigmoid
Introduction
Floating point subnormal values can impact the performance of certain mathematical operations on GPU because they require additional handling by the instructions on the hardware. This impact can be more pronounced in workloads that are bottlenecked by instruction throughput.
If some operations are not sensitive to subnormal values, using flush-to-zero (FTZ) instructions or other mathematical approximations can improve performance by treating subnormal values as zero, thereby reducing the instruction overhead associated with handling them. Sigmoid is one such operation where FTZ can be applied effectively.
In this blog post, I would like to quickly discuss how approximation techniques can be applied to the sigmoid function in CUDA to improve performance in instruction-throughput-bound workloads.
CUDA Flush-To-Zero Sigmoid
We have two implementations of the approximated sigmoid function in CUDA. The first one is FTZ based and it results in four instructions per element, which is much fewer than the native non-FTZ reference implementation. The second one is even more efficient but comes at the cost of slightly reduced numerical accuracy.
The first implementation changes the sigmoid computation formula mathematically to make it more amenable to FTZ optimizations. Specifically, it rewrites the sigmoid function as:
$$
\begin{align}
\text{sigmoid}(x)
&= \frac{1}{1 + e^{-x}} \\
&= \frac{1}{1 + 2^{-x \cdot \log_2 e}} \\
\end{align}
$$
This allows the use of FTZ-friendly instructions for the exponential and reciprocal operations, including ex2.approx.ftz.f32 for the base-2 exponential and rcp.approx.ftz.f32 for the reciprocal.
The second implementation approximates the sigmoid function using tanh, leveraging the mathematical identity:
$$
\begin{align}
\text{sigmoid}(x) &= \frac{1}{2} \left(1 + \tanh\left(\frac{x}{2}\right)\right)
\end{align}
$$
There is tanh.approx.f32 instruction in CUDA that provides an approximate computation of the hyperbolic tangent function, which can results in more efficient instructions as we will see later.
The implementations of the sigmoid functions using the native tl.sigmoid, the FTZ-based approach, and the tanh approximation in Triton are provided below. We will measure their numerical parities, benchmark their performances, and examine their PTX and SASS code after compilation.
1 | #!/usr/bin/env python3 |
I run the Triton sigmoid comparison script in an NVIDIA PyTorch container nvcr.io/nvidia/pytorch:26.08-py3 on my local machine that has an NVIDIA GeForce RTX 5080 GPU (SM120).
1 | $ python triton_sigmoid.py |
In the performance benchmarks, we could see that the PTX tanh approximation achieves the highest instruction throughput, followed by the FTZ-based implementation, and finally the native tl.sigmoid. The PTX FTZ implementation is about 5% faster than the native tl.sigmoid, whereas the PTX tanh approximation is nearly twice as fast as the native tl.sigmoid.
In the numerical parity checks, we could see that the native tl.sigmoid and the PTX FTZ implementation have exactly the same numerical errors, this is because the difference in the subnormal values for sigmoid is too small to be meaningful in practice. The PTX tanh approximation, on the other hand, introduces a slightly larger numerical error. Whether it is acceptable depends on the specific application and the tolerance for numerical inaccuracies.
By examining the SASS instructions generated for each variant, we can understand how the high-level PTX or Triton code maps to the underlying hardware execution resources. This helps explain the observed performance differences in the benchmarks. In addition to a bunch of FADD and FMUL instructions handled by the FP32 ALU pipeline, the presence of MUFU.EX2, MUFU.RCP, and MUFU.TANH instructions indicates the utilization of the special-function pipeline (SFU) for transcendental operations.
| Variant | PTX Intent | Generated SASS SFU Operations | Main Conclusion |
|---|---|---|---|
tl.sigmoid |
Triton’s built-in sigmoid lowering | MUFU.EX2 + MUFU.RCP |
Compiler-generated implementation, including extra numerical handling |
| PTX FTZ | Explicit ex2.approx.ftz + rcp.approx.ftz |
MUFU.EX2 + MUFU.RCP |
The requested FTZ instruction sequence survived lowering |
| PTX tanh approx | Explicit tanh.approx.f32 |
MUFU.TANH |
Native approximate hardware tanh survived lowering |
Specifically, the major difference between the variants is that there are 2 MUFU instructions in the tl.sigmoid and PTX FTZ variants, whereas the PTX tanh approximation only has 1 MUFU instruction. This has a strong correlation with the observed performance differences, as the PTX tanh approximation can utilize the special-function pipeline more efficiently, leading to higher instruction throughput.
In GPU microarchitecture, different types of instructions are handled by different execution pipelines. For example, the following figure illustrates the NVIDIA GH100 architecture, GH100 SM microarchitecture, and its various execution pipelines on the chip.
Understanding which pipeline an instruction uses can help explain the observed performance characteristics of various implementations. Some of the common SASS instructions and their typical execution resources are summarized in the following table.
| SASS Instruction | Typical Execution Resource |
|---|---|
MUFU.EX2, MUFU.RCP, MUFU.TANH |
Special-function pipeline / SFU |
FADD, FMUL, FFMA |
FP32 ALU pipeline / CUDA cores |
IADD, IMAD, ISETP |
Integer ALU pipeline |
FSETP, FSEL |
FP32 compare/select logic or associated execution pipeline |
LDG, STG |
Load/store pipeline and memory hierarchy |
Our benchmarking results with the SASS instruction analysis indicate that the performance of the sigmoid implementation variants is bottlenecked by the utilization of the special-function (SFU) pipeline, particularly the number of MUFU instructions and their impact on instruction throughput.
Miscellaneous
The performance difference between the various sigmoid implementation variants can vary depending on the GPU architecture and the Triton compiler translation decisions. On some GPU architectures, such as A100 (SM80), the PTX FTZ variant might perform better than the tl.sigmoid variant for about 10%. With different Triton versions, the translation of tl.sigmoid might change, potentially affecting the performance comparison between the variants.
References
CUDA Approximate Sigmoid