<?xml version="1.0"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
	<id>https://www.emmtrix.com/w139/api.php?action=feedcontributions&amp;feedformat=atom&amp;user=Timo.stripf</id>
	<title>emmtrix Wiki - User contributions [en]</title>
	<link rel="self" type="application/atom+xml" href="https://www.emmtrix.com/w139/api.php?action=feedcontributions&amp;feedformat=atom&amp;user=Timo.stripf"/>
	<link rel="alternate" type="text/html" href="https://www.emmtrix.com/wiki/Special:Contributions/Timo.stripf"/>
	<updated>2026-05-09T14:54:13Z</updated>
	<subtitle>User contributions</subtitle>
	<generator>MediaWiki 1.39.10</generator>
	<entry>
		<id>https://www.emmtrix.com/w139/index.php?title=tanh_Software_Implementation&amp;diff=2939</id>
		<title>tanh Software Implementation</title>
		<link rel="alternate" type="text/html" href="https://www.emmtrix.com/w139/index.php?title=tanh_Software_Implementation&amp;diff=2939"/>
		<updated>2026-02-04T12:20:11Z</updated>

		<summary type="html">&lt;p&gt;Timo.stripf: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;[[File:Hyperbolic Tangent.svg|thumb]]&lt;br /&gt;
The hyperbolic tangent function, commonly referred to as &amp;lt;code&amp;gt;tanh&amp;lt;/code&amp;gt;, is a mathematical function that maps real numbers to the range &amp;lt;math&amp;gt;(-1, 1)&amp;lt;/math&amp;gt;. It is defined as:&lt;br /&gt;
&lt;br /&gt;
:&amp;lt;big&amp;gt;&amp;lt;math&amp;gt;\tanh(x) = \frac{e^x - e^{-x}}{e^x + e^{-x}}&amp;lt;/math&amp;gt;&amp;lt;/big&amp;gt;&lt;br /&gt;
&lt;br /&gt;
From a software implementation perspective, &amp;lt;code&amp;gt;tanh&amp;lt;/code&amp;gt; is a numerically challenging function due to the use of exponential functions, which can easily overflow for large inputs and suffer from catastrophic cancellation for very small values of &amp;lt;math&amp;gt;x&amp;lt;/math&amp;gt;. These issues are particularly relevant in single-precision floating-point (&amp;lt;code&amp;gt;float32&amp;lt;/code&amp;gt;) implementations, where both accuracy and performance are critical.&lt;br /&gt;
&lt;br /&gt;
In machine learning and deep learning, &amp;lt;code&amp;gt;tanh&amp;lt;/code&amp;gt; is widely used as an activation function because it introduces non-linearity and produces outputs centered around zero, which can improve gradient-based optimization. As a result, &amp;lt;code&amp;gt;tanh&amp;lt;/code&amp;gt; is often evaluated billions of times during training and inference, making efficient and numerically stable implementations essential.&lt;br /&gt;
&lt;br /&gt;
This article focuses on practical software implementations of &amp;lt;code&amp;gt;tanh&amp;lt;/code&amp;gt;, analyzing different computational formulations with respect to floating-point accuracy, ULP error, overflow behavior, and NaN generation. The goal is to provide a foundation for designing robust &amp;lt;code&amp;gt;tanh&amp;lt;/code&amp;gt; implementations suitable for performance-critical environments such as numerical libraries, embedded systems, and AI workloads.&lt;br /&gt;
&lt;br /&gt;
== Problem Overview ==&lt;br /&gt;
The hyperbolic tangent function poses several challenges when implemented in finite-precision floating-point arithmetic. While the mathematical definition is simple, direct translations into software often suffer from numerical instability and performance issues.&lt;br /&gt;
&lt;br /&gt;
A primary challenge is the use of exponential functions. For large input values, &amp;lt;code&amp;gt;exp(x)&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;exp(-x)&amp;lt;/code&amp;gt; can overflow in single-precision floating-point arithmetic, leading to infinities and, in some formulations, NaN results. Avoiding overflow therefore requires careful reformulation or explicit range handling.&lt;br /&gt;
&lt;br /&gt;
For very small input values, catastrophic cancellation becomes the dominant issue. Expressions such as &amp;lt;math&amp;gt;e^x - e^{-x}&amp;lt;/math&amp;gt; involve the subtraction of nearly equal numbers, causing a severe loss of significant bits and large ULP errors. In these regions, naive implementations may return zero or highly inaccurate results even though &amp;lt;math&amp;gt;\tanh(x) \approx x&amp;lt;/math&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
In addition to accuracy concerns, performance is a critical factor. In many applications, especially machine learning and numerical simulations, &amp;lt;code&amp;gt;tanh&amp;lt;/code&amp;gt; is evaluated extremely frequently. This makes it necessary to balance numerical robustness with the cost of transcendental function calls, branch complexity, and instruction-level efficiency.&lt;br /&gt;
&lt;br /&gt;
==Direct Exponential Formulations==&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+&lt;br /&gt;
!Id&lt;br /&gt;
!Formula&lt;br /&gt;
!Highest ULP Error&lt;br /&gt;
!ULP &amp;lt;= 2.0&lt;br /&gt;
!NaN&lt;br /&gt;
|-&lt;br /&gt;
|exp_v1&lt;br /&gt;
|&amp;lt;math&amp;gt;\frac{e^x - e^{-x}}{e^x + e^{-x}}&amp;lt;/math&amp;gt;&lt;br /&gt;
|x &amp;lt; 0: 8.55638e+08 at -2.98023e-08 with 0x0p+0 instead of -0x1p-25&lt;br /&gt;
&lt;br /&gt;
x &amp;gt; 0: 8.55638e+08 at 2.98023e-08 with 0x0p+0 instead of 0x1p-25&lt;br /&gt;
| -88.7246 &amp;lt; x &amp;lt; -7.14197&lt;br /&gt;
7.14197 &amp;lt; x &amp;lt; 88.7246&lt;br /&gt;
| -inf &amp;lt; x &amp;lt; -88.7246&lt;br /&gt;
88.7246 &amp;lt; x &amp;lt; inf&lt;br /&gt;
|-&lt;br /&gt;
|exp_v2&lt;br /&gt;
|&amp;lt;math&amp;gt;\frac{e^{2x} - 1}{e^{2x} + 1}&amp;lt;/math&amp;gt;&lt;br /&gt;
|x &amp;lt; 0: 8.47249e+08 at -1.49012e-08 with 0x0p+0 instead of -0x1p-26&lt;br /&gt;
x &amp;gt; 0: 8.55638e+08 at 2.98023e-08 with 0x0p+0 instead of 0x1.fffffep-26&lt;br /&gt;
| -inf &amp;lt; x &amp;lt; -0.547821&lt;br /&gt;
0.254822 &amp;lt; x &amp;lt; 8.31787&lt;br /&gt;
&lt;br /&gt;
8.38379 &amp;lt; x &amp;lt; 44.3623&lt;br /&gt;
|44.3623 &amp;lt; x &amp;lt; inf&lt;br /&gt;
|-&lt;br /&gt;
|exp_v3&lt;br /&gt;
|&amp;lt;math&amp;gt;1 - \frac{2}{e^{2x} + 1}&amp;lt;/math&amp;gt;&lt;br /&gt;
|x &amp;lt; 0: 8.59832e+08 at -4.47035e-08 with 0x0p+0 instead of -0x1.8p-25&lt;br /&gt;
x &amp;gt; 0: 8.68221e+08 at 8.9407e-08 with 0x0p+0 instead of 0x1.7ffffcp-24&lt;br /&gt;
| -inf &amp;lt; x &amp;lt; -6.26233&lt;br /&gt;
0.346382 &amp;lt; x &amp;lt; inf&lt;br /&gt;
| -&lt;br /&gt;
|-&lt;br /&gt;
|expm1&lt;br /&gt;
|&amp;lt;math&amp;gt;\frac{expm1(2x)}{expm1(2x) + 2}&amp;lt;/math&amp;gt; &lt;br /&gt;
|x &amp;lt; 0: 2.49585 at -3.95627 with -0x1.ffa00ap-1 instead of -0x1.ffa00ep-1&lt;br /&gt;
x &amp;gt; 0: 2.41657 at 0.0155837 with 0x1.fe9b66p-7 instead of 0x1.fe9b62p-7&lt;br /&gt;
| -inf &amp;lt; x &amp;lt; -5.57227&lt;br /&gt;
-0.42981 &amp;lt; x &amp;lt; 0.00777948&lt;br /&gt;
&lt;br /&gt;
0.125351 &amp;lt; x &amp;lt; 44.3623&lt;br /&gt;
|44.3623 &amp;lt; x &amp;lt; inf&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
The first equation is the definition of the hyperbolic tangent function. This equation requires two exponentials with different values. Since exponentials are expensive to compute, we can use the second equation to reduce the number of exponentials to one.&lt;br /&gt;
&lt;br /&gt;
The second equation has the drawback that both the numerator and the denominator can become infinity for large values of x which results in NaN. That happens for &amp;lt;math&amp;gt;|x| &amp;gt; \log(FLT\_MAX)/2 \approx 44&amp;lt;/math&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
The third equation solves this problem but has a slightly higher error for &amp;lt;math&amp;gt;x \approx 2^{-25}&amp;lt;/math&amp;gt;. In the following graph you can see the error chart for equation 3. The error charts for the other two equations are very similar. All three equations have an error of up to 16 mio ULPs for &amp;lt;math&amp;gt;x &amp;lt; 2^{-5}&amp;lt;/math&amp;gt; which means that up to 24 bits could be wrong.&lt;br /&gt;
&lt;br /&gt;
=== exp_v2 ===&lt;br /&gt;
&lt;br /&gt;
The following error graph shows the ULP error of the &amp;lt;code&amp;gt;exp_v2&amp;lt;/code&amp;gt; formulation across the full &amp;lt;code&amp;gt;float32&amp;lt;/code&amp;gt; input range. &lt;br /&gt;
This formulation reduces the number of exponential evaluations to one, but is particularly sensitive to catastrophic cancellation for very small input values and to overflow for large &amp;lt;math&amp;gt;|x|&amp;lt;/math&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
{{#widget:FloatErrorGraph|id=tanhf_exp_v2&lt;br /&gt;
  |data=[ [&amp;quot;-2^127&amp;quot;,0,1.17549e-38],[&amp;quot;-2^126&amp;quot;,0,1.17549e-38],[&amp;quot;-2^125&amp;quot;,0,1.17549e-38],[&amp;quot;-2^124&amp;quot;,0,1.17549e-38],[&amp;quot;-2^123&amp;quot;,0,1.17549e-38],[&amp;quot;-2^122&amp;quot;,0,1.17549e-38],[&amp;quot;-2^121&amp;quot;,0,1.17549e-38],[&amp;quot;-2^120&amp;quot;,0,1.17549e-38],[&amp;quot;-2^119&amp;quot;,0,1.17549e-38],[&amp;quot;-2^118&amp;quot;,0,1.17549e-38],[&amp;quot;-2^117&amp;quot;,0,1.17549e-38],[&amp;quot;-2^116&amp;quot;,0,1.17549e-38],[&amp;quot;-2^115&amp;quot;,0,1.17549e-38],[&amp;quot;-2^114&amp;quot;,0,1.17549e-38],[&amp;quot;-2^113&amp;quot;,0,1.17549e-38],[&amp;quot;-2^112&amp;quot;,0,1.17549e-38],[&amp;quot;-2^111&amp;quot;,0,1.17549e-38],[&amp;quot;-2^110&amp;quot;,0,1.17549e-38],[&amp;quot;-2^109&amp;quot;,0,1.17549e-38],[&amp;quot;-2^108&amp;quot;,0,1.17549e-38],[&amp;quot;-2^107&amp;quot;,0,1.17549e-38],[&amp;quot;-2^106&amp;quot;,0,1.17549e-38],[&amp;quot;-2^105&amp;quot;,0,1.17549e-38],[&amp;quot;-2^104&amp;quot;,0,1.17549e-38],[&amp;quot;-2^103&amp;quot;,0,1.17549e-38],[&amp;quot;-2^102&amp;quot;,0,1.17549e-38],[&amp;quot;-2^101&amp;quot;,0,1.17549e-38],[&amp;quot;-2^100&amp;quot;,0,1.17549e-38],[&amp;quot;-2^99&amp;quot;,0,1.17549e-38],[&amp;quot;-2^98&amp;quot;,0,1.17549e-38],[&amp;quot;-2^97&amp;quot;,0,1.17549e-38],[&amp;quot;-2^96&amp;quot;,0,1.17549e-38],[&amp;quot;-2^95&amp;quot;,0,1.17549e-38],[&amp;quot;-2^94&amp;quot;,0,1.17549e-38],[&amp;quot;-2^93&amp;quot;,0,1.17549e-38],[&amp;quot;-2^92&amp;quot;,0,1.17549e-38],[&amp;quot;-2^91&amp;quot;,0,1.17549e-38],[&amp;quot;-2^90&amp;quot;,0,1.17549e-38],[&amp;quot;-2^89&amp;quot;,0,1.17549e-38],[&amp;quot;-2^88&amp;quot;,0,1.17549e-38],[&amp;quot;-2^87&amp;quot;,0,1.17549e-38],[&amp;quot;-2^86&amp;quot;,0,1.17549e-38],[&amp;quot;-2^85&amp;quot;,0,1.17549e-38],[&amp;quot;-2^84&amp;quot;,0,1.17549e-38],[&amp;quot;-2^83&amp;quot;,0,1.17549e-38],[&amp;quot;-2^82&amp;quot;,0,1.17549e-38],[&amp;quot;-2^81&amp;quot;,0,1.17549e-38],[&amp;quot;-2^80&amp;quot;,0,1.17549e-38],[&amp;quot;-2^79&amp;quot;,0,1.17549e-38],[&amp;quot;-2^78&amp;quot;,0,1.17549e-38],[&amp;quot;-2^77&amp;quot;,0,1.17549e-38],[&amp;quot;-2^76&amp;quot;,0,1.17549e-38],[&amp;quot;-2^75&amp;quot;,0,1.17549e-38],[&amp;quot;-2^74&amp;quot;,0,1.17549e-38],[&amp;quot;-2^73&amp;quot;,0,1.17549e-38],[&amp;quot;-2^72&amp;quot;,0,1.17549e-38],[&amp;quot;-2^71&amp;quot;,0,1.17549e-38],[&amp;quot;-2^70&amp;quot;,0,1.17549e-38],[&amp;quot;-2^69&amp;quot;,0,1.17549e-38],[&amp;quot;-2^68&amp;quot;,0,1.17549e-38],[&amp;quot;-2^67&amp;quot;,0,1.17549e-38],[&amp;quot;-2^66&amp;quot;,0,1.17549e-38],[&amp;quot;-2^65&amp;quot;,0,1.17549e-38],[&amp;quot;-2^64&amp;quot;,0,1.17549e-38],[&amp;quot;-2^63&amp;quot;,0,1.17549e-38],[&amp;quot;-2^62&amp;quot;,0,1.17549e-38],[&amp;quot;-2^61&amp;quot;,0,1.17549e-38],[&amp;quot;-2^60&amp;quot;,0,1.17549e-38],[&amp;quot;-2^59&amp;quot;,0,1.17549e-38],[&amp;quot;-2^58&amp;quot;,0,1.17549e-38],[&amp;quot;-2^57&amp;quot;,0,1.17549e-38],[&amp;quot;-2^56&amp;quot;,0,1.17549e-38],[&amp;quot;-2^55&amp;quot;,0,1.17549e-38],[&amp;quot;-2^54&amp;quot;,0,1.17549e-38],[&amp;quot;-2^53&amp;quot;,0,1.17549e-38],[&amp;quot;-2^52&amp;quot;,0,1.17549e-38],[&amp;quot;-2^51&amp;quot;,0,1.17549e-38],[&amp;quot;-2^50&amp;quot;,0,1.17549e-38],[&amp;quot;-2^49&amp;quot;,0,1.17549e-38],[&amp;quot;-2^48&amp;quot;,0,1.17549e-38],[&amp;quot;-2^47&amp;quot;,0,1.17549e-38],[&amp;quot;-2^46&amp;quot;,0,1.17549e-38],[&amp;quot;-2^45&amp;quot;,0,1.17549e-38],[&amp;quot;-2^44&amp;quot;,0,1.17549e-38],[&amp;quot;-2^43&amp;quot;,0,1.17549e-38],[&amp;quot;-2^42&amp;quot;,0,1.17549e-38],[&amp;quot;-2^41&amp;quot;,0,1.17549e-38],[&amp;quot;-2^40&amp;quot;,0,1.17549e-38],[&amp;quot;-2^39&amp;quot;,0,1.17549e-38],[&amp;quot;-2^38&amp;quot;,0,1.17549e-38],[&amp;quot;-2^37&amp;quot;,0,1.17549e-38],[&amp;quot;-2^36&amp;quot;,0,1.17549e-38],[&amp;quot;-2^35&amp;quot;,0,1.17549e-38],[&amp;quot;-2^34&amp;quot;,0,1.17549e-38],[&amp;quot;-2^33&amp;quot;,0,1.17549e-38],[&amp;quot;-2^32&amp;quot;,0,1.17549e-38],[&amp;quot;-2^31&amp;quot;,0,1.17549e-38],[&amp;quot;-2^30&amp;quot;,0,1.17549e-38],[&amp;quot;-2^29&amp;quot;,0,1.17549e-38],[&amp;quot;-2^28&amp;quot;,0,1.17549e-38],[&amp;quot;-2^27&amp;quot;,0,1.17549e-38],[&amp;quot;-2^26&amp;quot;,0,1.17549e-38],[&amp;quot;-2^25&amp;quot;,0,1.17549e-38],[&amp;quot;-2^24&amp;quot;,0,1.17549e-38],[&amp;quot;-2^23&amp;quot;,0,1.17549e-38],[&amp;quot;-2^22&amp;quot;,0,1.17549e-38],[&amp;quot;-2^21&amp;quot;,0,1.17549e-38],[&amp;quot;-2^20&amp;quot;,0,1.17549e-38],[&amp;quot;-2^19&amp;quot;,0,1.17549e-38],[&amp;quot;-2^18&amp;quot;,0,1.17549e-38],[&amp;quot;-2^17&amp;quot;,0,1.17549e-38],[&amp;quot;-2^16&amp;quot;,0,1.17549e-38],[&amp;quot;-2^15&amp;quot;,0,1.17549e-38],[&amp;quot;-2^14&amp;quot;,0,1.17549e-38],[&amp;quot;-2^13&amp;quot;,0,1.17549e-38],[&amp;quot;-2^12&amp;quot;,0,1.17549e-38],[&amp;quot;-2^11&amp;quot;,0,1.17549e-38],[&amp;quot;-2^10&amp;quot;,0,1.17549e-38],[&amp;quot;-2^9&amp;quot;,0,1.17549e-38],[&amp;quot;-2^8&amp;quot;,0,1.17549e-38],[&amp;quot;-2^7&amp;quot;,0,1.17549e-38],[&amp;quot;-2^6&amp;quot;,0,1.17549e-38],[&amp;quot;-2^5&amp;quot;,0,1.17549e-38],[&amp;quot;-2^4&amp;quot;,2.53131e-14,4.24683e-07],[&amp;quot;-2^3&amp;quot;,5.95888e-08,0.999734],[&amp;quot;-2^2&amp;quot;,8.86696e-08,1.48763],[&amp;quot;-2^1&amp;quot;,8.91609e-08,1.49587],[&amp;quot;-2^0&amp;quot;,8.73743e-08,1.4659],[&amp;quot;-2^-1&amp;quot;,8.65382e-08,2.13222],[&amp;quot;-2^-2&amp;quot;,6.18481e-08,2.61428],[&amp;quot;-2^-3&amp;quot;,3.88071e-08,3.08594],[&amp;quot;-2^-4&amp;quot;,2.6234e-08,4.23594],[&amp;quot;-2^-5&amp;quot;,2.02799e-08,8.19726],[&amp;quot;-2^-6&amp;quot;,1.75183e-08,12.4495],[&amp;quot;-2^-7&amp;quot;,1.61937e-08,17.3878],[&amp;quot;-2^-8&amp;quot;,1.5566e-08,33.4278],[&amp;quot;-2^-9&amp;quot;,1.51688e-08,65.1495],[&amp;quot;-2^-10&amp;quot;,1.50281e-08,129.09],[&amp;quot;-2^-11&amp;quot;,1.49613e-08,257.033],[&amp;quot;-2^-12&amp;quot;,1.49296e-08,512.977],[&amp;quot;-2^-13&amp;quot;,1.49174e-08,1025.12],[&amp;quot;-2^-14&amp;quot;,1.4909e-08,2049.08],[&amp;quot;-2^-15&amp;quot;,1.49049e-08,4097.02],[&amp;quot;-2^-16&amp;quot;,1.4903e-08,8193],[&amp;quot;-2^-17&amp;quot;,1.49021e-08,16385],[&amp;quot;-2^-18&amp;quot;,1.48862e-08,32735],[&amp;quot;-2^-19&amp;quot;,1.48973e-08,65519],[&amp;quot;-2^-20&amp;quot;,1.49001e-08,131063],[&amp;quot;-2^-21&amp;quot;,1.49009e-08,262139],[&amp;quot;-2^-22&amp;quot;,1.49011e-08,524285],[&amp;quot;-2^-23&amp;quot;,1.49011e-08,1.04857e+06],[&amp;quot;-2^-24&amp;quot;,1.49011e-08,2.09715e+06],[&amp;quot;-2^-25&amp;quot;,1.49012e-08,4.1943e+06],[&amp;quot;-2^-26&amp;quot;,1.49007e-08,8.38835e+06],[&amp;quot;-2^-27&amp;quot;,1.49012e-08,8.47249e+08],[&amp;quot;-2^-28&amp;quot;,7.45058e-09,8.38861e+08],[&amp;quot;-2^-29&amp;quot;,3.72529e-09,8.30472e+08],[&amp;quot;-2^-30&amp;quot;,1.86265e-09,8.22084e+08],[&amp;quot;-2^-31&amp;quot;,9.31323e-10,8.13695e+08],[&amp;quot;-2^-32&amp;quot;,4.65661e-10,8.05306e+08],[&amp;quot;-2^-33&amp;quot;,2.32831e-10,7.96918e+08],[&amp;quot;-2^-34&amp;quot;,1.16415e-10,7.88529e+08],[&amp;quot;-2^-35&amp;quot;,5.82077e-11,7.80141e+08],[&amp;quot;-2^-36&amp;quot;,2.91038e-11,7.71752e+08],[&amp;quot;-2^-37&amp;quot;,1.45519e-11,7.63363e+08],[&amp;quot;-2^-38&amp;quot;,7.27596e-12,7.54975e+08],[&amp;quot;-2^-39&amp;quot;,3.63798e-12,7.46586e+08],[&amp;quot;-2^-40&amp;quot;,1.81899e-12,7.38198e+08],[&amp;quot;-2^-41&amp;quot;,9.09495e-13,7.29809e+08],[&amp;quot;-2^-42&amp;quot;,4.54747e-13,7.2142e+08],[&amp;quot;-2^-43&amp;quot;,2.27374e-13,7.13032e+08],[&amp;quot;-2^-44&amp;quot;,1.13687e-13,7.04643e+08],[&amp;quot;-2^-45&amp;quot;,5.68434e-14,6.96254e+08],[&amp;quot;-2^-46&amp;quot;,2.84217e-14,6.87866e+08],[&amp;quot;-2^-47&amp;quot;,1.42109e-14,6.79477e+08],[&amp;quot;-2^-48&amp;quot;,7.10543e-15,6.71089e+08],[&amp;quot;-2^-49&amp;quot;,3.55271e-15,6.627e+08],[&amp;quot;-2^-50&amp;quot;,1.77636e-15,6.54311e+08],[&amp;quot;-2^-51&amp;quot;,8.88178e-16,6.45923e+08],[&amp;quot;-2^-52&amp;quot;,4.44089e-16,6.37534e+08],[&amp;quot;-2^-53&amp;quot;,2.22045e-16,6.29146e+08],[&amp;quot;-2^-54&amp;quot;,1.11022e-16,6.20757e+08],[&amp;quot;-2^-55&amp;quot;,5.55111e-17,6.12368e+08],[&amp;quot;-2^-56&amp;quot;,2.77556e-17,6.0398e+08],[&amp;quot;-2^-57&amp;quot;,1.38778e-17,5.95591e+08],[&amp;quot;-2^-58&amp;quot;,6.93889e-18,5.87203e+08],[&amp;quot;-2^-59&amp;quot;,3.46945e-18,5.78814e+08],[&amp;quot;-2^-60&amp;quot;,1.73472e-18,5.70425e+08],[&amp;quot;-2^-61&amp;quot;,8.67362e-19,5.62037e+08],[&amp;quot;-2^-62&amp;quot;,4.33681e-19,5.53648e+08],[&amp;quot;-2^-63&amp;quot;,2.1684e-19,5.4526e+08],[&amp;quot;-2^-64&amp;quot;,1.0842e-19,5.36871e+08],[&amp;quot;-2^-65&amp;quot;,5.42101e-20,5.28482e+08],[&amp;quot;-2^-66&amp;quot;,2.71051e-20,5.20094e+08],[&amp;quot;-2^-67&amp;quot;,1.35525e-20,5.11705e+08],[&amp;quot;-2^-68&amp;quot;,6.77626e-21,5.03316e+08],[&amp;quot;-2^-69&amp;quot;,3.38813e-21,4.94928e+08],[&amp;quot;-2^-70&amp;quot;,1.69407e-21,4.86539e+08],[&amp;quot;-2^-71&amp;quot;,8.47033e-22,4.78151e+08],[&amp;quot;-2^-72&amp;quot;,4.23516e-22,4.69762e+08],[&amp;quot;-2^-73&amp;quot;,2.11758e-22,4.61373e+08],[&amp;quot;-2^-74&amp;quot;,1.05879e-22,4.52985e+08],[&amp;quot;-2^-75&amp;quot;,5.29396e-23,4.44596e+08],[&amp;quot;-2^-76&amp;quot;,2.64698e-23,4.36208e+08],[&amp;quot;-2^-77&amp;quot;,1.32349e-23,4.27819e+08],[&amp;quot;-2^-78&amp;quot;,6.61744e-24,4.1943e+08],[&amp;quot;-2^-79&amp;quot;,3.30872e-24,4.11042e+08],[&amp;quot;-2^-80&amp;quot;,1.65436e-24,4.02653e+08],[&amp;quot;-2^-81&amp;quot;,8.27181e-25,3.94265e+08],[&amp;quot;-2^-82&amp;quot;,4.1359e-25,3.85876e+08],[&amp;quot;-2^-83&amp;quot;,2.06795e-25,3.77487e+08],[&amp;quot;-2^-84&amp;quot;,1.03398e-25,3.69099e+08],[&amp;quot;-2^-85&amp;quot;,5.16988e-26,3.6071e+08],[&amp;quot;-2^-86&amp;quot;,2.58494e-26,3.52322e+08],[&amp;quot;-2^-87&amp;quot;,1.29247e-26,3.43933e+08],[&amp;quot;-2^-88&amp;quot;,6.46235e-27,3.35544e+08],[&amp;quot;-2^-89&amp;quot;,3.23117e-27,3.27156e+08],[&amp;quot;-2^-90&amp;quot;,1.61559e-27,3.18767e+08],[&amp;quot;-2^-91&amp;quot;,8.07794e-28,3.10378e+08],[&amp;quot;-2^-92&amp;quot;,4.03897e-28,3.0199e+08],[&amp;quot;-2^-93&amp;quot;,2.01948e-28,2.93601e+08],[&amp;quot;-2^-94&amp;quot;,1.00974e-28,2.85213e+08],[&amp;quot;-2^-95&amp;quot;,5.04871e-29,2.76824e+08],[&amp;quot;-2^-96&amp;quot;,2.52435e-29,2.68435e+08],[&amp;quot;-2^-97&amp;quot;,1.26218e-29,2.60047e+08],[&amp;quot;-2^-98&amp;quot;,6.31089e-30,2.51658e+08],[&amp;quot;-2^-99&amp;quot;,3.15544e-30,2.4327e+08],[&amp;quot;-2^-100&amp;quot;,1.57772e-30,2.34881e+08],[&amp;quot;-2^-101&amp;quot;,7.88861e-31,2.26492e+08],[&amp;quot;-2^-102&amp;quot;,3.9443e-31,2.18104e+08],[&amp;quot;-2^-103&amp;quot;,1.97215e-31,2.09715e+08],[&amp;quot;-2^-104&amp;quot;,9.86076e-32,2.01327e+08],[&amp;quot;-2^-105&amp;quot;,4.93038e-32,1.92938e+08],[&amp;quot;-2^-106&amp;quot;,2.46519e-32,1.84549e+08],[&amp;quot;-2^-107&amp;quot;,1.2326e-32,1.76161e+08],[&amp;quot;-2^-108&amp;quot;,6.16298e-33,1.67772e+08],[&amp;quot;-2^-109&amp;quot;,3.08149e-33,1.59384e+08],[&amp;quot;-2^-110&amp;quot;,1.54074e-33,1.50995e+08],[&amp;quot;-2^-111&amp;quot;,7.70372e-34,1.42606e+08],[&amp;quot;-2^-112&amp;quot;,3.85186e-34,1.34218e+08],[&amp;quot;-2^-113&amp;quot;,1.92593e-34,1.25829e+08],[&amp;quot;-2^-114&amp;quot;,9.62965e-35,1.17441e+08],[&amp;quot;-2^-115&amp;quot;,4.81482e-35,1.09052e+08],[&amp;quot;-2^-116&amp;quot;,2.40741e-35,1.00663e+08],[&amp;quot;-2^-117&amp;quot;,1.20371e-35,9.22747e+07],[&amp;quot;-2^-118&amp;quot;,6.01853e-36,8.38861e+07],[&amp;quot;-2^-119&amp;quot;,3.00927e-36,7.54975e+07],[&amp;quot;-2^-120&amp;quot;,1.50463e-36,6.71089e+07],[&amp;quot;-2^-121&amp;quot;,7.52316e-37,5.87203e+07],[&amp;quot;-2^-122&amp;quot;,3.76158e-37,5.03316e+07],[&amp;quot;-2^-123&amp;quot;,1.88079e-37,4.1943e+07],[&amp;quot;-2^-124&amp;quot;,9.40395e-38,3.35544e+07],[&amp;quot;-2^-125&amp;quot;,4.70198e-38,2.51658e+07],[&amp;quot;-2^-126&amp;quot;,2.35099e-38,1.67772e+07],[&amp;quot;-SN&amp;quot;,1.17546e-38,8.38835e+06],[&amp;quot;0&amp;quot;,0,1.17549e-38],[&amp;quot;+SN&amp;quot;,1.17546e-38,8.38835e+06],[&amp;quot;+2^-126&amp;quot;,2.35095e-38,1.6777e+07],[&amp;quot;+2^-125&amp;quot;,4.70191e-38,2.51656e+07],[&amp;quot;+2^-124&amp;quot;,9.40381e-38,3.35542e+07],[&amp;quot;+2^-123&amp;quot;,1.88076e-37,4.19428e+07],[&amp;quot;+2^-122&amp;quot;,3.76152e-37,5.03314e+07],[&amp;quot;+2^-121&amp;quot;,7.52305e-37,5.872e+07],[&amp;quot;+2^-120&amp;quot;,1.50461e-36,6.71086e+07],[&amp;quot;+2^-119&amp;quot;,3.00922e-36,7.54972e+07],[&amp;quot;+2^-118&amp;quot;,6.01844e-36,8.38858e+07],[&amp;quot;+2^-117&amp;quot;,1.20369e-35,9.22744e+07],[&amp;quot;+2^-116&amp;quot;,2.40738e-35,1.00663e+08],[&amp;quot;+2^-115&amp;quot;,4.81475e-35,1.09052e+08],[&amp;quot;+2^-114&amp;quot;,9.6295e-35,1.1744e+08],[&amp;quot;+2^-113&amp;quot;,1.9259e-34,1.25829e+08],[&amp;quot;+2^-112&amp;quot;,3.8518e-34,1.34217e+08],[&amp;quot;+2^-111&amp;quot;,7.7036e-34,1.42606e+08],[&amp;quot;+2^-110&amp;quot;,1.54072e-33,1.50995e+08],[&amp;quot;+2^-109&amp;quot;,3.08144e-33,1.59383e+08],[&amp;quot;+2^-108&amp;quot;,6.16288e-33,1.67772e+08],[&amp;quot;+2^-107&amp;quot;,1.23258e-32,1.76161e+08],[&amp;quot;+2^-106&amp;quot;,2.46515e-32,1.84549e+08],[&amp;quot;+2^-105&amp;quot;,4.93031e-32,1.92938e+08],[&amp;quot;+2^-104&amp;quot;,9.86061e-32,2.01326e+08],[&amp;quot;+2^-103&amp;quot;,1.97212e-31,2.09715e+08],[&amp;quot;+2^-102&amp;quot;,3.94424e-31,2.18104e+08],[&amp;quot;+2^-101&amp;quot;,7.88849e-31,2.26492e+08],[&amp;quot;+2^-100&amp;quot;,1.5777e-30,2.34881e+08],[&amp;quot;+2^-99&amp;quot;,3.1554e-30,2.43269e+08],[&amp;quot;+2^-98&amp;quot;,6.31079e-30,2.51658e+08],[&amp;quot;+2^-97&amp;quot;,1.26216e-29,2.60047e+08],[&amp;quot;+2^-96&amp;quot;,2.52432e-29,2.68435e+08],[&amp;quot;+2^-95&amp;quot;,5.04863e-29,2.76824e+08],[&amp;quot;+2^-94&amp;quot;,1.00973e-28,2.85212e+08],[&amp;quot;+2^-93&amp;quot;,2.01945e-28,2.93601e+08],[&amp;quot;+2^-92&amp;quot;,4.03891e-28,3.0199e+08],[&amp;quot;+2^-91&amp;quot;,8.07781e-28,3.10378e+08],[&amp;quot;+2^-90&amp;quot;,1.61556e-27,3.18767e+08],[&amp;quot;+2^-89&amp;quot;,3.23112e-27,3.27155e+08],[&amp;quot;+2^-88&amp;quot;,6.46225e-27,3.35544e+08],[&amp;quot;+2^-87&amp;quot;,1.29245e-26,3.43933e+08],[&amp;quot;+2^-86&amp;quot;,2.5849e-26,3.52321e+08],[&amp;quot;+2^-85&amp;quot;,5.1698e-26,3.6071e+08],[&amp;quot;+2^-84&amp;quot;,1.03396e-25,3.69098e+08],[&amp;quot;+2^-83&amp;quot;,2.06792e-25,3.77487e+08],[&amp;quot;+2^-82&amp;quot;,4.13584e-25,3.85876e+08],[&amp;quot;+2^-81&amp;quot;,8.27168e-25,3.94264e+08],[&amp;quot;+2^-80&amp;quot;,1.65434e-24,4.02653e+08],[&amp;quot;+2^-79&amp;quot;,3.30867e-24,4.11042e+08],[&amp;quot;+2^-78&amp;quot;,6.61734e-24,4.1943e+08],[&amp;quot;+2^-77&amp;quot;,1.32347e-23,4.27819e+08],[&amp;quot;+2^-76&amp;quot;,2.64694e-23,4.36207e+08],[&amp;quot;+2^-75&amp;quot;,5.29388e-23,4.44596e+08],[&amp;quot;+2^-74&amp;quot;,1.05878e-22,4.52985e+08],[&amp;quot;+2^-73&amp;quot;,2.11755e-22,4.61373e+08],[&amp;quot;+2^-72&amp;quot;,4.2351e-22,4.69762e+08],[&amp;quot;+2^-71&amp;quot;,8.4702e-22,4.7815e+08],[&amp;quot;+2^-70&amp;quot;,1.69404e-21,4.86539e+08],[&amp;quot;+2^-69&amp;quot;,3.38808e-21,4.94928e+08],[&amp;quot;+2^-68&amp;quot;,6.77616e-21,5.03316e+08],[&amp;quot;+2^-67&amp;quot;,1.35523e-20,5.11705e+08],[&amp;quot;+2^-66&amp;quot;,2.71046e-20,5.20093e+08],[&amp;quot;+2^-65&amp;quot;,5.42093e-20,5.28482e+08],[&amp;quot;+2^-64&amp;quot;,1.08419e-19,5.36871e+08],[&amp;quot;+2^-63&amp;quot;,2.16837e-19,5.45259e+08],[&amp;quot;+2^-62&amp;quot;,4.33674e-19,5.53648e+08],[&amp;quot;+2^-61&amp;quot;,8.67349e-19,5.62036e+08],[&amp;quot;+2^-60&amp;quot;,1.7347e-18,5.70425e+08],[&amp;quot;+2^-59&amp;quot;,3.46939e-18,5.78814e+08],[&amp;quot;+2^-58&amp;quot;,6.93879e-18,5.87202e+08],[&amp;quot;+2^-57&amp;quot;,1.38776e-17,5.95591e+08],[&amp;quot;+2^-56&amp;quot;,2.77552e-17,6.0398e+08],[&amp;quot;+2^-55&amp;quot;,5.55103e-17,6.12368e+08],[&amp;quot;+2^-54&amp;quot;,1.11021e-16,6.20757e+08],[&amp;quot;+2^-53&amp;quot;,2.22041e-16,6.29145e+08],[&amp;quot;+2^-52&amp;quot;,4.44082e-16,6.37534e+08],[&amp;quot;+2^-51&amp;quot;,8.88165e-16,6.45923e+08],[&amp;quot;+2^-50&amp;quot;,1.77633e-15,6.54311e+08],[&amp;quot;+2^-49&amp;quot;,3.55266e-15,6.627e+08],[&amp;quot;+2^-48&amp;quot;,7.10532e-15,6.71088e+08],[&amp;quot;+2^-47&amp;quot;,1.42106e-14,6.79477e+08],[&amp;quot;+2^-46&amp;quot;,2.84213e-14,6.87866e+08],[&amp;quot;+2^-45&amp;quot;,5.68426e-14,6.96254e+08],[&amp;quot;+2^-44&amp;quot;,1.13685e-13,7.04643e+08],[&amp;quot;+2^-43&amp;quot;,2.2737e-13,7.13031e+08],[&amp;quot;+2^-42&amp;quot;,4.5474e-13,7.2142e+08],[&amp;quot;+2^-41&amp;quot;,9.09481e-13,7.29809e+08],[&amp;quot;+2^-40&amp;quot;,1.81896e-12,7.38197e+08],[&amp;quot;+2^-39&amp;quot;,3.63792e-12,7.46586e+08],[&amp;quot;+2^-38&amp;quot;,7.27585e-12,7.54974e+08],[&amp;quot;+2^-37&amp;quot;,1.45517e-11,7.63363e+08],[&amp;quot;+2^-36&amp;quot;,2.91034e-11,7.71752e+08],[&amp;quot;+2^-35&amp;quot;,5.82068e-11,7.8014e+08],[&amp;quot;+2^-34&amp;quot;,1.16414e-10,7.88529e+08],[&amp;quot;+2^-33&amp;quot;,2.32827e-10,7.96918e+08],[&amp;quot;+2^-32&amp;quot;,4.65654e-10,8.05306e+08],[&amp;quot;+2^-31&amp;quot;,9.31308e-10,8.13695e+08],[&amp;quot;+2^-30&amp;quot;,1.86262e-09,8.22083e+08],[&amp;quot;+2^-29&amp;quot;,3.72523e-09,8.30472e+08],[&amp;quot;+2^-28&amp;quot;,7.45047e-09,8.38861e+08],[&amp;quot;+2^-27&amp;quot;,1.49009e-08,8.47249e+08],[&amp;quot;+2^-26&amp;quot;,2.98019e-08,8.55638e+08],[&amp;quot;+2^-25&amp;quot;,2.98023e-08,8.38861e+06],[&amp;quot;+2^-24&amp;quot;,2.98023e-08,4.1943e+06],[&amp;quot;+2^-23&amp;quot;,2.98023e-08,2.09715e+06],[&amp;quot;+2^-22&amp;quot;,2.98023e-08,1.04857e+06],[&amp;quot;+2^-21&amp;quot;,2.98021e-08,524284],[&amp;quot;+2^-20&amp;quot;,2.98014e-08,262136],[&amp;quot;+2^-19&amp;quot;,2.97987e-08,131056],[&amp;quot;+2^-18&amp;quot;,2.97878e-08,65504],[&amp;quot;+2^-17&amp;quot;,2.98014e-08,32767],[&amp;quot;+2^-16&amp;quot;,2.98005e-08,16384],[&amp;quot;+2^-15&amp;quot;,2.98023e-08,8192],[&amp;quot;+2^-14&amp;quot;,2.98025e-08,4351.99],[&amp;quot;+2^-13&amp;quot;,2.98044e-08,2815.96],[&amp;quot;+2^-12&amp;quot;,2.98091e-08,1024.23],[&amp;quot;+2^-11&amp;quot;,2.9812e-08,512.167],[&amp;quot;+2^-10&amp;quot;,2.98154e-08,256.113],[&amp;quot;+2^-9&amp;quot;,2.98591e-08,128.244],[&amp;quot;+2^-8&amp;quot;,3.00204e-08,85.3328],[&amp;quot;+2^-7&amp;quot;,3.01947e-08,43.675],[&amp;quot;+2^-6&amp;quot;,3.05713e-08,30.45],[&amp;quot;+2^-5&amp;quot;,3.14319e-08,8.43743],[&amp;quot;+2^-4&amp;quot;,3.31291e-08,7.77058],[&amp;quot;+2^-3&amp;quot;,3.58597e-08,3.99659],[&amp;quot;+2^-2&amp;quot;,4.14639e-08,2.29246],[&amp;quot;+2^-1&amp;quot;,8.02409e-08,1.34622],[&amp;quot;+2^0&amp;quot;,8.66201e-08,1.45324],[&amp;quot;+2^1&amp;quot;,8.8654e-08,1.48737],[&amp;quot;+2^2&amp;quot;,6.7976e-08,1.14045],[&amp;quot;+2^3&amp;quot;,1.33955e-07,2.2474],[&amp;quot;+2^4&amp;quot;,2.53131e-14,4.24683e-07],[&amp;quot;+2^5&amp;quot;,0,null],[&amp;quot;+2^6&amp;quot;,0,null],[&amp;quot;+2^7&amp;quot;,0,null],[&amp;quot;+2^8&amp;quot;,0,null],[&amp;quot;+2^9&amp;quot;,0,null],[&amp;quot;+2^10&amp;quot;,0,null],[&amp;quot;+2^11&amp;quot;,0,null],[&amp;quot;+2^12&amp;quot;,0,null],[&amp;quot;+2^13&amp;quot;,0,null],[&amp;quot;+2^14&amp;quot;,0,null],[&amp;quot;+2^15&amp;quot;,0,null],[&amp;quot;+2^16&amp;quot;,0,null],[&amp;quot;+2^17&amp;quot;,0,null],[&amp;quot;+2^18&amp;quot;,0,null],[&amp;quot;+2^19&amp;quot;,0,null],[&amp;quot;+2^20&amp;quot;,0,null],[&amp;quot;+2^21&amp;quot;,0,null],[&amp;quot;+2^22&amp;quot;,0,null],[&amp;quot;+2^23&amp;quot;,0,null],[&amp;quot;+2^24&amp;quot;,0,null],[&amp;quot;+2^25&amp;quot;,0,null],[&amp;quot;+2^26&amp;quot;,0,null],[&amp;quot;+2^27&amp;quot;,0,null],[&amp;quot;+2^28&amp;quot;,0,null],[&amp;quot;+2^29&amp;quot;,0,null],[&amp;quot;+2^30&amp;quot;,0,null],[&amp;quot;+2^31&amp;quot;,0,null],[&amp;quot;+2^32&amp;quot;,0,null],[&amp;quot;+2^33&amp;quot;,0,null],[&amp;quot;+2^34&amp;quot;,0,null],[&amp;quot;+2^35&amp;quot;,0,null],[&amp;quot;+2^36&amp;quot;,0,null],[&amp;quot;+2^37&amp;quot;,0,null],[&amp;quot;+2^38&amp;quot;,0,null],[&amp;quot;+2^39&amp;quot;,0,null],[&amp;quot;+2^40&amp;quot;,0,null],[&amp;quot;+2^41&amp;quot;,0,null],[&amp;quot;+2^42&amp;quot;,0,null],[&amp;quot;+2^43&amp;quot;,0,null],[&amp;quot;+2^44&amp;quot;,0,null],[&amp;quot;+2^45&amp;quot;,0,null],[&amp;quot;+2^46&amp;quot;,0,null],[&amp;quot;+2^47&amp;quot;,0,null],[&amp;quot;+2^48&amp;quot;,0,null],[&amp;quot;+2^49&amp;quot;,0,null],[&amp;quot;+2^50&amp;quot;,0,null],[&amp;quot;+2^51&amp;quot;,0,null],[&amp;quot;+2^52&amp;quot;,0,null],[&amp;quot;+2^53&amp;quot;,0,null],[&amp;quot;+2^54&amp;quot;,0,null],[&amp;quot;+2^55&amp;quot;,0,null],[&amp;quot;+2^56&amp;quot;,0,null],[&amp;quot;+2^57&amp;quot;,0,null],[&amp;quot;+2^58&amp;quot;,0,null],[&amp;quot;+2^59&amp;quot;,0,null],[&amp;quot;+2^60&amp;quot;,0,null],[&amp;quot;+2^61&amp;quot;,0,null],[&amp;quot;+2^62&amp;quot;,0,null],[&amp;quot;+2^63&amp;quot;,0,null],[&amp;quot;+2^64&amp;quot;,0,null],[&amp;quot;+2^65&amp;quot;,0,null],[&amp;quot;+2^66&amp;quot;,0,null],[&amp;quot;+2^67&amp;quot;,0,null],[&amp;quot;+2^68&amp;quot;,0,null],[&amp;quot;+2^69&amp;quot;,0,null],[&amp;quot;+2^70&amp;quot;,0,null],[&amp;quot;+2^71&amp;quot;,0,null],[&amp;quot;+2^72&amp;quot;,0,null],[&amp;quot;+2^73&amp;quot;,0,null],[&amp;quot;+2^74&amp;quot;,0,null],[&amp;quot;+2^75&amp;quot;,0,null],[&amp;quot;+2^76&amp;quot;,0,null],[&amp;quot;+2^77&amp;quot;,0,null],[&amp;quot;+2^78&amp;quot;,0,null],[&amp;quot;+2^79&amp;quot;,0,null],[&amp;quot;+2^80&amp;quot;,0,null],[&amp;quot;+2^81&amp;quot;,0,null],[&amp;quot;+2^82&amp;quot;,0,null],[&amp;quot;+2^83&amp;quot;,0,null],[&amp;quot;+2^84&amp;quot;,0,null],[&amp;quot;+2^85&amp;quot;,0,null],[&amp;quot;+2^86&amp;quot;,0,null],[&amp;quot;+2^87&amp;quot;,0,null],[&amp;quot;+2^88&amp;quot;,0,null],[&amp;quot;+2^89&amp;quot;,0,null],[&amp;quot;+2^90&amp;quot;,0,null],[&amp;quot;+2^91&amp;quot;,0,null],[&amp;quot;+2^92&amp;quot;,0,null],[&amp;quot;+2^93&amp;quot;,0,null],[&amp;quot;+2^94&amp;quot;,0,null],[&amp;quot;+2^95&amp;quot;,0,null],[&amp;quot;+2^96&amp;quot;,0,null],[&amp;quot;+2^97&amp;quot;,0,null],[&amp;quot;+2^98&amp;quot;,0,null],[&amp;quot;+2^99&amp;quot;,0,null],[&amp;quot;+2^100&amp;quot;,0,null],[&amp;quot;+2^101&amp;quot;,0,null],[&amp;quot;+2^102&amp;quot;,0,null],[&amp;quot;+2^103&amp;quot;,0,null],[&amp;quot;+2^104&amp;quot;,0,null],[&amp;quot;+2^105&amp;quot;,0,null],[&amp;quot;+2^106&amp;quot;,0,null],[&amp;quot;+2^107&amp;quot;,0,null],[&amp;quot;+2^108&amp;quot;,0,null],[&amp;quot;+2^109&amp;quot;,0,null],[&amp;quot;+2^110&amp;quot;,0,null],[&amp;quot;+2^111&amp;quot;,0,null],[&amp;quot;+2^112&amp;quot;,0,null],[&amp;quot;+2^113&amp;quot;,0,null],[&amp;quot;+2^114&amp;quot;,0,null],[&amp;quot;+2^115&amp;quot;,0,null],[&amp;quot;+2^116&amp;quot;,0,null],[&amp;quot;+2^117&amp;quot;,0,null],[&amp;quot;+2^118&amp;quot;,0,null],[&amp;quot;+2^119&amp;quot;,0,null],[&amp;quot;+2^120&amp;quot;,0,null],[&amp;quot;+2^121&amp;quot;,0,null],[&amp;quot;+2^122&amp;quot;,0,null],[&amp;quot;+2^123&amp;quot;,0,null],[&amp;quot;+2^124&amp;quot;,0,null],[&amp;quot;+2^125&amp;quot;,0,null],[&amp;quot;+2^126&amp;quot;,0,null],[&amp;quot;+2^127&amp;quot;,0,null] ]&lt;br /&gt;
 }}&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Key observation:&#039;&#039;&#039;  &lt;br /&gt;
For &amp;lt;math&amp;gt;|x| \ll 1&amp;lt;/math&amp;gt;, the ULP error grows rapidly and reaches extremely large values due to loss of significance. &lt;br /&gt;
For sufficiently large &amp;lt;math&amp;gt;x&amp;lt;/math&amp;gt;, both numerator and denominator overflow, leading to NaN results.&lt;br /&gt;
&lt;br /&gt;
=== expm1 ===&lt;br /&gt;
&lt;br /&gt;
This graph visualizes the ULP error of the &amp;lt;code&amp;gt;expm1&amp;lt;/code&amp;gt;-based formulation.&lt;br /&gt;
Using &amp;lt;code&amp;gt;expm1&amp;lt;/code&amp;gt; significantly improves numerical accuracy for small input values by avoiding catastrophic cancellation in the expression &amp;lt;math&amp;gt;e^{2x} - 1&amp;lt;/math&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
{{#widget:FloatErrorGraph|id=tanhf_expm1&lt;br /&gt;
  |data=[ [&amp;quot;-2^127&amp;quot;,0,1.17549e-38],[&amp;quot;-2^126&amp;quot;,0,1.17549e-38],[&amp;quot;-2^125&amp;quot;,0,1.17549e-38],[&amp;quot;-2^124&amp;quot;,0,1.17549e-38],[&amp;quot;-2^123&amp;quot;,0,1.17549e-38],[&amp;quot;-2^122&amp;quot;,0,1.17549e-38],[&amp;quot;-2^121&amp;quot;,0,1.17549e-38],[&amp;quot;-2^120&amp;quot;,0,1.17549e-38],[&amp;quot;-2^119&amp;quot;,0,1.17549e-38],[&amp;quot;-2^118&amp;quot;,0,1.17549e-38],[&amp;quot;-2^117&amp;quot;,0,1.17549e-38],[&amp;quot;-2^116&amp;quot;,0,1.17549e-38],[&amp;quot;-2^115&amp;quot;,0,1.17549e-38],[&amp;quot;-2^114&amp;quot;,0,1.17549e-38],[&amp;quot;-2^113&amp;quot;,0,1.17549e-38],[&amp;quot;-2^112&amp;quot;,0,1.17549e-38],[&amp;quot;-2^111&amp;quot;,0,1.17549e-38],[&amp;quot;-2^110&amp;quot;,0,1.17549e-38],[&amp;quot;-2^109&amp;quot;,0,1.17549e-38],[&amp;quot;-2^108&amp;quot;,0,1.17549e-38],[&amp;quot;-2^107&amp;quot;,0,1.17549e-38],[&amp;quot;-2^106&amp;quot;,0,1.17549e-38],[&amp;quot;-2^105&amp;quot;,0,1.17549e-38],[&amp;quot;-2^104&amp;quot;,0,1.17549e-38],[&amp;quot;-2^103&amp;quot;,0,1.17549e-38],[&amp;quot;-2^102&amp;quot;,0,1.17549e-38],[&amp;quot;-2^101&amp;quot;,0,1.17549e-38],[&amp;quot;-2^100&amp;quot;,0,1.17549e-38],[&amp;quot;-2^99&amp;quot;,0,1.17549e-38],[&amp;quot;-2^98&amp;quot;,0,1.17549e-38],[&amp;quot;-2^97&amp;quot;,0,1.17549e-38],[&amp;quot;-2^96&amp;quot;,0,1.17549e-38],[&amp;quot;-2^95&amp;quot;,0,1.17549e-38],[&amp;quot;-2^94&amp;quot;,0,1.17549e-38],[&amp;quot;-2^93&amp;quot;,0,1.17549e-38],[&amp;quot;-2^92&amp;quot;,0,1.17549e-38],[&amp;quot;-2^91&amp;quot;,0,1.17549e-38],[&amp;quot;-2^90&amp;quot;,0,1.17549e-38],[&amp;quot;-2^89&amp;quot;,0,1.17549e-38],[&amp;quot;-2^88&amp;quot;,0,1.17549e-38],[&amp;quot;-2^87&amp;quot;,0,1.17549e-38],[&amp;quot;-2^86&amp;quot;,0,1.17549e-38],[&amp;quot;-2^85&amp;quot;,0,1.17549e-38],[&amp;quot;-2^84&amp;quot;,0,1.17549e-38],[&amp;quot;-2^83&amp;quot;,0,1.17549e-38],[&amp;quot;-2^82&amp;quot;,0,1.17549e-38],[&amp;quot;-2^81&amp;quot;,0,1.17549e-38],[&amp;quot;-2^80&amp;quot;,0,1.17549e-38],[&amp;quot;-2^79&amp;quot;,0,1.17549e-38],[&amp;quot;-2^78&amp;quot;,0,1.17549e-38],[&amp;quot;-2^77&amp;quot;,0,1.17549e-38],[&amp;quot;-2^76&amp;quot;,0,1.17549e-38],[&amp;quot;-2^75&amp;quot;,0,1.17549e-38],[&amp;quot;-2^74&amp;quot;,0,1.17549e-38],[&amp;quot;-2^73&amp;quot;,0,1.17549e-38],[&amp;quot;-2^72&amp;quot;,0,1.17549e-38],[&amp;quot;-2^71&amp;quot;,0,1.17549e-38],[&amp;quot;-2^70&amp;quot;,0,1.17549e-38],[&amp;quot;-2^69&amp;quot;,0,1.17549e-38],[&amp;quot;-2^68&amp;quot;,0,1.17549e-38],[&amp;quot;-2^67&amp;quot;,0,1.17549e-38],[&amp;quot;-2^66&amp;quot;,0,1.17549e-38],[&amp;quot;-2^65&amp;quot;,0,1.17549e-38],[&amp;quot;-2^64&amp;quot;,0,1.17549e-38],[&amp;quot;-2^63&amp;quot;,0,1.17549e-38],[&amp;quot;-2^62&amp;quot;,0,1.17549e-38],[&amp;quot;-2^61&amp;quot;,0,1.17549e-38],[&amp;quot;-2^60&amp;quot;,0,1.17549e-38],[&amp;quot;-2^59&amp;quot;,0,1.17549e-38],[&amp;quot;-2^58&amp;quot;,0,1.17549e-38],[&amp;quot;-2^57&amp;quot;,0,1.17549e-38],[&amp;quot;-2^56&amp;quot;,0,1.17549e-38],[&amp;quot;-2^55&amp;quot;,0,1.17549e-38],[&amp;quot;-2^54&amp;quot;,0,1.17549e-38],[&amp;quot;-2^53&amp;quot;,0,1.17549e-38],[&amp;quot;-2^52&amp;quot;,0,1.17549e-38],[&amp;quot;-2^51&amp;quot;,0,1.17549e-38],[&amp;quot;-2^50&amp;quot;,0,1.17549e-38],[&amp;quot;-2^49&amp;quot;,0,1.17549e-38],[&amp;quot;-2^48&amp;quot;,0,1.17549e-38],[&amp;quot;-2^47&amp;quot;,0,1.17549e-38],[&amp;quot;-2^46&amp;quot;,0,1.17549e-38],[&amp;quot;-2^45&amp;quot;,0,1.17549e-38],[&amp;quot;-2^44&amp;quot;,0,1.17549e-38],[&amp;quot;-2^43&amp;quot;,0,1.17549e-38],[&amp;quot;-2^42&amp;quot;,0,1.17549e-38],[&amp;quot;-2^41&amp;quot;,0,1.17549e-38],[&amp;quot;-2^40&amp;quot;,0,1.17549e-38],[&amp;quot;-2^39&amp;quot;,0,1.17549e-38],[&amp;quot;-2^38&amp;quot;,0,1.17549e-38],[&amp;quot;-2^37&amp;quot;,0,1.17549e-38],[&amp;quot;-2^36&amp;quot;,0,1.17549e-38],[&amp;quot;-2^35&amp;quot;,0,1.17549e-38],[&amp;quot;-2^34&amp;quot;,0,1.17549e-38],[&amp;quot;-2^33&amp;quot;,0,1.17549e-38],[&amp;quot;-2^32&amp;quot;,0,1.17549e-38],[&amp;quot;-2^31&amp;quot;,0,1.17549e-38],[&amp;quot;-2^30&amp;quot;,0,1.17549e-38],[&amp;quot;-2^29&amp;quot;,0,1.17549e-38],[&amp;quot;-2^28&amp;quot;,0,1.17549e-38],[&amp;quot;-2^27&amp;quot;,0,1.17549e-38],[&amp;quot;-2^26&amp;quot;,0,1.17549e-38],[&amp;quot;-2^25&amp;quot;,0,1.17549e-38],[&amp;quot;-2^24&amp;quot;,0,1.17549e-38],[&amp;quot;-2^23&amp;quot;,0,1.17549e-38],[&amp;quot;-2^22&amp;quot;,0,1.17549e-38],[&amp;quot;-2^21&amp;quot;,0,1.17549e-38],[&amp;quot;-2^20&amp;quot;,0,1.17549e-38],[&amp;quot;-2^19&amp;quot;,0,1.17549e-38],[&amp;quot;-2^18&amp;quot;,0,1.17549e-38],[&amp;quot;-2^17&amp;quot;,0,1.17549e-38],[&amp;quot;-2^16&amp;quot;,0,1.17549e-38],[&amp;quot;-2^15&amp;quot;,0,1.17549e-38],[&amp;quot;-2^14&amp;quot;,0,1.17549e-38],[&amp;quot;-2^13&amp;quot;,0,1.17549e-38],[&amp;quot;-2^12&amp;quot;,0,1.17549e-38],[&amp;quot;-2^11&amp;quot;,0,1.17549e-38],[&amp;quot;-2^10&amp;quot;,0,1.17549e-38],[&amp;quot;-2^9&amp;quot;,0,1.17549e-38],[&amp;quot;-2^8&amp;quot;,0,1.17549e-38],[&amp;quot;-2^7&amp;quot;,0,1.17549e-38],[&amp;quot;-2^6&amp;quot;,0,1.17549e-38],[&amp;quot;-2^5&amp;quot;,0,1.17549e-38],[&amp;quot;-2^4&amp;quot;,2.53131e-14,4.24683e-07],[&amp;quot;-2^3&amp;quot;,1.19138e-07,1.9988],[&amp;quot;-2^2&amp;quot;,1.46791e-07,2.46274],[&amp;quot;-2^1&amp;quot;,1.47723e-07,2.47839],[&amp;quot;-2^0&amp;quot;,1.40758e-07,2.36153],[&amp;quot;-2^-1&amp;quot;,1.13422e-07,2.23445],[&amp;quot;-2^-2&amp;quot;,6.38012e-08,2.14081],[&amp;quot;-2^-3&amp;quot;,2.69538e-08,1.80884],[&amp;quot;-2^-4&amp;quot;,1.21308e-08,1.62817],[&amp;quot;-2^-5&amp;quot;,5.59535e-09,1.50199],[&amp;quot;-2^-6&amp;quot;,2.69177e-09,1.44513],[&amp;quot;-2^-7&amp;quot;,1.31289e-09,1.4097],[&amp;quot;-2^-8&amp;quot;,6.49688e-10,1.3952],[&amp;quot;-2^-9&amp;quot;,3.32043e-10,1.42611],[&amp;quot;-2^-10&amp;quot;,1.66275e-10,1.4283],[&amp;quot;-2^-11&amp;quot;,8.08595e-11,1.38916],[&amp;quot;-2^-12&amp;quot;,3.8939e-11,1.33794],[&amp;quot;-2^-13&amp;quot;,1.9401e-11,1.33323],[&amp;quot;-2^-14&amp;quot;,7.88212e-12,1.08331],[&amp;quot;-2^-15&amp;quot;,3.71372e-12,1.02082],[&amp;quot;-2^-16&amp;quot;,1.82845e-12,1.0052],[&amp;quot;-2^-17&amp;quot;,9.10676e-13,1.0013],[&amp;quot;-2^-18&amp;quot;,4.54895e-13,1.00032],[&amp;quot;-2^-19&amp;quot;,2.27392e-13,1.00008],[&amp;quot;-2^-20&amp;quot;,1.13689e-13,1.00002],[&amp;quot;-2^-21&amp;quot;,5.68437e-14,1],[&amp;quot;-2^-22&amp;quot;,2.84217e-14,1],[&amp;quot;-2^-23&amp;quot;,1.42109e-14,1],[&amp;quot;-2^-24&amp;quot;,7.10543e-15,1],[&amp;quot;-2^-25&amp;quot;,3.55271e-15,1],[&amp;quot;-2^-26&amp;quot;,9.92617e-24,5.58794e-09],[&amp;quot;-2^-27&amp;quot;,1.65436e-24,1.86265e-09],[&amp;quot;-2^-28&amp;quot;,8.27181e-25,1.86265e-09],[&amp;quot;-2^-29&amp;quot;,4.1359e-25,1.86265e-09],[&amp;quot;-2^-30&amp;quot;,2.06795e-25,1.86265e-09],[&amp;quot;-2^-31&amp;quot;,1.03398e-25,1.86265e-09],[&amp;quot;-2^-32&amp;quot;,5.16988e-26,1.86265e-09],[&amp;quot;-2^-33&amp;quot;,2.58494e-26,1.86265e-09],[&amp;quot;-2^-34&amp;quot;,1.29247e-26,1.86265e-09],[&amp;quot;-2^-35&amp;quot;,6.46235e-27,1.86265e-09],[&amp;quot;-2^-36&amp;quot;,3.23117e-27,1.86265e-09],[&amp;quot;-2^-37&amp;quot;,1.61559e-27,1.86265e-09],[&amp;quot;-2^-38&amp;quot;,8.07794e-28,1.86265e-09],[&amp;quot;-2^-39&amp;quot;,4.03897e-28,1.86265e-09],[&amp;quot;-2^-40&amp;quot;,2.01948e-28,1.86265e-09],[&amp;quot;-2^-41&amp;quot;,1.00974e-28,1.86265e-09],[&amp;quot;-2^-42&amp;quot;,5.04871e-29,1.86265e-09],[&amp;quot;-2^-43&amp;quot;,2.52435e-29,1.86265e-09],[&amp;quot;-2^-44&amp;quot;,1.26218e-29,1.86265e-09],[&amp;quot;-2^-45&amp;quot;,6.31089e-30,1.86265e-09],[&amp;quot;-2^-46&amp;quot;,3.15544e-30,1.86265e-09],[&amp;quot;-2^-47&amp;quot;,1.57772e-30,1.86265e-09],[&amp;quot;-2^-48&amp;quot;,7.88861e-31,1.86265e-09],[&amp;quot;-2^-49&amp;quot;,3.9443e-31,1.86265e-09],[&amp;quot;-2^-50&amp;quot;,1.97215e-31,1.86265e-09],[&amp;quot;-2^-51&amp;quot;,9.86076e-32,1.86265e-09],[&amp;quot;-2^-52&amp;quot;,4.93038e-32,1.86265e-09],[&amp;quot;-2^-53&amp;quot;,2.46519e-32,1.86265e-09],[&amp;quot;-2^-54&amp;quot;,1.2326e-32,1.86265e-09],[&amp;quot;-2^-55&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-56&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-57&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-58&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-59&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-60&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-61&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-62&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-63&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-64&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-65&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-66&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-67&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-68&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-69&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-70&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-71&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-72&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-73&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-74&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-75&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-76&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-77&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-78&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-79&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-80&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-81&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-82&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-83&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-84&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-85&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-86&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-87&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-88&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-89&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-90&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-91&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-92&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-93&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-94&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-95&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-96&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-97&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-98&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-99&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-100&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-101&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-102&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-103&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-104&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-105&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-106&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-107&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-108&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-109&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-110&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-111&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-112&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-113&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-114&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-115&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-116&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-117&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-118&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-119&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-120&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-121&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-122&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-123&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-124&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-125&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-126&amp;quot;,0,1.17549e-38],[&amp;quot;-SN&amp;quot;,0,1.17549e-38],[&amp;quot;0&amp;quot;,0,1.17549e-38],[&amp;quot;+SN&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-126&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-125&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-124&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-123&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-122&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-121&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-120&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-119&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-118&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-117&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-116&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-115&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-114&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-113&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-112&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-111&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-110&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-109&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-108&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-107&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-106&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-105&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-104&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-103&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-102&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-101&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-100&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-99&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-98&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-97&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-96&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-95&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-94&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-93&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-92&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-91&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-90&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-89&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-88&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-87&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-86&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-85&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-84&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-83&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-82&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-81&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-80&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-79&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-78&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-77&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-76&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-75&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-74&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-73&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-72&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-71&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-70&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-69&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-68&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-67&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-66&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-65&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-64&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-63&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-62&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-61&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-60&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-59&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-58&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-57&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-56&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-55&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-54&amp;quot;,1.2326e-32,1.86265e-09],[&amp;quot;+2^-53&amp;quot;,2.46519e-32,1.86265e-09],[&amp;quot;+2^-52&amp;quot;,4.93038e-32,1.86265e-09],[&amp;quot;+2^-51&amp;quot;,9.86076e-32,1.86265e-09],[&amp;quot;+2^-50&amp;quot;,1.97215e-31,1.86265e-09],[&amp;quot;+2^-49&amp;quot;,3.9443e-31,1.86265e-09],[&amp;quot;+2^-48&amp;quot;,7.88861e-31,1.86265e-09],[&amp;quot;+2^-47&amp;quot;,1.57772e-30,1.86265e-09],[&amp;quot;+2^-46&amp;quot;,3.15544e-30,1.86265e-09],[&amp;quot;+2^-45&amp;quot;,6.31089e-30,1.86265e-09],[&amp;quot;+2^-44&amp;quot;,1.26218e-29,1.86265e-09],[&amp;quot;+2^-43&amp;quot;,2.52435e-29,1.86265e-09],[&amp;quot;+2^-42&amp;quot;,5.04871e-29,1.86265e-09],[&amp;quot;+2^-41&amp;quot;,1.00974e-28,1.86265e-09],[&amp;quot;+2^-40&amp;quot;,2.01948e-28,1.86265e-09],[&amp;quot;+2^-39&amp;quot;,4.03897e-28,1.86265e-09],[&amp;quot;+2^-38&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-37&amp;quot;,1.61559e-27,1.86265e-09],[&amp;quot;+2^-36&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-35&amp;quot;,6.46235e-27,1.86265e-09],[&amp;quot;+2^-34&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-33&amp;quot;,2.58494e-26,1.86265e-09],[&amp;quot;+2^-32&amp;quot;,5.16988e-26,1.86265e-09],[&amp;quot;+2^-31&amp;quot;,1.03398e-25,1.86265e-09],[&amp;quot;+2^-30&amp;quot;,2.06795e-25,1.86265e-09],[&amp;quot;+2^-29&amp;quot;,4.1359e-25,1.86265e-09],[&amp;quot;+2^-28&amp;quot;,8.27181e-25,1.86265e-09],[&amp;quot;+2^-27&amp;quot;,1.65436e-24,1.86265e-09],[&amp;quot;+2^-26&amp;quot;,9.92617e-24,5.58794e-09],[&amp;quot;+2^-25&amp;quot;,3.55271e-15,1],[&amp;quot;+2^-24&amp;quot;,7.10543e-15,1],[&amp;quot;+2^-23&amp;quot;,1.42109e-14,1],[&amp;quot;+2^-22&amp;quot;,2.84217e-14,1],[&amp;quot;+2^-21&amp;quot;,5.68437e-14,1],[&amp;quot;+2^-20&amp;quot;,1.13689e-13,1.00002],[&amp;quot;+2^-19&amp;quot;,2.27391e-13,1.00008],[&amp;quot;+2^-18&amp;quot;,4.54892e-13,1.00032],[&amp;quot;+2^-17&amp;quot;,9.10665e-13,1.00129],[&amp;quot;+2^-16&amp;quot;,1.82841e-12,1.00518],[&amp;quot;+2^-15&amp;quot;,3.71353e-12,1.02077],[&amp;quot;+2^-14&amp;quot;,1.39798e-11,1.92136],[&amp;quot;+2^-13&amp;quot;,2.63239e-11,1.80896],[&amp;quot;+2^-12&amp;quot;,5.37712e-11,1.84757],[&amp;quot;+2^-11&amp;quot;,1.09932e-10,1.88861],[&amp;quot;+2^-10&amp;quot;,2.24787e-10,1.93091],[&amp;quot;+2^-9&amp;quot;,4.3452e-10,1.86625],[&amp;quot;+2^-8&amp;quot;,1.01783e-09,2.18576],[&amp;quot;+2^-7&amp;quot;,2.10562e-09,2.26089],[&amp;quot;+2^-6&amp;quot;,4.03954e-09,2.16871],[&amp;quot;+2^-5&amp;quot;,8.15746e-09,2.18975],[&amp;quot;+2^-4&amp;quot;,1.5545e-08,2.08641],[&amp;quot;+2^-3&amp;quot;,2.62153e-08,2.05864],[&amp;quot;+2^-2&amp;quot;,4.09295e-08,1.73804],[&amp;quot;+2^-1&amp;quot;,8.02409e-08,1.34622],[&amp;quot;+2^0&amp;quot;,8.66201e-08,1.45324],[&amp;quot;+2^1&amp;quot;,8.8654e-08,1.48737],[&amp;quot;+2^2&amp;quot;,6.7976e-08,1.14045],[&amp;quot;+2^3&amp;quot;,7.44117e-08,1.24842],[&amp;quot;+2^4&amp;quot;,2.53131e-14,4.24683e-07],[&amp;quot;+2^5&amp;quot;,0,null],[&amp;quot;+2^6&amp;quot;,0,null],[&amp;quot;+2^7&amp;quot;,0,null],[&amp;quot;+2^8&amp;quot;,0,null],[&amp;quot;+2^9&amp;quot;,0,null],[&amp;quot;+2^10&amp;quot;,0,null],[&amp;quot;+2^11&amp;quot;,0,null],[&amp;quot;+2^12&amp;quot;,0,null],[&amp;quot;+2^13&amp;quot;,0,null],[&amp;quot;+2^14&amp;quot;,0,null],[&amp;quot;+2^15&amp;quot;,0,null],[&amp;quot;+2^16&amp;quot;,0,null],[&amp;quot;+2^17&amp;quot;,0,null],[&amp;quot;+2^18&amp;quot;,0,null],[&amp;quot;+2^19&amp;quot;,0,null],[&amp;quot;+2^20&amp;quot;,0,null],[&amp;quot;+2^21&amp;quot;,0,null],[&amp;quot;+2^22&amp;quot;,0,null],[&amp;quot;+2^23&amp;quot;,0,null],[&amp;quot;+2^24&amp;quot;,0,null],[&amp;quot;+2^25&amp;quot;,0,null],[&amp;quot;+2^26&amp;quot;,0,null],[&amp;quot;+2^27&amp;quot;,0,null],[&amp;quot;+2^28&amp;quot;,0,null],[&amp;quot;+2^29&amp;quot;,0,null],[&amp;quot;+2^30&amp;quot;,0,null],[&amp;quot;+2^31&amp;quot;,0,null],[&amp;quot;+2^32&amp;quot;,0,null],[&amp;quot;+2^33&amp;quot;,0,null],[&amp;quot;+2^34&amp;quot;,0,null],[&amp;quot;+2^35&amp;quot;,0,null],[&amp;quot;+2^36&amp;quot;,0,null],[&amp;quot;+2^37&amp;quot;,0,null],[&amp;quot;+2^38&amp;quot;,0,null],[&amp;quot;+2^39&amp;quot;,0,null],[&amp;quot;+2^40&amp;quot;,0,null],[&amp;quot;+2^41&amp;quot;,0,null],[&amp;quot;+2^42&amp;quot;,0,null],[&amp;quot;+2^43&amp;quot;,0,null],[&amp;quot;+2^44&amp;quot;,0,null],[&amp;quot;+2^45&amp;quot;,0,null],[&amp;quot;+2^46&amp;quot;,0,null],[&amp;quot;+2^47&amp;quot;,0,null],[&amp;quot;+2^48&amp;quot;,0,null],[&amp;quot;+2^49&amp;quot;,0,null],[&amp;quot;+2^50&amp;quot;,0,null],[&amp;quot;+2^51&amp;quot;,0,null],[&amp;quot;+2^52&amp;quot;,0,null],[&amp;quot;+2^53&amp;quot;,0,null],[&amp;quot;+2^54&amp;quot;,0,null],[&amp;quot;+2^55&amp;quot;,0,null],[&amp;quot;+2^56&amp;quot;,0,null],[&amp;quot;+2^57&amp;quot;,0,null],[&amp;quot;+2^58&amp;quot;,0,null],[&amp;quot;+2^59&amp;quot;,0,null],[&amp;quot;+2^60&amp;quot;,0,null],[&amp;quot;+2^61&amp;quot;,0,null],[&amp;quot;+2^62&amp;quot;,0,null],[&amp;quot;+2^63&amp;quot;,0,null],[&amp;quot;+2^64&amp;quot;,0,null],[&amp;quot;+2^65&amp;quot;,0,null],[&amp;quot;+2^66&amp;quot;,0,null],[&amp;quot;+2^67&amp;quot;,0,null],[&amp;quot;+2^68&amp;quot;,0,null],[&amp;quot;+2^69&amp;quot;,0,null],[&amp;quot;+2^70&amp;quot;,0,null],[&amp;quot;+2^71&amp;quot;,0,null],[&amp;quot;+2^72&amp;quot;,0,null],[&amp;quot;+2^73&amp;quot;,0,null],[&amp;quot;+2^74&amp;quot;,0,null],[&amp;quot;+2^75&amp;quot;,0,null],[&amp;quot;+2^76&amp;quot;,0,null],[&amp;quot;+2^77&amp;quot;,0,null],[&amp;quot;+2^78&amp;quot;,0,null],[&amp;quot;+2^79&amp;quot;,0,null],[&amp;quot;+2^80&amp;quot;,0,null],[&amp;quot;+2^81&amp;quot;,0,null],[&amp;quot;+2^82&amp;quot;,0,null],[&amp;quot;+2^83&amp;quot;,0,null],[&amp;quot;+2^84&amp;quot;,0,null],[&amp;quot;+2^85&amp;quot;,0,null],[&amp;quot;+2^86&amp;quot;,0,null],[&amp;quot;+2^87&amp;quot;,0,null],[&amp;quot;+2^88&amp;quot;,0,null],[&amp;quot;+2^89&amp;quot;,0,null],[&amp;quot;+2^90&amp;quot;,0,null],[&amp;quot;+2^91&amp;quot;,0,null],[&amp;quot;+2^92&amp;quot;,0,null],[&amp;quot;+2^93&amp;quot;,0,null],[&amp;quot;+2^94&amp;quot;,0,null],[&amp;quot;+2^95&amp;quot;,0,null],[&amp;quot;+2^96&amp;quot;,0,null],[&amp;quot;+2^97&amp;quot;,0,null],[&amp;quot;+2^98&amp;quot;,0,null],[&amp;quot;+2^99&amp;quot;,0,null],[&amp;quot;+2^100&amp;quot;,0,null],[&amp;quot;+2^101&amp;quot;,0,null],[&amp;quot;+2^102&amp;quot;,0,null],[&amp;quot;+2^103&amp;quot;,0,null],[&amp;quot;+2^104&amp;quot;,0,null],[&amp;quot;+2^105&amp;quot;,0,null],[&amp;quot;+2^106&amp;quot;,0,null],[&amp;quot;+2^107&amp;quot;,0,null],[&amp;quot;+2^108&amp;quot;,0,null],[&amp;quot;+2^109&amp;quot;,0,null],[&amp;quot;+2^110&amp;quot;,0,null],[&amp;quot;+2^111&amp;quot;,0,null],[&amp;quot;+2^112&amp;quot;,0,null],[&amp;quot;+2^113&amp;quot;,0,null],[&amp;quot;+2^114&amp;quot;,0,null],[&amp;quot;+2^115&amp;quot;,0,null],[&amp;quot;+2^116&amp;quot;,0,null],[&amp;quot;+2^117&amp;quot;,0,null],[&amp;quot;+2^118&amp;quot;,0,null],[&amp;quot;+2^119&amp;quot;,0,null],[&amp;quot;+2^120&amp;quot;,0,null],[&amp;quot;+2^121&amp;quot;,0,null],[&amp;quot;+2^122&amp;quot;,0,null],[&amp;quot;+2^123&amp;quot;,0,null],[&amp;quot;+2^124&amp;quot;,0,null],[&amp;quot;+2^125&amp;quot;,0,null],[&amp;quot;+2^126&amp;quot;,0,null],[&amp;quot;+2^127&amp;quot;,0,null] ]&lt;br /&gt;
 }}&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Key observation:&#039;&#039;&#039;  &lt;br /&gt;
Compared to direct exponential formulations, the &amp;lt;code&amp;gt;expm1&amp;lt;/code&amp;gt; approach maintains low ULP error for small and medium values of &amp;lt;math&amp;gt;x&amp;lt;/math&amp;gt;. &lt;br /&gt;
Overflow is still present for very large inputs, but the overall error behavior is substantially more stable.&lt;br /&gt;
&lt;br /&gt;
== Numerical Issues and Error Metrics ==&lt;br /&gt;
&lt;br /&gt;
To evaluate and compare different software implementations of &amp;lt;code&amp;gt;tanh&amp;lt;/code&amp;gt;, several numerical error metrics are used throughout this article. These metrics make it possible to reason about accuracy, robustness, and failure modes in floating-point arithmetic.&lt;br /&gt;
&lt;br /&gt;
=== ULP Error ===&lt;br /&gt;
ULP (Unit in the Last Place) measures the distance between two adjacent representable floating-point numbers. The ULP error of a computed result is defined as the difference, measured in ULPs, between the computed value and the correctly rounded reference value.&lt;br /&gt;
&lt;br /&gt;
An error of &amp;lt;code&amp;gt;ULP ≤ 2&amp;lt;/code&amp;gt; is commonly considered acceptable for single-precision transcendental functions, as it indicates that the result is very close to the correctly rounded value.&lt;br /&gt;
&lt;br /&gt;
Large ULP errors typically indicate catastrophic cancellation, loss of significant bits, or severe rounding effects.&lt;br /&gt;
&lt;br /&gt;
=== NaN and Overflow ===&lt;br /&gt;
NaN (Not a Number) results occur when undefined floating-point operations are performed, such as &amp;lt;code&amp;gt;inf / inf&amp;lt;/code&amp;gt;. In &amp;lt;code&amp;gt;tanh&amp;lt;/code&amp;gt; implementations, this often happens when both the numerator and denominator of a formulation overflow to infinity.&lt;br /&gt;
&lt;br /&gt;
Tracking NaN generation is critical, as NaNs can silently propagate through numerical pipelines and invalidate downstream results.&lt;br /&gt;
&lt;br /&gt;
=== Error Graphs ===&lt;br /&gt;
The error graphs shown in the following sections visualize ULP error across the full &amp;lt;code&amp;gt;float32&amp;lt;/code&amp;gt; input domain. They make it possible to identify:&lt;br /&gt;
* regions with catastrophic cancellation,&lt;br /&gt;
* input ranges that trigger overflow or NaNs,&lt;br /&gt;
* and numerical stability differences between formulations.&lt;br /&gt;
&lt;br /&gt;
These graphs are a key tool for understanding not only the maximum error, but also how errors are distributed across different input magnitudes.&lt;br /&gt;
&lt;br /&gt;
== Taylor Expansion (Local Approximation) ==&lt;br /&gt;
&lt;br /&gt;
Taylor series approximations of &amp;lt;code&amp;gt;tanh(x)&amp;lt;/code&amp;gt; expand the function around &amp;lt;math&amp;gt;x = 0&amp;lt;/math&amp;gt; and therefore provide a very accurate approximation only for sufficiently small input magnitudes.&lt;br /&gt;
&lt;br /&gt;
For small &amp;lt;math&amp;gt;|x|&amp;lt;/math&amp;gt;, the hyperbolic tangent behaves approximately linear (&amp;lt;math&amp;gt;\tanh(x) \approx x&amp;lt;/math&amp;gt;), and low-order Taylor polynomials can achieve excellent accuracy with minimal computational cost. This makes Taylor-based approximations attractive as a fast path in range-reduced implementations.&lt;br /&gt;
&lt;br /&gt;
However, Taylor expansions have a strictly limited radius of convergence. Outside a narrow neighborhood around zero, the approximation error grows extremely rapidly and quickly becomes unusable. As a result, Taylor polynomials are not suitable as standalone implementations of &amp;lt;code&amp;gt;tanh&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
{{#widget:FloatErrorGraph|id=taylor&lt;br /&gt;
  |data=[ [&amp;quot;-2^127&amp;quot;,0,0],[&amp;quot;-2^126&amp;quot;,0,0],[&amp;quot;-2^125&amp;quot;,0,0],[&amp;quot;-2^124&amp;quot;,0,0],[&amp;quot;-2^123&amp;quot;,0,0],[&amp;quot;-2^122&amp;quot;,0,0],[&amp;quot;-2^121&amp;quot;,0,0],[&amp;quot;-2^120&amp;quot;,0,0],[&amp;quot;-2^119&amp;quot;,0,0],[&amp;quot;-2^118&amp;quot;,0,0],[&amp;quot;-2^117&amp;quot;,0,0],[&amp;quot;-2^116&amp;quot;,0,0],[&amp;quot;-2^115&amp;quot;,0,0],[&amp;quot;-2^114&amp;quot;,0,0],[&amp;quot;-2^113&amp;quot;,0,0],[&amp;quot;-2^112&amp;quot;,0,0],[&amp;quot;-2^111&amp;quot;,0,0],[&amp;quot;-2^110&amp;quot;,0,0],[&amp;quot;-2^109&amp;quot;,0,0],[&amp;quot;-2^108&amp;quot;,0,0],[&amp;quot;-2^107&amp;quot;,0,0],[&amp;quot;-2^106&amp;quot;,0,0],[&amp;quot;-2^105&amp;quot;,0,0],[&amp;quot;-2^104&amp;quot;,0,0],[&amp;quot;-2^103&amp;quot;,0,0],[&amp;quot;-2^102&amp;quot;,0,0],[&amp;quot;-2^101&amp;quot;,0,0],[&amp;quot;-2^100&amp;quot;,0,0],[&amp;quot;-2^99&amp;quot;,0,0],[&amp;quot;-2^98&amp;quot;,0,0],[&amp;quot;-2^97&amp;quot;,0,0],[&amp;quot;-2^96&amp;quot;,0,0],[&amp;quot;-2^95&amp;quot;,0,0],[&amp;quot;-2^94&amp;quot;,0,0],[&amp;quot;-2^93&amp;quot;,0,0],[&amp;quot;-2^92&amp;quot;,0,0],[&amp;quot;-2^91&amp;quot;,0,0],[&amp;quot;-2^90&amp;quot;,0,0],[&amp;quot;-2^89&amp;quot;,0,0],[&amp;quot;-2^88&amp;quot;,0,0],[&amp;quot;-2^87&amp;quot;,0,0],[&amp;quot;-2^86&amp;quot;,0,0],[&amp;quot;-2^85&amp;quot;,0,0],[&amp;quot;-2^84&amp;quot;,0,0],[&amp;quot;-2^83&amp;quot;,0,0],[&amp;quot;-2^82&amp;quot;,0,0],[&amp;quot;-2^81&amp;quot;,0,0],[&amp;quot;-2^80&amp;quot;,0,0],[&amp;quot;-2^79&amp;quot;,0,0],[&amp;quot;-2^78&amp;quot;,0,0],[&amp;quot;-2^77&amp;quot;,0,0],[&amp;quot;-2^76&amp;quot;,0,0],[&amp;quot;-2^75&amp;quot;,0,0],[&amp;quot;-2^74&amp;quot;,0,0],[&amp;quot;-2^73&amp;quot;,0,0],[&amp;quot;-2^72&amp;quot;,0,0],[&amp;quot;-2^71&amp;quot;,0,0],[&amp;quot;-2^70&amp;quot;,0,0],[&amp;quot;-2^69&amp;quot;,0,0],[&amp;quot;-2^68&amp;quot;,0,0],[&amp;quot;-2^67&amp;quot;,0,0],[&amp;quot;-2^66&amp;quot;,0,0],[&amp;quot;-2^65&amp;quot;,0,0],[&amp;quot;-2^64&amp;quot;,0,0],[&amp;quot;-2^63&amp;quot;,1.84467e+19,3.09485e+26],[&amp;quot;-2^62&amp;quot;,9.22336e+18,1.54742e+26],[&amp;quot;-2^61&amp;quot;,4.61168e+18,7.73712e+25],[&amp;quot;-2^60&amp;quot;,2.30584e+18,3.86856e+25],[&amp;quot;-2^59&amp;quot;,1.15292e+18,1.93428e+25],[&amp;quot;-2^58&amp;quot;,5.7646e+17,9.6714e+24],[&amp;quot;-2^57&amp;quot;,2.8823e+17,4.8357e+24],[&amp;quot;-2^56&amp;quot;,1.44115e+17,2.41785e+24],[&amp;quot;-2^55&amp;quot;,7.20575e+16,1.20892e+24],[&amp;quot;-2^54&amp;quot;,3.60288e+16,6.04462e+23],[&amp;quot;-2^53&amp;quot;,1.80144e+16,3.02231e+23],[&amp;quot;-2^52&amp;quot;,9.00719e+15,1.51116e+23],[&amp;quot;-2^51&amp;quot;,4.5036e+15,7.55578e+22],[&amp;quot;-2^50&amp;quot;,2.2518e+15,3.77789e+22],[&amp;quot;-2^49&amp;quot;,1.1259e+15,1.88894e+22],[&amp;quot;-2^48&amp;quot;,5.62949e+14,9.44472e+21],[&amp;quot;-2^47&amp;quot;,2.81475e+14,4.72236e+21],[&amp;quot;-2^46&amp;quot;,1.40737e+14,2.36118e+21],[&amp;quot;-2^45&amp;quot;,7.03687e+13,1.18059e+21],[&amp;quot;-2^44&amp;quot;,3.51843e+13,5.90295e+20],[&amp;quot;-2^43&amp;quot;,1.75922e+13,2.95148e+20],[&amp;quot;-2^42&amp;quot;,8.79608e+12,1.47574e+20],[&amp;quot;-2^41&amp;quot;,4.39804e+12,7.37869e+19],[&amp;quot;-2^40&amp;quot;,2.19902e+12,3.68935e+19],[&amp;quot;-2^39&amp;quot;,1.09951e+12,1.84467e+19],[&amp;quot;-2^38&amp;quot;,5.49755e+11,9.22336e+18],[&amp;quot;-2^37&amp;quot;,2.74878e+11,4.61168e+18],[&amp;quot;-2^36&amp;quot;,1.37439e+11,2.30584e+18],[&amp;quot;-2^35&amp;quot;,6.87194e+10,1.15292e+18],[&amp;quot;-2^34&amp;quot;,3.43597e+10,5.7646e+17],[&amp;quot;-2^33&amp;quot;,1.71799e+10,2.8823e+17],[&amp;quot;-2^32&amp;quot;,8.58993e+09,1.44115e+17],[&amp;quot;-2^31&amp;quot;,4.29085e+09,7.19885e+16],[&amp;quot;-2^30&amp;quot;,2.14542e+09,3.59943e+16],[&amp;quot;-2^29&amp;quot;,1.07271e+09,1.79971e+16],[&amp;quot;-2^28&amp;quot;,5.36356e+08,8.99856e+15],[&amp;quot;-2^27&amp;quot;,2.68178e+08,4.49928e+15],[&amp;quot;-2^26&amp;quot;,1.34089e+08,2.24964e+15],[&amp;quot;-2^25&amp;quot;,6.70445e+07,1.12482e+15],[&amp;quot;-2^24&amp;quot;,3.35223e+07,5.6241e+14],[&amp;quot;-2^23&amp;quot;,1.67611e+07,2.81205e+14],[&amp;quot;-2^22&amp;quot;,8.38056e+06,1.40603e+14],[&amp;quot;-2^21&amp;quot;,4.19028e+06,7.03013e+13],[&amp;quot;-2^20&amp;quot;,2.09514e+06,3.51506e+13],[&amp;quot;-2^19&amp;quot;,1.04757e+06,1.75753e+13],[&amp;quot;-2^18&amp;quot;,523784,8.78764e+12],[&amp;quot;-2^17&amp;quot;,261892,4.39381e+12],[&amp;quot;-2^16&amp;quot;,130945,2.1969e+12],[&amp;quot;-2^15&amp;quot;,65472.2,1.09844e+12],[&amp;quot;-2^14&amp;quot;,32735.6,5.49212e+11],[&amp;quot;-2^13&amp;quot;,16367.3,2.74598e+11],[&amp;quot;-2^12&amp;quot;,8183.15,1.3729e+11],[&amp;quot;-2^11&amp;quot;,4091.07,6.86368e+10],[&amp;quot;-2^10&amp;quot;,2045.04,3.431e+10],[&amp;quot;-2^9&amp;quot;,1022.02,1.71466e+10],[&amp;quot;-2^8&amp;quot;,510.509,8.56492e+09],[&amp;quot;-2^7&amp;quot;,254.754,4.27406e+09],[&amp;quot;-2^6&amp;quot;,126.876,2.12862e+09],[&amp;quot;-2^5&amp;quot;,62.9354,1.05588e+09],[&amp;quot;-2^4&amp;quot;,30.9629,5.19471e+08],[&amp;quot;-2^3&amp;quot;,14.9718,2.51185e+08],[&amp;quot;-2^2&amp;quot;,6.96672,1.16882e+08],[&amp;quot;-2^1&amp;quot;,2.94633,4.94312e+07],[&amp;quot;-2^0&amp;quot;,0.940715,1.57826e+07],[&amp;quot;-2^-1&amp;quot;,0.127635,2.14135e+06],[&amp;quot;-2^-2&amp;quot;,0.00318735,106950],[&amp;quot;-2^-3&amp;quot;,3.11475e-05,2090.28],[&amp;quot;-2^-4&amp;quot;,2.57737e-07,34.5929],[&amp;quot;-2^-5&amp;quot;,3.89213e-09,1.04478],[&amp;quot;-2^-6&amp;quot;,9.51472e-10,0.510818],[&amp;quot;-2^-7&amp;quot;,4.66402e-10,0.500795],[&amp;quot;-2^-8&amp;quot;,2.32917e-10,0.500185],[&amp;quot;-2^-9&amp;quot;,1.16423e-10,0.500033],[&amp;quot;-2^-10&amp;quot;,5.82082e-11,0.500004],[&amp;quot;-2^-11&amp;quot;,2.91037e-11,0.499998],[&amp;quot;-2^-12&amp;quot;,1.45519e-11,0.5],[&amp;quot;-2^-13&amp;quot;,4.85062e-12,0.333332],[&amp;quot;-2^-14&amp;quot;,6.06328e-13,0.0833331],[&amp;quot;-2^-15&amp;quot;,7.5791e-14,0.0208333],[&amp;quot;-2^-16&amp;quot;,9.47387e-15,0.00520832],[&amp;quot;-2^-17&amp;quot;,1.18424e-15,0.00130208],[&amp;quot;-2^-18&amp;quot;,1.48029e-16,0.00032552],[&amp;quot;-2^-19&amp;quot;,1.85039e-17,8.13808e-05],[&amp;quot;-2^-20&amp;quot;,2.31304e-18,2.03457e-05],[&amp;quot;-2^-21&amp;quot;,2.89156e-19,5.08688e-06],[&amp;quot;-2^-22&amp;quot;,3.61577e-20,1.27219e-06],[&amp;quot;-2^-23&amp;quot;,4.52633e-21,3.18512e-07],[&amp;quot;-2^-24&amp;quot;,5.691e-22,8.00937e-08],[&amp;quot;-2^-25&amp;quot;,7.27919e-23,2.04891e-08],[&amp;quot;-2^-26&amp;quot;,9.92617e-24,5.58794e-09],[&amp;quot;-2^-27&amp;quot;,1.65436e-24,1.86265e-09],[&amp;quot;-2^-28&amp;quot;,8.27181e-25,1.86265e-09],[&amp;quot;-2^-29&amp;quot;,4.1359e-25,1.86265e-09],[&amp;quot;-2^-30&amp;quot;,2.06795e-25,1.86265e-09],[&amp;quot;-2^-31&amp;quot;,1.03398e-25,1.86265e-09],[&amp;quot;-2^-32&amp;quot;,5.16988e-26,1.86265e-09],[&amp;quot;-2^-33&amp;quot;,2.58494e-26,1.86265e-09],[&amp;quot;-2^-34&amp;quot;,1.29247e-26,1.86265e-09],[&amp;quot;-2^-35&amp;quot;,6.46235e-27,1.86265e-09],[&amp;quot;-2^-36&amp;quot;,3.23117e-27,1.86265e-09],[&amp;quot;-2^-37&amp;quot;,1.61559e-27,1.86265e-09],[&amp;quot;-2^-38&amp;quot;,8.07794e-28,1.86265e-09],[&amp;quot;-2^-39&amp;quot;,4.03897e-28,1.86265e-09],[&amp;quot;-2^-40&amp;quot;,2.01948e-28,1.86265e-09],[&amp;quot;-2^-41&amp;quot;,1.00974e-28,1.86265e-09],[&amp;quot;-2^-42&amp;quot;,5.04871e-29,1.86265e-09],[&amp;quot;-2^-43&amp;quot;,2.52435e-29,1.86265e-09],[&amp;quot;-2^-44&amp;quot;,1.26218e-29,1.86265e-09],[&amp;quot;-2^-45&amp;quot;,6.31089e-30,1.86265e-09],[&amp;quot;-2^-46&amp;quot;,3.15544e-30,1.86265e-09],[&amp;quot;-2^-47&amp;quot;,1.57772e-30,1.86265e-09],[&amp;quot;-2^-48&amp;quot;,7.88861e-31,1.86265e-09],[&amp;quot;-2^-49&amp;quot;,3.9443e-31,1.86265e-09],[&amp;quot;-2^-50&amp;quot;,1.97215e-31,1.86265e-09],[&amp;quot;-2^-51&amp;quot;,9.86076e-32,1.86265e-09],[&amp;quot;-2^-52&amp;quot;,4.93038e-32,1.86265e-09],[&amp;quot;-2^-53&amp;quot;,2.46519e-32,1.86265e-09],[&amp;quot;-2^-54&amp;quot;,1.2326e-32,1.86265e-09],[&amp;quot;-2^-55&amp;quot;,0,0],[&amp;quot;-2^-56&amp;quot;,0,0],[&amp;quot;-2^-57&amp;quot;,0,0],[&amp;quot;-2^-58&amp;quot;,0,0],[&amp;quot;-2^-59&amp;quot;,0,0],[&amp;quot;-2^-60&amp;quot;,0,0],[&amp;quot;-2^-61&amp;quot;,0,0],[&amp;quot;-2^-62&amp;quot;,0,0],[&amp;quot;-2^-63&amp;quot;,0,0],[&amp;quot;-2^-64&amp;quot;,0,0],[&amp;quot;-2^-65&amp;quot;,0,0],[&amp;quot;-2^-66&amp;quot;,0,0],[&amp;quot;-2^-67&amp;quot;,0,0],[&amp;quot;-2^-68&amp;quot;,0,0],[&amp;quot;-2^-69&amp;quot;,0,0],[&amp;quot;-2^-70&amp;quot;,0,0],[&amp;quot;-2^-71&amp;quot;,0,0],[&amp;quot;-2^-72&amp;quot;,0,0],[&amp;quot;-2^-73&amp;quot;,0,0],[&amp;quot;-2^-74&amp;quot;,0,0],[&amp;quot;-2^-75&amp;quot;,0,0],[&amp;quot;-2^-76&amp;quot;,0,0],[&amp;quot;-2^-77&amp;quot;,0,0],[&amp;quot;-2^-78&amp;quot;,0,0],[&amp;quot;-2^-79&amp;quot;,0,0],[&amp;quot;-2^-80&amp;quot;,0,0],[&amp;quot;-2^-81&amp;quot;,0,0],[&amp;quot;-2^-82&amp;quot;,0,0],[&amp;quot;-2^-83&amp;quot;,0,0],[&amp;quot;-2^-84&amp;quot;,0,0],[&amp;quot;-2^-85&amp;quot;,0,0],[&amp;quot;-2^-86&amp;quot;,0,0],[&amp;quot;-2^-87&amp;quot;,0,0],[&amp;quot;-2^-88&amp;quot;,0,0],[&amp;quot;-2^-89&amp;quot;,0,0],[&amp;quot;-2^-90&amp;quot;,0,0],[&amp;quot;-2^-91&amp;quot;,0,0],[&amp;quot;-2^-92&amp;quot;,0,0],[&amp;quot;-2^-93&amp;quot;,0,0],[&amp;quot;-2^-94&amp;quot;,0,0],[&amp;quot;-2^-95&amp;quot;,0,0],[&amp;quot;-2^-96&amp;quot;,0,0],[&amp;quot;-2^-97&amp;quot;,0,0],[&amp;quot;-2^-98&amp;quot;,0,0],[&amp;quot;-2^-99&amp;quot;,0,0],[&amp;quot;-2^-100&amp;quot;,0,0],[&amp;quot;-2^-101&amp;quot;,0,0],[&amp;quot;-2^-102&amp;quot;,0,0],[&amp;quot;-2^-103&amp;quot;,0,0],[&amp;quot;-2^-104&amp;quot;,0,0],[&amp;quot;-2^-105&amp;quot;,0,0],[&amp;quot;-2^-106&amp;quot;,0,0],[&amp;quot;-2^-107&amp;quot;,0,0],[&amp;quot;-2^-108&amp;quot;,0,0],[&amp;quot;-2^-109&amp;quot;,0,0],[&amp;quot;-2^-110&amp;quot;,0,0],[&amp;quot;-2^-111&amp;quot;,0,0],[&amp;quot;-2^-112&amp;quot;,0,0],[&amp;quot;-2^-113&amp;quot;,0,0],[&amp;quot;-2^-114&amp;quot;,0,0],[&amp;quot;-2^-115&amp;quot;,0,0],[&amp;quot;-2^-116&amp;quot;,0,0],[&amp;quot;-2^-117&amp;quot;,0,0],[&amp;quot;-2^-118&amp;quot;,0,0],[&amp;quot;-2^-119&amp;quot;,0,0],[&amp;quot;-2^-120&amp;quot;,0,0],[&amp;quot;-2^-121&amp;quot;,0,0],[&amp;quot;-2^-122&amp;quot;,0,0],[&amp;quot;-2^-123&amp;quot;,0,0],[&amp;quot;-2^-124&amp;quot;,0,0],[&amp;quot;-2^-125&amp;quot;,0,0],[&amp;quot;-2^-126&amp;quot;,0,0],[&amp;quot;-SN&amp;quot;,0,0],[&amp;quot;0&amp;quot;,0,0],[&amp;quot;+SN&amp;quot;,0,0],[&amp;quot;+2^-126&amp;quot;,0,0],[&amp;quot;+2^-125&amp;quot;,0,0],[&amp;quot;+2^-124&amp;quot;,0,0],[&amp;quot;+2^-123&amp;quot;,0,0],[&amp;quot;+2^-122&amp;quot;,0,0],[&amp;quot;+2^-121&amp;quot;,0,0],[&amp;quot;+2^-120&amp;quot;,0,0],[&amp;quot;+2^-119&amp;quot;,0,0],[&amp;quot;+2^-118&amp;quot;,0,0],[&amp;quot;+2^-117&amp;quot;,0,0],[&amp;quot;+2^-116&amp;quot;,0,0],[&amp;quot;+2^-115&amp;quot;,0,0],[&amp;quot;+2^-114&amp;quot;,0,0],[&amp;quot;+2^-113&amp;quot;,0,0],[&amp;quot;+2^-112&amp;quot;,0,0],[&amp;quot;+2^-111&amp;quot;,0,0],[&amp;quot;+2^-110&amp;quot;,0,0],[&amp;quot;+2^-109&amp;quot;,0,0],[&amp;quot;+2^-108&amp;quot;,0,0],[&amp;quot;+2^-107&amp;quot;,0,0],[&amp;quot;+2^-106&amp;quot;,0,0],[&amp;quot;+2^-105&amp;quot;,0,0],[&amp;quot;+2^-104&amp;quot;,0,0],[&amp;quot;+2^-103&amp;quot;,0,0],[&amp;quot;+2^-102&amp;quot;,0,0],[&amp;quot;+2^-101&amp;quot;,0,0],[&amp;quot;+2^-100&amp;quot;,0,0],[&amp;quot;+2^-99&amp;quot;,0,0],[&amp;quot;+2^-98&amp;quot;,0,0],[&amp;quot;+2^-97&amp;quot;,0,0],[&amp;quot;+2^-96&amp;quot;,0,0],[&amp;quot;+2^-95&amp;quot;,0,0],[&amp;quot;+2^-94&amp;quot;,0,0],[&amp;quot;+2^-93&amp;quot;,0,0],[&amp;quot;+2^-92&amp;quot;,0,0],[&amp;quot;+2^-91&amp;quot;,0,0],[&amp;quot;+2^-90&amp;quot;,0,0],[&amp;quot;+2^-89&amp;quot;,0,0],[&amp;quot;+2^-88&amp;quot;,0,0],[&amp;quot;+2^-87&amp;quot;,0,0],[&amp;quot;+2^-86&amp;quot;,0,0],[&amp;quot;+2^-85&amp;quot;,0,0],[&amp;quot;+2^-84&amp;quot;,0,0],[&amp;quot;+2^-83&amp;quot;,0,0],[&amp;quot;+2^-82&amp;quot;,0,0],[&amp;quot;+2^-81&amp;quot;,0,0],[&amp;quot;+2^-80&amp;quot;,0,0],[&amp;quot;+2^-79&amp;quot;,0,0],[&amp;quot;+2^-78&amp;quot;,0,0],[&amp;quot;+2^-77&amp;quot;,0,0],[&amp;quot;+2^-76&amp;quot;,0,0],[&amp;quot;+2^-75&amp;quot;,0,0],[&amp;quot;+2^-74&amp;quot;,0,0],[&amp;quot;+2^-73&amp;quot;,0,0],[&amp;quot;+2^-72&amp;quot;,0,0],[&amp;quot;+2^-71&amp;quot;,0,0],[&amp;quot;+2^-70&amp;quot;,0,0],[&amp;quot;+2^-69&amp;quot;,0,0],[&amp;quot;+2^-68&amp;quot;,0,0],[&amp;quot;+2^-67&amp;quot;,0,0],[&amp;quot;+2^-66&amp;quot;,0,0],[&amp;quot;+2^-65&amp;quot;,0,0],[&amp;quot;+2^-64&amp;quot;,0,0],[&amp;quot;+2^-63&amp;quot;,0,0],[&amp;quot;+2^-62&amp;quot;,0,0],[&amp;quot;+2^-61&amp;quot;,0,0],[&amp;quot;+2^-60&amp;quot;,0,0],[&amp;quot;+2^-59&amp;quot;,0,0],[&amp;quot;+2^-58&amp;quot;,0,0],[&amp;quot;+2^-57&amp;quot;,0,0],[&amp;quot;+2^-56&amp;quot;,0,0],[&amp;quot;+2^-55&amp;quot;,0,0],[&amp;quot;+2^-54&amp;quot;,1.2326e-32,1.86265e-09],[&amp;quot;+2^-53&amp;quot;,2.46519e-32,1.86265e-09],[&amp;quot;+2^-52&amp;quot;,4.93038e-32,1.86265e-09],[&amp;quot;+2^-51&amp;quot;,9.86076e-32,1.86265e-09],[&amp;quot;+2^-50&amp;quot;,1.97215e-31,1.86265e-09],[&amp;quot;+2^-49&amp;quot;,3.9443e-31,1.86265e-09],[&amp;quot;+2^-48&amp;quot;,7.88861e-31,1.86265e-09],[&amp;quot;+2^-47&amp;quot;,1.57772e-30,1.86265e-09],[&amp;quot;+2^-46&amp;quot;,3.15544e-30,1.86265e-09],[&amp;quot;+2^-45&amp;quot;,6.31089e-30,1.86265e-09],[&amp;quot;+2^-44&amp;quot;,1.26218e-29,1.86265e-09],[&amp;quot;+2^-43&amp;quot;,2.52435e-29,1.86265e-09],[&amp;quot;+2^-42&amp;quot;,5.04871e-29,1.86265e-09],[&amp;quot;+2^-41&amp;quot;,1.00974e-28,1.86265e-09],[&amp;quot;+2^-40&amp;quot;,2.01948e-28,1.86265e-09],[&amp;quot;+2^-39&amp;quot;,4.03897e-28,1.86265e-09],[&amp;quot;+2^-38&amp;quot;,8.07794e-28,1.86265e-09],[&amp;quot;+2^-37&amp;quot;,1.61559e-27,1.86265e-09],[&amp;quot;+2^-36&amp;quot;,3.23117e-27,1.86265e-09],[&amp;quot;+2^-35&amp;quot;,6.46235e-27,1.86265e-09],[&amp;quot;+2^-34&amp;quot;,1.29247e-26,1.86265e-09],[&amp;quot;+2^-33&amp;quot;,2.58494e-26,1.86265e-09],[&amp;quot;+2^-32&amp;quot;,5.16988e-26,1.86265e-09],[&amp;quot;+2^-31&amp;quot;,1.03398e-25,1.86265e-09],[&amp;quot;+2^-30&amp;quot;,2.06795e-25,1.86265e-09],[&amp;quot;+2^-29&amp;quot;,4.1359e-25,1.86265e-09],[&amp;quot;+2^-28&amp;quot;,8.27181e-25,1.86265e-09],[&amp;quot;+2^-27&amp;quot;,1.65436e-24,1.86265e-09],[&amp;quot;+2^-26&amp;quot;,9.92617e-24,5.58794e-09],[&amp;quot;+2^-25&amp;quot;,7.27919e-23,2.04891e-08],[&amp;quot;+2^-24&amp;quot;,5.691e-22,8.00937e-08],[&amp;quot;+2^-23&amp;quot;,4.52633e-21,3.18512e-07],[&amp;quot;+2^-22&amp;quot;,3.61577e-20,1.27219e-06],[&amp;quot;+2^-21&amp;quot;,2.89156e-19,5.08688e-06],[&amp;quot;+2^-20&amp;quot;,2.31304e-18,2.03457e-05],[&amp;quot;+2^-19&amp;quot;,1.85039e-17,8.13808e-05],[&amp;quot;+2^-18&amp;quot;,1.48029e-16,0.00032552],[&amp;quot;+2^-17&amp;quot;,1.18424e-15,0.00130208],[&amp;quot;+2^-16&amp;quot;,9.47387e-15,0.00520832],[&amp;quot;+2^-15&amp;quot;,7.5791e-14,0.0208333],[&amp;quot;+2^-14&amp;quot;,6.06328e-13,0.0833331],[&amp;quot;+2^-13&amp;quot;,4.85062e-12,0.333332],[&amp;quot;+2^-12&amp;quot;,1.45519e-11,0.5],[&amp;quot;+2^-11&amp;quot;,2.91037e-11,0.499998],[&amp;quot;+2^-10&amp;quot;,5.82082e-11,0.500004],[&amp;quot;+2^-9&amp;quot;,1.16423e-10,0.500033],[&amp;quot;+2^-8&amp;quot;,2.32917e-10,0.500185],[&amp;quot;+2^-7&amp;quot;,4.66402e-10,0.500795],[&amp;quot;+2^-6&amp;quot;,9.51472e-10,0.510818],[&amp;quot;+2^-5&amp;quot;,3.89213e-09,1.04478],[&amp;quot;+2^-4&amp;quot;,2.57737e-07,34.5929],[&amp;quot;+2^-3&amp;quot;,3.11475e-05,2090.28],[&amp;quot;+2^-2&amp;quot;,0.00318735,106950],[&amp;quot;+2^-1&amp;quot;,0.127635,2.14135e+06],[&amp;quot;+2^0&amp;quot;,0.940715,1.57826e+07],[&amp;quot;+2^1&amp;quot;,2.94633,4.94312e+07],[&amp;quot;+2^2&amp;quot;,6.96672,1.16882e+08],[&amp;quot;+2^3&amp;quot;,14.9718,1.33874e+08],[&amp;quot;+2^4&amp;quot;,30.9629,2.59736e+08],[&amp;quot;+2^5&amp;quot;,62.9354,5.27941e+08],[&amp;quot;+2^6&amp;quot;,126.876,1.06431e+09],[&amp;quot;+2^7&amp;quot;,254.754,2.13703e+09],[&amp;quot;+2^8&amp;quot;,510.509,4.28246e+09],[&amp;quot;+2^9&amp;quot;,1022.02,8.57331e+09],[&amp;quot;+2^10&amp;quot;,2045.04,1.7155e+10],[&amp;quot;+2^11&amp;quot;,4091.07,3.43184e+10],[&amp;quot;+2^12&amp;quot;,8183.15,6.86452e+10],[&amp;quot;+2^13&amp;quot;,16367.3,1.37299e+11],[&amp;quot;+2^14&amp;quot;,32735.6,2.74606e+11],[&amp;quot;+2^15&amp;quot;,65472.2,5.4922e+11],[&amp;quot;+2^16&amp;quot;,130945,1.09845e+12],[&amp;quot;+2^17&amp;quot;,261892,2.19691e+12],[&amp;quot;+2^18&amp;quot;,523784,4.39382e+12],[&amp;quot;+2^19&amp;quot;,1.04757e+06,8.78765e+12],[&amp;quot;+2^20&amp;quot;,2.09514e+06,1.75753e+13],[&amp;quot;+2^21&amp;quot;,4.19028e+06,3.51506e+13],[&amp;quot;+2^22&amp;quot;,8.38056e+06,7.03013e+13],[&amp;quot;+2^23&amp;quot;,1.67611e+07,1.40603e+14],[&amp;quot;+2^24&amp;quot;,3.35223e+07,2.81205e+14],[&amp;quot;+2^25&amp;quot;,6.70445e+07,5.6241e+14],[&amp;quot;+2^26&amp;quot;,1.34089e+08,1.12482e+15],[&amp;quot;+2^27&amp;quot;,2.68178e+08,2.24964e+15],[&amp;quot;+2^28&amp;quot;,5.36356e+08,4.49928e+15],[&amp;quot;+2^29&amp;quot;,1.07271e+09,8.99856e+15],[&amp;quot;+2^30&amp;quot;,2.14542e+09,1.79971e+16],[&amp;quot;+2^31&amp;quot;,4.29085e+09,3.59943e+16],[&amp;quot;+2^32&amp;quot;,8.58993e+09,7.20575e+16],[&amp;quot;+2^33&amp;quot;,1.71799e+10,1.44115e+17],[&amp;quot;+2^34&amp;quot;,3.43597e+10,2.8823e+17],[&amp;quot;+2^35&amp;quot;,6.87194e+10,5.7646e+17],[&amp;quot;+2^36&amp;quot;,1.37439e+11,1.15292e+18],[&amp;quot;+2^37&amp;quot;,2.74878e+11,2.30584e+18],[&amp;quot;+2^38&amp;quot;,5.49755e+11,4.61168e+18],[&amp;quot;+2^39&amp;quot;,1.09951e+12,9.22336e+18],[&amp;quot;+2^40&amp;quot;,2.19902e+12,1.84467e+19],[&amp;quot;+2^41&amp;quot;,4.39804e+12,3.68935e+19],[&amp;quot;+2^42&amp;quot;,8.79608e+12,7.37869e+19],[&amp;quot;+2^43&amp;quot;,1.75922e+13,1.47574e+20],[&amp;quot;+2^44&amp;quot;,3.51843e+13,2.95148e+20],[&amp;quot;+2^45&amp;quot;,7.03687e+13,5.90295e+20],[&amp;quot;+2^46&amp;quot;,1.40737e+14,1.18059e+21],[&amp;quot;+2^47&amp;quot;,2.81475e+14,2.36118e+21],[&amp;quot;+2^48&amp;quot;,5.62949e+14,4.72236e+21],[&amp;quot;+2^49&amp;quot;,1.1259e+15,9.44472e+21],[&amp;quot;+2^50&amp;quot;,2.2518e+15,1.88894e+22],[&amp;quot;+2^51&amp;quot;,4.5036e+15,3.77789e+22],[&amp;quot;+2^52&amp;quot;,9.00719e+15,7.55578e+22],[&amp;quot;+2^53&amp;quot;,1.80144e+16,1.51116e+23],[&amp;quot;+2^54&amp;quot;,3.60288e+16,3.02231e+23],[&amp;quot;+2^55&amp;quot;,7.20575e+16,6.04462e+23],[&amp;quot;+2^56&amp;quot;,1.44115e+17,1.20892e+24],[&amp;quot;+2^57&amp;quot;,2.8823e+17,2.41785e+24],[&amp;quot;+2^58&amp;quot;,5.7646e+17,4.8357e+24],[&amp;quot;+2^59&amp;quot;,1.15292e+18,9.6714e+24],[&amp;quot;+2^60&amp;quot;,2.30584e+18,1.93428e+25],[&amp;quot;+2^61&amp;quot;,4.61168e+18,3.86856e+25],[&amp;quot;+2^62&amp;quot;,9.22336e+18,7.73712e+25],[&amp;quot;+2^63&amp;quot;,1.84467e+19,1.54742e+26],[&amp;quot;+2^64&amp;quot;,0,0],[&amp;quot;+2^65&amp;quot;,0,0],[&amp;quot;+2^66&amp;quot;,0,0],[&amp;quot;+2^67&amp;quot;,0,0],[&amp;quot;+2^68&amp;quot;,0,0],[&amp;quot;+2^69&amp;quot;,0,0],[&amp;quot;+2^70&amp;quot;,0,0],[&amp;quot;+2^71&amp;quot;,0,0],[&amp;quot;+2^72&amp;quot;,0,0],[&amp;quot;+2^73&amp;quot;,0,0],[&amp;quot;+2^74&amp;quot;,0,0],[&amp;quot;+2^75&amp;quot;,0,0],[&amp;quot;+2^76&amp;quot;,0,0],[&amp;quot;+2^77&amp;quot;,0,0],[&amp;quot;+2^78&amp;quot;,0,0],[&amp;quot;+2^79&amp;quot;,0,0],[&amp;quot;+2^80&amp;quot;,0,0],[&amp;quot;+2^81&amp;quot;,0,0],[&amp;quot;+2^82&amp;quot;,0,0],[&amp;quot;+2^83&amp;quot;,0,0],[&amp;quot;+2^84&amp;quot;,0,0],[&amp;quot;+2^85&amp;quot;,0,0],[&amp;quot;+2^86&amp;quot;,0,0],[&amp;quot;+2^87&amp;quot;,0,0],[&amp;quot;+2^88&amp;quot;,0,0],[&amp;quot;+2^89&amp;quot;,0,0],[&amp;quot;+2^90&amp;quot;,0,0],[&amp;quot;+2^91&amp;quot;,0,0],[&amp;quot;+2^92&amp;quot;,0,0],[&amp;quot;+2^93&amp;quot;,0,0],[&amp;quot;+2^94&amp;quot;,0,0],[&amp;quot;+2^95&amp;quot;,0,0],[&amp;quot;+2^96&amp;quot;,0,0],[&amp;quot;+2^97&amp;quot;,0,0],[&amp;quot;+2^98&amp;quot;,0,0],[&amp;quot;+2^99&amp;quot;,0,0],[&amp;quot;+2^100&amp;quot;,0,0],[&amp;quot;+2^101&amp;quot;,0,0],[&amp;quot;+2^102&amp;quot;,0,0],[&amp;quot;+2^103&amp;quot;,0,0],[&amp;quot;+2^104&amp;quot;,0,0],[&amp;quot;+2^105&amp;quot;,0,0],[&amp;quot;+2^106&amp;quot;,0,0],[&amp;quot;+2^107&amp;quot;,0,0],[&amp;quot;+2^108&amp;quot;,0,0],[&amp;quot;+2^109&amp;quot;,0,0],[&amp;quot;+2^110&amp;quot;,0,0],[&amp;quot;+2^111&amp;quot;,0,0],[&amp;quot;+2^112&amp;quot;,0,0],[&amp;quot;+2^113&amp;quot;,0,0],[&amp;quot;+2^114&amp;quot;,0,0],[&amp;quot;+2^115&amp;quot;,0,0],[&amp;quot;+2^116&amp;quot;,0,0],[&amp;quot;+2^117&amp;quot;,0,0],[&amp;quot;+2^118&amp;quot;,0,0],[&amp;quot;+2^119&amp;quot;,0,0],[&amp;quot;+2^120&amp;quot;,0,0],[&amp;quot;+2^121&amp;quot;,0,0],[&amp;quot;+2^122&amp;quot;,0,0],[&amp;quot;+2^123&amp;quot;,0,0],[&amp;quot;+2^124&amp;quot;,0,0],[&amp;quot;+2^125&amp;quot;,0,0],[&amp;quot;+2^126&amp;quot;,0,0],[&amp;quot;+2^127&amp;quot;,0,0] ]&lt;br /&gt;
 }}&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Key observation:&#039;&#039;&#039;  &lt;br /&gt;
The Taylor approximation is highly accurate near zero but diverges extremely fast outside its convergence region.  &lt;br /&gt;
This makes it unsuitable as a standalone &amp;lt;code&amp;gt;tanh&amp;lt;/code&amp;gt; implementation, but very effective as a local approximation when combined with range-based dispatching.&lt;br /&gt;
&lt;br /&gt;
== Localized Approximation Functions ==&lt;br /&gt;
&lt;br /&gt;
Localized approximation functions are designed to provide highly accurate and computationally efficient evaluations of &amp;lt;code&amp;gt;tanh(x)&amp;lt;/code&amp;gt; within a restricted input range. &lt;br /&gt;
They are typically used as building blocks in range-based dispatch strategies, where different approximations are selected depending on the magnitude of the input value.&lt;br /&gt;
&lt;br /&gt;
By limiting the valid input range, these approximations can achieve very low ULP error with simple polynomial or rational expressions, avoiding expensive transcendental functions.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
!Id&lt;br /&gt;
!Formula&lt;br /&gt;
!ULP &amp;lt;= 2.0&lt;br /&gt;
!NaN&lt;br /&gt;
|-&lt;br /&gt;
|signum&lt;br /&gt;
|&amp;lt;math&amp;gt;\sgn{x}&amp;lt;/math&amp;gt;&lt;br /&gt;
| -inf &amp;lt; x &amp;lt; -8.31763&lt;br /&gt;
8.31763 &amp;lt; x &amp;lt; inf&lt;br /&gt;
| -&lt;br /&gt;
|-&lt;br /&gt;
|poly1&lt;br /&gt;
|&amp;lt;math&amp;gt;x&amp;lt;/math&amp;gt;&lt;br /&gt;
| -0.000704229 &amp;lt; x &amp;lt; 0.000704229&lt;br /&gt;
| -&lt;br /&gt;
|-&lt;br /&gt;
|poly3&lt;br /&gt;
|&amp;lt;math&amp;gt;x - \frac{1}{3} x^3&amp;lt;/math&amp;gt;&lt;br /&gt;
| -0.0291781 &amp;lt; x &amp;lt; 0.0291781 &lt;br /&gt;
| -&lt;br /&gt;
|-&lt;br /&gt;
|lambert7&lt;br /&gt;
|&amp;lt;math&amp;gt;\frac{x \cdot (135135 + x^2 \cdot (17325 + x^2 \cdot (378 + x^2)))}{135135 + x^2 \cdot (62370 + x^2 \cdot (3150 + 28 \cdot x^2))}&lt;br /&gt;
&amp;lt;/math&amp;gt;&lt;br /&gt;
| -0.000947416 &amp;lt; x &amp;lt; 0.000947207&lt;br /&gt;
| -inf &amp;lt; x &amp;lt; -1.51629e+06&lt;br /&gt;
1.51629e+06 &amp;lt; x &amp;lt; inf&lt;br /&gt;
|-&lt;br /&gt;
|pade&lt;br /&gt;
|&amp;lt;math&amp;gt;x + x \cdot \frac{x^2 \cdot \left( p_1 \cdot x^2 + p_0 \right)}{\left( x^2 + q_1 \right) \cdot x^2 + q_0} &lt;br /&gt;
&lt;br /&gt;
&amp;lt;/math&amp;gt; &amp;lt;math&amp;gt;\begin{align}&lt;br /&gt;
p_0 &amp;amp;= -0.2059432032 \\&lt;br /&gt;
p_1 &amp;amp;= -0.0009577527 \\&lt;br /&gt;
q_0 &amp;amp;= 0.6178299136 \\ &lt;br /&gt;
q_1 &amp;amp;= 0.25&lt;br /&gt;
\end{align}&lt;br /&gt;
&amp;lt;/math&amp;gt;&lt;br /&gt;
| -0.0016973 &amp;lt; x &amp;lt; 0.0016973 &lt;br /&gt;
| -inf &amp;lt; x &amp;lt; -6.14896e+18&lt;br /&gt;
6.14896e+18 &amp;lt; x &amp;lt; inf&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
The functions listed above are not intended to be used independently over the full input domain. &lt;br /&gt;
Instead, they are commonly combined with explicit range checks and sign handling to form a complete &amp;lt;code&amp;gt;tanh&amp;lt;/code&amp;gt; implementation.&lt;br /&gt;
&lt;br /&gt;
For example, low-order polynomials are well suited for very small &amp;lt;math&amp;gt;|x|&amp;lt;/math&amp;gt;, while rational or higher-order approximations extend the usable range before switching to exponential-based formulations or saturation.&lt;br /&gt;
&lt;br /&gt;
==Recommended Implementation Strategy==&lt;br /&gt;
A robust and high-performance &amp;lt;code&amp;gt;tanh(x)&amp;lt;/code&amp;gt; implementation typically combines multiple techniques, each optimized for a specific input range. No single formulation provides optimal accuracy and performance across the entire floating-point domain.&lt;br /&gt;
&lt;br /&gt;
A common strategy is based on range-dependent dispatching:&lt;br /&gt;
*&#039;&#039;&#039;Sign handling&#039;&#039;&#039;&lt;br /&gt;
: Use the odd symmetry of &amp;lt;code&amp;gt;tanh&amp;lt;/code&amp;gt; (&amp;lt;math&amp;gt;\tanh(-x) = -\tanh(x)&amp;lt;/math&amp;gt;) to reduce the implementation to non-negative inputs.*&#039;&#039;&#039;Very small inputs (&amp;lt;math&amp;gt;|x| \ll 1&amp;lt;/math&amp;gt;)&#039;&#039;&#039;&lt;br /&gt;
: Use a low-order polynomial or Taylor approximation, exploiting &amp;lt;math&amp;gt;\tanh(x) \approx x&amp;lt;/math&amp;gt; to avoid cancellation and expensive transcendental functions.&lt;br /&gt;
*&#039;&#039;&#039;Small to medium inputs&#039;&#039;&#039;&lt;br /&gt;
: Apply a localized polynomial or rational approximation with bounded ULP error.&lt;br /&gt;
*&#039;&#039;&#039;Medium to large inputs&#039;&#039;&#039;&lt;br /&gt;
: Use an &amp;lt;code&amp;gt;expm1&amp;lt;/code&amp;gt;-based formulation to maintain numerical stability while avoiding catastrophic cancellation.&lt;br /&gt;
*&#039;&#039;&#039;Very large inputs&#039;&#039;&#039;&lt;br /&gt;
: Directly return &amp;lt;code&amp;gt;±1&amp;lt;/code&amp;gt;, as &amp;lt;code&amp;gt;tanh(x)&amp;lt;/code&amp;gt; saturates rapidly and additional computation provides no meaningful benefit.&lt;br /&gt;
This hybrid approach minimizes ULP error, avoids NaN generation, and allows fine-grained control over the accuracy–performance trade-off. It is widely used in optimized math libraries and hardware-accelerated implementations.&lt;br /&gt;
&lt;br /&gt;
== See Also ==&lt;br /&gt;
&lt;br /&gt;
* [[ULP Difference of Float Numbers]]&lt;br /&gt;
&lt;br /&gt;
== External Links ==&lt;br /&gt;
&lt;br /&gt;
* https://yaikhom.com/2020-04-28-localised-approximation-of-hyperbolic-tangents.html#mjx-eqn-eqn7-th%20degree%20lambert%20approximant&lt;br /&gt;
*https://github.com/llvm/llvm-project/blob/main/libc/src/math/generic/tanhf.cpp&lt;br /&gt;
*https://forums.developer.nvidia.com/t/hardware-accelerated-tanh-on-turing/173291&lt;br /&gt;
[[Category:Numerical Precision]]&lt;/div&gt;</summary>
		<author><name>Timo.stripf</name></author>
	</entry>
	<entry>
		<id>https://www.emmtrix.com/w139/index.php?title=tanh_Software_Implementation&amp;diff=2938</id>
		<title>tanh Software Implementation</title>
		<link rel="alternate" type="text/html" href="https://www.emmtrix.com/w139/index.php?title=tanh_Software_Implementation&amp;diff=2938"/>
		<updated>2026-02-04T12:18:52Z</updated>

		<summary type="html">&lt;p&gt;Timo.stripf: /* External Links */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;[[File:Hyperbolic Tangent.svg|thumb]]&lt;br /&gt;
The hyperbolic tangent function, commonly referred to as &amp;lt;code&amp;gt;tanh&amp;lt;/code&amp;gt;, is a mathematical function that maps real numbers to the range &amp;lt;math&amp;gt;(-1, 1)&amp;lt;/math&amp;gt;. It is defined as:&lt;br /&gt;
&lt;br /&gt;
:&amp;lt;math&amp;gt;\tanh(x) = \frac{e^x - e^{-x}}{e^x + e^{-x}}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
From a software implementation perspective, &amp;lt;code&amp;gt;tanh&amp;lt;/code&amp;gt; is a numerically challenging function due to the use of exponential functions, which can easily overflow for large inputs and suffer from catastrophic cancellation for very small values of &amp;lt;math&amp;gt;x&amp;lt;/math&amp;gt;. These issues are particularly relevant in single-precision floating-point (&amp;lt;code&amp;gt;float32&amp;lt;/code&amp;gt;) implementations, where both accuracy and performance are critical.&lt;br /&gt;
&lt;br /&gt;
In machine learning and deep learning, &amp;lt;code&amp;gt;tanh&amp;lt;/code&amp;gt; is widely used as an activation function because it introduces non-linearity and produces outputs centered around zero, which can improve gradient-based optimization. As a result, &amp;lt;code&amp;gt;tanh&amp;lt;/code&amp;gt; is often evaluated billions of times during training and inference, making efficient and numerically stable implementations essential.&lt;br /&gt;
&lt;br /&gt;
This article focuses on practical software implementations of &amp;lt;code&amp;gt;tanh&amp;lt;/code&amp;gt;, analyzing different computational formulations with respect to floating-point accuracy, ULP error, overflow behavior, and NaN generation. The goal is to provide a foundation for designing robust &amp;lt;code&amp;gt;tanh&amp;lt;/code&amp;gt; implementations suitable for performance-critical environments such as numerical libraries, embedded systems, and AI workloads.&lt;br /&gt;
&lt;br /&gt;
== Problem Overview ==&lt;br /&gt;
The hyperbolic tangent function poses several challenges when implemented in finite-precision floating-point arithmetic. While the mathematical definition is simple, direct translations into software often suffer from numerical instability and performance issues.&lt;br /&gt;
&lt;br /&gt;
A primary challenge is the use of exponential functions. For large input values, &amp;lt;code&amp;gt;exp(x)&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;exp(-x)&amp;lt;/code&amp;gt; can overflow in single-precision floating-point arithmetic, leading to infinities and, in some formulations, NaN results. Avoiding overflow therefore requires careful reformulation or explicit range handling.&lt;br /&gt;
&lt;br /&gt;
For very small input values, catastrophic cancellation becomes the dominant issue. Expressions such as &amp;lt;math&amp;gt;e^x - e^{-x}&amp;lt;/math&amp;gt; involve the subtraction of nearly equal numbers, causing a severe loss of significant bits and large ULP errors. In these regions, naive implementations may return zero or highly inaccurate results even though &amp;lt;math&amp;gt;\tanh(x) \approx x&amp;lt;/math&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
In addition to accuracy concerns, performance is a critical factor. In many applications, especially machine learning and numerical simulations, &amp;lt;code&amp;gt;tanh&amp;lt;/code&amp;gt; is evaluated extremely frequently. This makes it necessary to balance numerical robustness with the cost of transcendental function calls, branch complexity, and instruction-level efficiency.&lt;br /&gt;
&lt;br /&gt;
==Direct Exponential Formulations==&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+&lt;br /&gt;
!Id&lt;br /&gt;
!Formula&lt;br /&gt;
!Highest ULP Error&lt;br /&gt;
!ULP &amp;lt;= 2.0&lt;br /&gt;
!NaN&lt;br /&gt;
|-&lt;br /&gt;
|exp_v1&lt;br /&gt;
|&amp;lt;math&amp;gt;\frac{e^x - e^{-x}}{e^x + e^{-x}}&amp;lt;/math&amp;gt;&lt;br /&gt;
|x &amp;lt; 0: 8.55638e+08 at -2.98023e-08 with 0x0p+0 instead of -0x1p-25&lt;br /&gt;
&lt;br /&gt;
x &amp;gt; 0: 8.55638e+08 at 2.98023e-08 with 0x0p+0 instead of 0x1p-25&lt;br /&gt;
| -88.7246 &amp;lt; x &amp;lt; -7.14197&lt;br /&gt;
7.14197 &amp;lt; x &amp;lt; 88.7246&lt;br /&gt;
| -inf &amp;lt; x &amp;lt; -88.7246&lt;br /&gt;
88.7246 &amp;lt; x &amp;lt; inf&lt;br /&gt;
|-&lt;br /&gt;
|exp_v2&lt;br /&gt;
|&amp;lt;math&amp;gt;\frac{e^{2x} - 1}{e^{2x} + 1}&amp;lt;/math&amp;gt;&lt;br /&gt;
|x &amp;lt; 0: 8.47249e+08 at -1.49012e-08 with 0x0p+0 instead of -0x1p-26&lt;br /&gt;
x &amp;gt; 0: 8.55638e+08 at 2.98023e-08 with 0x0p+0 instead of 0x1.fffffep-26&lt;br /&gt;
| -inf &amp;lt; x &amp;lt; -0.547821&lt;br /&gt;
0.254822 &amp;lt; x &amp;lt; 8.31787&lt;br /&gt;
&lt;br /&gt;
8.38379 &amp;lt; x &amp;lt; 44.3623&lt;br /&gt;
|44.3623 &amp;lt; x &amp;lt; inf&lt;br /&gt;
|-&lt;br /&gt;
|exp_v3&lt;br /&gt;
|&amp;lt;math&amp;gt;1 - \frac{2}{e^{2x} + 1}&amp;lt;/math&amp;gt;&lt;br /&gt;
|x &amp;lt; 0: 8.59832e+08 at -4.47035e-08 with 0x0p+0 instead of -0x1.8p-25&lt;br /&gt;
x &amp;gt; 0: 8.68221e+08 at 8.9407e-08 with 0x0p+0 instead of 0x1.7ffffcp-24&lt;br /&gt;
| -inf &amp;lt; x &amp;lt; -6.26233&lt;br /&gt;
0.346382 &amp;lt; x &amp;lt; inf&lt;br /&gt;
| -&lt;br /&gt;
|-&lt;br /&gt;
|expm1&lt;br /&gt;
|&amp;lt;math&amp;gt;\frac{expm1(2x)}{expm1(2x) + 2}&amp;lt;/math&amp;gt; &lt;br /&gt;
|x &amp;lt; 0: 2.49585 at -3.95627 with -0x1.ffa00ap-1 instead of -0x1.ffa00ep-1&lt;br /&gt;
x &amp;gt; 0: 2.41657 at 0.0155837 with 0x1.fe9b66p-7 instead of 0x1.fe9b62p-7&lt;br /&gt;
| -inf &amp;lt; x &amp;lt; -5.57227&lt;br /&gt;
-0.42981 &amp;lt; x &amp;lt; 0.00777948&lt;br /&gt;
&lt;br /&gt;
0.125351 &amp;lt; x &amp;lt; 44.3623&lt;br /&gt;
|44.3623 &amp;lt; x &amp;lt; inf&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
The first equation is the definition of the hyperbolic tangent function. This equation requires two exponentials with different values. Since exponentials are expensive to compute, we can use the second equation to reduce the number of exponentials to one.&lt;br /&gt;
&lt;br /&gt;
The second equation has the drawback that both the numerator and the denominator can become infinity for large values of x which results in NaN. That happens for &amp;lt;math&amp;gt;|x| &amp;gt; \log(FLT\_MAX)/2 \approx 44&amp;lt;/math&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
The third equation solves this problem but has a slightly higher error for &amp;lt;math&amp;gt;x \approx 2^{-25}&amp;lt;/math&amp;gt;. In the following graph you can see the error chart for equation 3. The error charts for the other two equations are very similar. All three equations have an error of up to 16 mio ULPs for &amp;lt;math&amp;gt;x &amp;lt; 2^{-5}&amp;lt;/math&amp;gt; which means that up to 24 bits could be wrong.&lt;br /&gt;
&lt;br /&gt;
=== exp_v2 ===&lt;br /&gt;
&lt;br /&gt;
The following error graph shows the ULP error of the &amp;lt;code&amp;gt;exp_v2&amp;lt;/code&amp;gt; formulation across the full &amp;lt;code&amp;gt;float32&amp;lt;/code&amp;gt; input range. &lt;br /&gt;
This formulation reduces the number of exponential evaluations to one, but is particularly sensitive to catastrophic cancellation for very small input values and to overflow for large &amp;lt;math&amp;gt;|x|&amp;lt;/math&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
{{#widget:FloatErrorGraph|id=tanhf_exp_v2&lt;br /&gt;
  |data=[ [&amp;quot;-2^127&amp;quot;,0,1.17549e-38],[&amp;quot;-2^126&amp;quot;,0,1.17549e-38],[&amp;quot;-2^125&amp;quot;,0,1.17549e-38],[&amp;quot;-2^124&amp;quot;,0,1.17549e-38],[&amp;quot;-2^123&amp;quot;,0,1.17549e-38],[&amp;quot;-2^122&amp;quot;,0,1.17549e-38],[&amp;quot;-2^121&amp;quot;,0,1.17549e-38],[&amp;quot;-2^120&amp;quot;,0,1.17549e-38],[&amp;quot;-2^119&amp;quot;,0,1.17549e-38],[&amp;quot;-2^118&amp;quot;,0,1.17549e-38],[&amp;quot;-2^117&amp;quot;,0,1.17549e-38],[&amp;quot;-2^116&amp;quot;,0,1.17549e-38],[&amp;quot;-2^115&amp;quot;,0,1.17549e-38],[&amp;quot;-2^114&amp;quot;,0,1.17549e-38],[&amp;quot;-2^113&amp;quot;,0,1.17549e-38],[&amp;quot;-2^112&amp;quot;,0,1.17549e-38],[&amp;quot;-2^111&amp;quot;,0,1.17549e-38],[&amp;quot;-2^110&amp;quot;,0,1.17549e-38],[&amp;quot;-2^109&amp;quot;,0,1.17549e-38],[&amp;quot;-2^108&amp;quot;,0,1.17549e-38],[&amp;quot;-2^107&amp;quot;,0,1.17549e-38],[&amp;quot;-2^106&amp;quot;,0,1.17549e-38],[&amp;quot;-2^105&amp;quot;,0,1.17549e-38],[&amp;quot;-2^104&amp;quot;,0,1.17549e-38],[&amp;quot;-2^103&amp;quot;,0,1.17549e-38],[&amp;quot;-2^102&amp;quot;,0,1.17549e-38],[&amp;quot;-2^101&amp;quot;,0,1.17549e-38],[&amp;quot;-2^100&amp;quot;,0,1.17549e-38],[&amp;quot;-2^99&amp;quot;,0,1.17549e-38],[&amp;quot;-2^98&amp;quot;,0,1.17549e-38],[&amp;quot;-2^97&amp;quot;,0,1.17549e-38],[&amp;quot;-2^96&amp;quot;,0,1.17549e-38],[&amp;quot;-2^95&amp;quot;,0,1.17549e-38],[&amp;quot;-2^94&amp;quot;,0,1.17549e-38],[&amp;quot;-2^93&amp;quot;,0,1.17549e-38],[&amp;quot;-2^92&amp;quot;,0,1.17549e-38],[&amp;quot;-2^91&amp;quot;,0,1.17549e-38],[&amp;quot;-2^90&amp;quot;,0,1.17549e-38],[&amp;quot;-2^89&amp;quot;,0,1.17549e-38],[&amp;quot;-2^88&amp;quot;,0,1.17549e-38],[&amp;quot;-2^87&amp;quot;,0,1.17549e-38],[&amp;quot;-2^86&amp;quot;,0,1.17549e-38],[&amp;quot;-2^85&amp;quot;,0,1.17549e-38],[&amp;quot;-2^84&amp;quot;,0,1.17549e-38],[&amp;quot;-2^83&amp;quot;,0,1.17549e-38],[&amp;quot;-2^82&amp;quot;,0,1.17549e-38],[&amp;quot;-2^81&amp;quot;,0,1.17549e-38],[&amp;quot;-2^80&amp;quot;,0,1.17549e-38],[&amp;quot;-2^79&amp;quot;,0,1.17549e-38],[&amp;quot;-2^78&amp;quot;,0,1.17549e-38],[&amp;quot;-2^77&amp;quot;,0,1.17549e-38],[&amp;quot;-2^76&amp;quot;,0,1.17549e-38],[&amp;quot;-2^75&amp;quot;,0,1.17549e-38],[&amp;quot;-2^74&amp;quot;,0,1.17549e-38],[&amp;quot;-2^73&amp;quot;,0,1.17549e-38],[&amp;quot;-2^72&amp;quot;,0,1.17549e-38],[&amp;quot;-2^71&amp;quot;,0,1.17549e-38],[&amp;quot;-2^70&amp;quot;,0,1.17549e-38],[&amp;quot;-2^69&amp;quot;,0,1.17549e-38],[&amp;quot;-2^68&amp;quot;,0,1.17549e-38],[&amp;quot;-2^67&amp;quot;,0,1.17549e-38],[&amp;quot;-2^66&amp;quot;,0,1.17549e-38],[&amp;quot;-2^65&amp;quot;,0,1.17549e-38],[&amp;quot;-2^64&amp;quot;,0,1.17549e-38],[&amp;quot;-2^63&amp;quot;,0,1.17549e-38],[&amp;quot;-2^62&amp;quot;,0,1.17549e-38],[&amp;quot;-2^61&amp;quot;,0,1.17549e-38],[&amp;quot;-2^60&amp;quot;,0,1.17549e-38],[&amp;quot;-2^59&amp;quot;,0,1.17549e-38],[&amp;quot;-2^58&amp;quot;,0,1.17549e-38],[&amp;quot;-2^57&amp;quot;,0,1.17549e-38],[&amp;quot;-2^56&amp;quot;,0,1.17549e-38],[&amp;quot;-2^55&amp;quot;,0,1.17549e-38],[&amp;quot;-2^54&amp;quot;,0,1.17549e-38],[&amp;quot;-2^53&amp;quot;,0,1.17549e-38],[&amp;quot;-2^52&amp;quot;,0,1.17549e-38],[&amp;quot;-2^51&amp;quot;,0,1.17549e-38],[&amp;quot;-2^50&amp;quot;,0,1.17549e-38],[&amp;quot;-2^49&amp;quot;,0,1.17549e-38],[&amp;quot;-2^48&amp;quot;,0,1.17549e-38],[&amp;quot;-2^47&amp;quot;,0,1.17549e-38],[&amp;quot;-2^46&amp;quot;,0,1.17549e-38],[&amp;quot;-2^45&amp;quot;,0,1.17549e-38],[&amp;quot;-2^44&amp;quot;,0,1.17549e-38],[&amp;quot;-2^43&amp;quot;,0,1.17549e-38],[&amp;quot;-2^42&amp;quot;,0,1.17549e-38],[&amp;quot;-2^41&amp;quot;,0,1.17549e-38],[&amp;quot;-2^40&amp;quot;,0,1.17549e-38],[&amp;quot;-2^39&amp;quot;,0,1.17549e-38],[&amp;quot;-2^38&amp;quot;,0,1.17549e-38],[&amp;quot;-2^37&amp;quot;,0,1.17549e-38],[&amp;quot;-2^36&amp;quot;,0,1.17549e-38],[&amp;quot;-2^35&amp;quot;,0,1.17549e-38],[&amp;quot;-2^34&amp;quot;,0,1.17549e-38],[&amp;quot;-2^33&amp;quot;,0,1.17549e-38],[&amp;quot;-2^32&amp;quot;,0,1.17549e-38],[&amp;quot;-2^31&amp;quot;,0,1.17549e-38],[&amp;quot;-2^30&amp;quot;,0,1.17549e-38],[&amp;quot;-2^29&amp;quot;,0,1.17549e-38],[&amp;quot;-2^28&amp;quot;,0,1.17549e-38],[&amp;quot;-2^27&amp;quot;,0,1.17549e-38],[&amp;quot;-2^26&amp;quot;,0,1.17549e-38],[&amp;quot;-2^25&amp;quot;,0,1.17549e-38],[&amp;quot;-2^24&amp;quot;,0,1.17549e-38],[&amp;quot;-2^23&amp;quot;,0,1.17549e-38],[&amp;quot;-2^22&amp;quot;,0,1.17549e-38],[&amp;quot;-2^21&amp;quot;,0,1.17549e-38],[&amp;quot;-2^20&amp;quot;,0,1.17549e-38],[&amp;quot;-2^19&amp;quot;,0,1.17549e-38],[&amp;quot;-2^18&amp;quot;,0,1.17549e-38],[&amp;quot;-2^17&amp;quot;,0,1.17549e-38],[&amp;quot;-2^16&amp;quot;,0,1.17549e-38],[&amp;quot;-2^15&amp;quot;,0,1.17549e-38],[&amp;quot;-2^14&amp;quot;,0,1.17549e-38],[&amp;quot;-2^13&amp;quot;,0,1.17549e-38],[&amp;quot;-2^12&amp;quot;,0,1.17549e-38],[&amp;quot;-2^11&amp;quot;,0,1.17549e-38],[&amp;quot;-2^10&amp;quot;,0,1.17549e-38],[&amp;quot;-2^9&amp;quot;,0,1.17549e-38],[&amp;quot;-2^8&amp;quot;,0,1.17549e-38],[&amp;quot;-2^7&amp;quot;,0,1.17549e-38],[&amp;quot;-2^6&amp;quot;,0,1.17549e-38],[&amp;quot;-2^5&amp;quot;,0,1.17549e-38],[&amp;quot;-2^4&amp;quot;,2.53131e-14,4.24683e-07],[&amp;quot;-2^3&amp;quot;,5.95888e-08,0.999734],[&amp;quot;-2^2&amp;quot;,8.86696e-08,1.48763],[&amp;quot;-2^1&amp;quot;,8.91609e-08,1.49587],[&amp;quot;-2^0&amp;quot;,8.73743e-08,1.4659],[&amp;quot;-2^-1&amp;quot;,8.65382e-08,2.13222],[&amp;quot;-2^-2&amp;quot;,6.18481e-08,2.61428],[&amp;quot;-2^-3&amp;quot;,3.88071e-08,3.08594],[&amp;quot;-2^-4&amp;quot;,2.6234e-08,4.23594],[&amp;quot;-2^-5&amp;quot;,2.02799e-08,8.19726],[&amp;quot;-2^-6&amp;quot;,1.75183e-08,12.4495],[&amp;quot;-2^-7&amp;quot;,1.61937e-08,17.3878],[&amp;quot;-2^-8&amp;quot;,1.5566e-08,33.4278],[&amp;quot;-2^-9&amp;quot;,1.51688e-08,65.1495],[&amp;quot;-2^-10&amp;quot;,1.50281e-08,129.09],[&amp;quot;-2^-11&amp;quot;,1.49613e-08,257.033],[&amp;quot;-2^-12&amp;quot;,1.49296e-08,512.977],[&amp;quot;-2^-13&amp;quot;,1.49174e-08,1025.12],[&amp;quot;-2^-14&amp;quot;,1.4909e-08,2049.08],[&amp;quot;-2^-15&amp;quot;,1.49049e-08,4097.02],[&amp;quot;-2^-16&amp;quot;,1.4903e-08,8193],[&amp;quot;-2^-17&amp;quot;,1.49021e-08,16385],[&amp;quot;-2^-18&amp;quot;,1.48862e-08,32735],[&amp;quot;-2^-19&amp;quot;,1.48973e-08,65519],[&amp;quot;-2^-20&amp;quot;,1.49001e-08,131063],[&amp;quot;-2^-21&amp;quot;,1.49009e-08,262139],[&amp;quot;-2^-22&amp;quot;,1.49011e-08,524285],[&amp;quot;-2^-23&amp;quot;,1.49011e-08,1.04857e+06],[&amp;quot;-2^-24&amp;quot;,1.49011e-08,2.09715e+06],[&amp;quot;-2^-25&amp;quot;,1.49012e-08,4.1943e+06],[&amp;quot;-2^-26&amp;quot;,1.49007e-08,8.38835e+06],[&amp;quot;-2^-27&amp;quot;,1.49012e-08,8.47249e+08],[&amp;quot;-2^-28&amp;quot;,7.45058e-09,8.38861e+08],[&amp;quot;-2^-29&amp;quot;,3.72529e-09,8.30472e+08],[&amp;quot;-2^-30&amp;quot;,1.86265e-09,8.22084e+08],[&amp;quot;-2^-31&amp;quot;,9.31323e-10,8.13695e+08],[&amp;quot;-2^-32&amp;quot;,4.65661e-10,8.05306e+08],[&amp;quot;-2^-33&amp;quot;,2.32831e-10,7.96918e+08],[&amp;quot;-2^-34&amp;quot;,1.16415e-10,7.88529e+08],[&amp;quot;-2^-35&amp;quot;,5.82077e-11,7.80141e+08],[&amp;quot;-2^-36&amp;quot;,2.91038e-11,7.71752e+08],[&amp;quot;-2^-37&amp;quot;,1.45519e-11,7.63363e+08],[&amp;quot;-2^-38&amp;quot;,7.27596e-12,7.54975e+08],[&amp;quot;-2^-39&amp;quot;,3.63798e-12,7.46586e+08],[&amp;quot;-2^-40&amp;quot;,1.81899e-12,7.38198e+08],[&amp;quot;-2^-41&amp;quot;,9.09495e-13,7.29809e+08],[&amp;quot;-2^-42&amp;quot;,4.54747e-13,7.2142e+08],[&amp;quot;-2^-43&amp;quot;,2.27374e-13,7.13032e+08],[&amp;quot;-2^-44&amp;quot;,1.13687e-13,7.04643e+08],[&amp;quot;-2^-45&amp;quot;,5.68434e-14,6.96254e+08],[&amp;quot;-2^-46&amp;quot;,2.84217e-14,6.87866e+08],[&amp;quot;-2^-47&amp;quot;,1.42109e-14,6.79477e+08],[&amp;quot;-2^-48&amp;quot;,7.10543e-15,6.71089e+08],[&amp;quot;-2^-49&amp;quot;,3.55271e-15,6.627e+08],[&amp;quot;-2^-50&amp;quot;,1.77636e-15,6.54311e+08],[&amp;quot;-2^-51&amp;quot;,8.88178e-16,6.45923e+08],[&amp;quot;-2^-52&amp;quot;,4.44089e-16,6.37534e+08],[&amp;quot;-2^-53&amp;quot;,2.22045e-16,6.29146e+08],[&amp;quot;-2^-54&amp;quot;,1.11022e-16,6.20757e+08],[&amp;quot;-2^-55&amp;quot;,5.55111e-17,6.12368e+08],[&amp;quot;-2^-56&amp;quot;,2.77556e-17,6.0398e+08],[&amp;quot;-2^-57&amp;quot;,1.38778e-17,5.95591e+08],[&amp;quot;-2^-58&amp;quot;,6.93889e-18,5.87203e+08],[&amp;quot;-2^-59&amp;quot;,3.46945e-18,5.78814e+08],[&amp;quot;-2^-60&amp;quot;,1.73472e-18,5.70425e+08],[&amp;quot;-2^-61&amp;quot;,8.67362e-19,5.62037e+08],[&amp;quot;-2^-62&amp;quot;,4.33681e-19,5.53648e+08],[&amp;quot;-2^-63&amp;quot;,2.1684e-19,5.4526e+08],[&amp;quot;-2^-64&amp;quot;,1.0842e-19,5.36871e+08],[&amp;quot;-2^-65&amp;quot;,5.42101e-20,5.28482e+08],[&amp;quot;-2^-66&amp;quot;,2.71051e-20,5.20094e+08],[&amp;quot;-2^-67&amp;quot;,1.35525e-20,5.11705e+08],[&amp;quot;-2^-68&amp;quot;,6.77626e-21,5.03316e+08],[&amp;quot;-2^-69&amp;quot;,3.38813e-21,4.94928e+08],[&amp;quot;-2^-70&amp;quot;,1.69407e-21,4.86539e+08],[&amp;quot;-2^-71&amp;quot;,8.47033e-22,4.78151e+08],[&amp;quot;-2^-72&amp;quot;,4.23516e-22,4.69762e+08],[&amp;quot;-2^-73&amp;quot;,2.11758e-22,4.61373e+08],[&amp;quot;-2^-74&amp;quot;,1.05879e-22,4.52985e+08],[&amp;quot;-2^-75&amp;quot;,5.29396e-23,4.44596e+08],[&amp;quot;-2^-76&amp;quot;,2.64698e-23,4.36208e+08],[&amp;quot;-2^-77&amp;quot;,1.32349e-23,4.27819e+08],[&amp;quot;-2^-78&amp;quot;,6.61744e-24,4.1943e+08],[&amp;quot;-2^-79&amp;quot;,3.30872e-24,4.11042e+08],[&amp;quot;-2^-80&amp;quot;,1.65436e-24,4.02653e+08],[&amp;quot;-2^-81&amp;quot;,8.27181e-25,3.94265e+08],[&amp;quot;-2^-82&amp;quot;,4.1359e-25,3.85876e+08],[&amp;quot;-2^-83&amp;quot;,2.06795e-25,3.77487e+08],[&amp;quot;-2^-84&amp;quot;,1.03398e-25,3.69099e+08],[&amp;quot;-2^-85&amp;quot;,5.16988e-26,3.6071e+08],[&amp;quot;-2^-86&amp;quot;,2.58494e-26,3.52322e+08],[&amp;quot;-2^-87&amp;quot;,1.29247e-26,3.43933e+08],[&amp;quot;-2^-88&amp;quot;,6.46235e-27,3.35544e+08],[&amp;quot;-2^-89&amp;quot;,3.23117e-27,3.27156e+08],[&amp;quot;-2^-90&amp;quot;,1.61559e-27,3.18767e+08],[&amp;quot;-2^-91&amp;quot;,8.07794e-28,3.10378e+08],[&amp;quot;-2^-92&amp;quot;,4.03897e-28,3.0199e+08],[&amp;quot;-2^-93&amp;quot;,2.01948e-28,2.93601e+08],[&amp;quot;-2^-94&amp;quot;,1.00974e-28,2.85213e+08],[&amp;quot;-2^-95&amp;quot;,5.04871e-29,2.76824e+08],[&amp;quot;-2^-96&amp;quot;,2.52435e-29,2.68435e+08],[&amp;quot;-2^-97&amp;quot;,1.26218e-29,2.60047e+08],[&amp;quot;-2^-98&amp;quot;,6.31089e-30,2.51658e+08],[&amp;quot;-2^-99&amp;quot;,3.15544e-30,2.4327e+08],[&amp;quot;-2^-100&amp;quot;,1.57772e-30,2.34881e+08],[&amp;quot;-2^-101&amp;quot;,7.88861e-31,2.26492e+08],[&amp;quot;-2^-102&amp;quot;,3.9443e-31,2.18104e+08],[&amp;quot;-2^-103&amp;quot;,1.97215e-31,2.09715e+08],[&amp;quot;-2^-104&amp;quot;,9.86076e-32,2.01327e+08],[&amp;quot;-2^-105&amp;quot;,4.93038e-32,1.92938e+08],[&amp;quot;-2^-106&amp;quot;,2.46519e-32,1.84549e+08],[&amp;quot;-2^-107&amp;quot;,1.2326e-32,1.76161e+08],[&amp;quot;-2^-108&amp;quot;,6.16298e-33,1.67772e+08],[&amp;quot;-2^-109&amp;quot;,3.08149e-33,1.59384e+08],[&amp;quot;-2^-110&amp;quot;,1.54074e-33,1.50995e+08],[&amp;quot;-2^-111&amp;quot;,7.70372e-34,1.42606e+08],[&amp;quot;-2^-112&amp;quot;,3.85186e-34,1.34218e+08],[&amp;quot;-2^-113&amp;quot;,1.92593e-34,1.25829e+08],[&amp;quot;-2^-114&amp;quot;,9.62965e-35,1.17441e+08],[&amp;quot;-2^-115&amp;quot;,4.81482e-35,1.09052e+08],[&amp;quot;-2^-116&amp;quot;,2.40741e-35,1.00663e+08],[&amp;quot;-2^-117&amp;quot;,1.20371e-35,9.22747e+07],[&amp;quot;-2^-118&amp;quot;,6.01853e-36,8.38861e+07],[&amp;quot;-2^-119&amp;quot;,3.00927e-36,7.54975e+07],[&amp;quot;-2^-120&amp;quot;,1.50463e-36,6.71089e+07],[&amp;quot;-2^-121&amp;quot;,7.52316e-37,5.87203e+07],[&amp;quot;-2^-122&amp;quot;,3.76158e-37,5.03316e+07],[&amp;quot;-2^-123&amp;quot;,1.88079e-37,4.1943e+07],[&amp;quot;-2^-124&amp;quot;,9.40395e-38,3.35544e+07],[&amp;quot;-2^-125&amp;quot;,4.70198e-38,2.51658e+07],[&amp;quot;-2^-126&amp;quot;,2.35099e-38,1.67772e+07],[&amp;quot;-SN&amp;quot;,1.17546e-38,8.38835e+06],[&amp;quot;0&amp;quot;,0,1.17549e-38],[&amp;quot;+SN&amp;quot;,1.17546e-38,8.38835e+06],[&amp;quot;+2^-126&amp;quot;,2.35095e-38,1.6777e+07],[&amp;quot;+2^-125&amp;quot;,4.70191e-38,2.51656e+07],[&amp;quot;+2^-124&amp;quot;,9.40381e-38,3.35542e+07],[&amp;quot;+2^-123&amp;quot;,1.88076e-37,4.19428e+07],[&amp;quot;+2^-122&amp;quot;,3.76152e-37,5.03314e+07],[&amp;quot;+2^-121&amp;quot;,7.52305e-37,5.872e+07],[&amp;quot;+2^-120&amp;quot;,1.50461e-36,6.71086e+07],[&amp;quot;+2^-119&amp;quot;,3.00922e-36,7.54972e+07],[&amp;quot;+2^-118&amp;quot;,6.01844e-36,8.38858e+07],[&amp;quot;+2^-117&amp;quot;,1.20369e-35,9.22744e+07],[&amp;quot;+2^-116&amp;quot;,2.40738e-35,1.00663e+08],[&amp;quot;+2^-115&amp;quot;,4.81475e-35,1.09052e+08],[&amp;quot;+2^-114&amp;quot;,9.6295e-35,1.1744e+08],[&amp;quot;+2^-113&amp;quot;,1.9259e-34,1.25829e+08],[&amp;quot;+2^-112&amp;quot;,3.8518e-34,1.34217e+08],[&amp;quot;+2^-111&amp;quot;,7.7036e-34,1.42606e+08],[&amp;quot;+2^-110&amp;quot;,1.54072e-33,1.50995e+08],[&amp;quot;+2^-109&amp;quot;,3.08144e-33,1.59383e+08],[&amp;quot;+2^-108&amp;quot;,6.16288e-33,1.67772e+08],[&amp;quot;+2^-107&amp;quot;,1.23258e-32,1.76161e+08],[&amp;quot;+2^-106&amp;quot;,2.46515e-32,1.84549e+08],[&amp;quot;+2^-105&amp;quot;,4.93031e-32,1.92938e+08],[&amp;quot;+2^-104&amp;quot;,9.86061e-32,2.01326e+08],[&amp;quot;+2^-103&amp;quot;,1.97212e-31,2.09715e+08],[&amp;quot;+2^-102&amp;quot;,3.94424e-31,2.18104e+08],[&amp;quot;+2^-101&amp;quot;,7.88849e-31,2.26492e+08],[&amp;quot;+2^-100&amp;quot;,1.5777e-30,2.34881e+08],[&amp;quot;+2^-99&amp;quot;,3.1554e-30,2.43269e+08],[&amp;quot;+2^-98&amp;quot;,6.31079e-30,2.51658e+08],[&amp;quot;+2^-97&amp;quot;,1.26216e-29,2.60047e+08],[&amp;quot;+2^-96&amp;quot;,2.52432e-29,2.68435e+08],[&amp;quot;+2^-95&amp;quot;,5.04863e-29,2.76824e+08],[&amp;quot;+2^-94&amp;quot;,1.00973e-28,2.85212e+08],[&amp;quot;+2^-93&amp;quot;,2.01945e-28,2.93601e+08],[&amp;quot;+2^-92&amp;quot;,4.03891e-28,3.0199e+08],[&amp;quot;+2^-91&amp;quot;,8.07781e-28,3.10378e+08],[&amp;quot;+2^-90&amp;quot;,1.61556e-27,3.18767e+08],[&amp;quot;+2^-89&amp;quot;,3.23112e-27,3.27155e+08],[&amp;quot;+2^-88&amp;quot;,6.46225e-27,3.35544e+08],[&amp;quot;+2^-87&amp;quot;,1.29245e-26,3.43933e+08],[&amp;quot;+2^-86&amp;quot;,2.5849e-26,3.52321e+08],[&amp;quot;+2^-85&amp;quot;,5.1698e-26,3.6071e+08],[&amp;quot;+2^-84&amp;quot;,1.03396e-25,3.69098e+08],[&amp;quot;+2^-83&amp;quot;,2.06792e-25,3.77487e+08],[&amp;quot;+2^-82&amp;quot;,4.13584e-25,3.85876e+08],[&amp;quot;+2^-81&amp;quot;,8.27168e-25,3.94264e+08],[&amp;quot;+2^-80&amp;quot;,1.65434e-24,4.02653e+08],[&amp;quot;+2^-79&amp;quot;,3.30867e-24,4.11042e+08],[&amp;quot;+2^-78&amp;quot;,6.61734e-24,4.1943e+08],[&amp;quot;+2^-77&amp;quot;,1.32347e-23,4.27819e+08],[&amp;quot;+2^-76&amp;quot;,2.64694e-23,4.36207e+08],[&amp;quot;+2^-75&amp;quot;,5.29388e-23,4.44596e+08],[&amp;quot;+2^-74&amp;quot;,1.05878e-22,4.52985e+08],[&amp;quot;+2^-73&amp;quot;,2.11755e-22,4.61373e+08],[&amp;quot;+2^-72&amp;quot;,4.2351e-22,4.69762e+08],[&amp;quot;+2^-71&amp;quot;,8.4702e-22,4.7815e+08],[&amp;quot;+2^-70&amp;quot;,1.69404e-21,4.86539e+08],[&amp;quot;+2^-69&amp;quot;,3.38808e-21,4.94928e+08],[&amp;quot;+2^-68&amp;quot;,6.77616e-21,5.03316e+08],[&amp;quot;+2^-67&amp;quot;,1.35523e-20,5.11705e+08],[&amp;quot;+2^-66&amp;quot;,2.71046e-20,5.20093e+08],[&amp;quot;+2^-65&amp;quot;,5.42093e-20,5.28482e+08],[&amp;quot;+2^-64&amp;quot;,1.08419e-19,5.36871e+08],[&amp;quot;+2^-63&amp;quot;,2.16837e-19,5.45259e+08],[&amp;quot;+2^-62&amp;quot;,4.33674e-19,5.53648e+08],[&amp;quot;+2^-61&amp;quot;,8.67349e-19,5.62036e+08],[&amp;quot;+2^-60&amp;quot;,1.7347e-18,5.70425e+08],[&amp;quot;+2^-59&amp;quot;,3.46939e-18,5.78814e+08],[&amp;quot;+2^-58&amp;quot;,6.93879e-18,5.87202e+08],[&amp;quot;+2^-57&amp;quot;,1.38776e-17,5.95591e+08],[&amp;quot;+2^-56&amp;quot;,2.77552e-17,6.0398e+08],[&amp;quot;+2^-55&amp;quot;,5.55103e-17,6.12368e+08],[&amp;quot;+2^-54&amp;quot;,1.11021e-16,6.20757e+08],[&amp;quot;+2^-53&amp;quot;,2.22041e-16,6.29145e+08],[&amp;quot;+2^-52&amp;quot;,4.44082e-16,6.37534e+08],[&amp;quot;+2^-51&amp;quot;,8.88165e-16,6.45923e+08],[&amp;quot;+2^-50&amp;quot;,1.77633e-15,6.54311e+08],[&amp;quot;+2^-49&amp;quot;,3.55266e-15,6.627e+08],[&amp;quot;+2^-48&amp;quot;,7.10532e-15,6.71088e+08],[&amp;quot;+2^-47&amp;quot;,1.42106e-14,6.79477e+08],[&amp;quot;+2^-46&amp;quot;,2.84213e-14,6.87866e+08],[&amp;quot;+2^-45&amp;quot;,5.68426e-14,6.96254e+08],[&amp;quot;+2^-44&amp;quot;,1.13685e-13,7.04643e+08],[&amp;quot;+2^-43&amp;quot;,2.2737e-13,7.13031e+08],[&amp;quot;+2^-42&amp;quot;,4.5474e-13,7.2142e+08],[&amp;quot;+2^-41&amp;quot;,9.09481e-13,7.29809e+08],[&amp;quot;+2^-40&amp;quot;,1.81896e-12,7.38197e+08],[&amp;quot;+2^-39&amp;quot;,3.63792e-12,7.46586e+08],[&amp;quot;+2^-38&amp;quot;,7.27585e-12,7.54974e+08],[&amp;quot;+2^-37&amp;quot;,1.45517e-11,7.63363e+08],[&amp;quot;+2^-36&amp;quot;,2.91034e-11,7.71752e+08],[&amp;quot;+2^-35&amp;quot;,5.82068e-11,7.8014e+08],[&amp;quot;+2^-34&amp;quot;,1.16414e-10,7.88529e+08],[&amp;quot;+2^-33&amp;quot;,2.32827e-10,7.96918e+08],[&amp;quot;+2^-32&amp;quot;,4.65654e-10,8.05306e+08],[&amp;quot;+2^-31&amp;quot;,9.31308e-10,8.13695e+08],[&amp;quot;+2^-30&amp;quot;,1.86262e-09,8.22083e+08],[&amp;quot;+2^-29&amp;quot;,3.72523e-09,8.30472e+08],[&amp;quot;+2^-28&amp;quot;,7.45047e-09,8.38861e+08],[&amp;quot;+2^-27&amp;quot;,1.49009e-08,8.47249e+08],[&amp;quot;+2^-26&amp;quot;,2.98019e-08,8.55638e+08],[&amp;quot;+2^-25&amp;quot;,2.98023e-08,8.38861e+06],[&amp;quot;+2^-24&amp;quot;,2.98023e-08,4.1943e+06],[&amp;quot;+2^-23&amp;quot;,2.98023e-08,2.09715e+06],[&amp;quot;+2^-22&amp;quot;,2.98023e-08,1.04857e+06],[&amp;quot;+2^-21&amp;quot;,2.98021e-08,524284],[&amp;quot;+2^-20&amp;quot;,2.98014e-08,262136],[&amp;quot;+2^-19&amp;quot;,2.97987e-08,131056],[&amp;quot;+2^-18&amp;quot;,2.97878e-08,65504],[&amp;quot;+2^-17&amp;quot;,2.98014e-08,32767],[&amp;quot;+2^-16&amp;quot;,2.98005e-08,16384],[&amp;quot;+2^-15&amp;quot;,2.98023e-08,8192],[&amp;quot;+2^-14&amp;quot;,2.98025e-08,4351.99],[&amp;quot;+2^-13&amp;quot;,2.98044e-08,2815.96],[&amp;quot;+2^-12&amp;quot;,2.98091e-08,1024.23],[&amp;quot;+2^-11&amp;quot;,2.9812e-08,512.167],[&amp;quot;+2^-10&amp;quot;,2.98154e-08,256.113],[&amp;quot;+2^-9&amp;quot;,2.98591e-08,128.244],[&amp;quot;+2^-8&amp;quot;,3.00204e-08,85.3328],[&amp;quot;+2^-7&amp;quot;,3.01947e-08,43.675],[&amp;quot;+2^-6&amp;quot;,3.05713e-08,30.45],[&amp;quot;+2^-5&amp;quot;,3.14319e-08,8.43743],[&amp;quot;+2^-4&amp;quot;,3.31291e-08,7.77058],[&amp;quot;+2^-3&amp;quot;,3.58597e-08,3.99659],[&amp;quot;+2^-2&amp;quot;,4.14639e-08,2.29246],[&amp;quot;+2^-1&amp;quot;,8.02409e-08,1.34622],[&amp;quot;+2^0&amp;quot;,8.66201e-08,1.45324],[&amp;quot;+2^1&amp;quot;,8.8654e-08,1.48737],[&amp;quot;+2^2&amp;quot;,6.7976e-08,1.14045],[&amp;quot;+2^3&amp;quot;,1.33955e-07,2.2474],[&amp;quot;+2^4&amp;quot;,2.53131e-14,4.24683e-07],[&amp;quot;+2^5&amp;quot;,0,null],[&amp;quot;+2^6&amp;quot;,0,null],[&amp;quot;+2^7&amp;quot;,0,null],[&amp;quot;+2^8&amp;quot;,0,null],[&amp;quot;+2^9&amp;quot;,0,null],[&amp;quot;+2^10&amp;quot;,0,null],[&amp;quot;+2^11&amp;quot;,0,null],[&amp;quot;+2^12&amp;quot;,0,null],[&amp;quot;+2^13&amp;quot;,0,null],[&amp;quot;+2^14&amp;quot;,0,null],[&amp;quot;+2^15&amp;quot;,0,null],[&amp;quot;+2^16&amp;quot;,0,null],[&amp;quot;+2^17&amp;quot;,0,null],[&amp;quot;+2^18&amp;quot;,0,null],[&amp;quot;+2^19&amp;quot;,0,null],[&amp;quot;+2^20&amp;quot;,0,null],[&amp;quot;+2^21&amp;quot;,0,null],[&amp;quot;+2^22&amp;quot;,0,null],[&amp;quot;+2^23&amp;quot;,0,null],[&amp;quot;+2^24&amp;quot;,0,null],[&amp;quot;+2^25&amp;quot;,0,null],[&amp;quot;+2^26&amp;quot;,0,null],[&amp;quot;+2^27&amp;quot;,0,null],[&amp;quot;+2^28&amp;quot;,0,null],[&amp;quot;+2^29&amp;quot;,0,null],[&amp;quot;+2^30&amp;quot;,0,null],[&amp;quot;+2^31&amp;quot;,0,null],[&amp;quot;+2^32&amp;quot;,0,null],[&amp;quot;+2^33&amp;quot;,0,null],[&amp;quot;+2^34&amp;quot;,0,null],[&amp;quot;+2^35&amp;quot;,0,null],[&amp;quot;+2^36&amp;quot;,0,null],[&amp;quot;+2^37&amp;quot;,0,null],[&amp;quot;+2^38&amp;quot;,0,null],[&amp;quot;+2^39&amp;quot;,0,null],[&amp;quot;+2^40&amp;quot;,0,null],[&amp;quot;+2^41&amp;quot;,0,null],[&amp;quot;+2^42&amp;quot;,0,null],[&amp;quot;+2^43&amp;quot;,0,null],[&amp;quot;+2^44&amp;quot;,0,null],[&amp;quot;+2^45&amp;quot;,0,null],[&amp;quot;+2^46&amp;quot;,0,null],[&amp;quot;+2^47&amp;quot;,0,null],[&amp;quot;+2^48&amp;quot;,0,null],[&amp;quot;+2^49&amp;quot;,0,null],[&amp;quot;+2^50&amp;quot;,0,null],[&amp;quot;+2^51&amp;quot;,0,null],[&amp;quot;+2^52&amp;quot;,0,null],[&amp;quot;+2^53&amp;quot;,0,null],[&amp;quot;+2^54&amp;quot;,0,null],[&amp;quot;+2^55&amp;quot;,0,null],[&amp;quot;+2^56&amp;quot;,0,null],[&amp;quot;+2^57&amp;quot;,0,null],[&amp;quot;+2^58&amp;quot;,0,null],[&amp;quot;+2^59&amp;quot;,0,null],[&amp;quot;+2^60&amp;quot;,0,null],[&amp;quot;+2^61&amp;quot;,0,null],[&amp;quot;+2^62&amp;quot;,0,null],[&amp;quot;+2^63&amp;quot;,0,null],[&amp;quot;+2^64&amp;quot;,0,null],[&amp;quot;+2^65&amp;quot;,0,null],[&amp;quot;+2^66&amp;quot;,0,null],[&amp;quot;+2^67&amp;quot;,0,null],[&amp;quot;+2^68&amp;quot;,0,null],[&amp;quot;+2^69&amp;quot;,0,null],[&amp;quot;+2^70&amp;quot;,0,null],[&amp;quot;+2^71&amp;quot;,0,null],[&amp;quot;+2^72&amp;quot;,0,null],[&amp;quot;+2^73&amp;quot;,0,null],[&amp;quot;+2^74&amp;quot;,0,null],[&amp;quot;+2^75&amp;quot;,0,null],[&amp;quot;+2^76&amp;quot;,0,null],[&amp;quot;+2^77&amp;quot;,0,null],[&amp;quot;+2^78&amp;quot;,0,null],[&amp;quot;+2^79&amp;quot;,0,null],[&amp;quot;+2^80&amp;quot;,0,null],[&amp;quot;+2^81&amp;quot;,0,null],[&amp;quot;+2^82&amp;quot;,0,null],[&amp;quot;+2^83&amp;quot;,0,null],[&amp;quot;+2^84&amp;quot;,0,null],[&amp;quot;+2^85&amp;quot;,0,null],[&amp;quot;+2^86&amp;quot;,0,null],[&amp;quot;+2^87&amp;quot;,0,null],[&amp;quot;+2^88&amp;quot;,0,null],[&amp;quot;+2^89&amp;quot;,0,null],[&amp;quot;+2^90&amp;quot;,0,null],[&amp;quot;+2^91&amp;quot;,0,null],[&amp;quot;+2^92&amp;quot;,0,null],[&amp;quot;+2^93&amp;quot;,0,null],[&amp;quot;+2^94&amp;quot;,0,null],[&amp;quot;+2^95&amp;quot;,0,null],[&amp;quot;+2^96&amp;quot;,0,null],[&amp;quot;+2^97&amp;quot;,0,null],[&amp;quot;+2^98&amp;quot;,0,null],[&amp;quot;+2^99&amp;quot;,0,null],[&amp;quot;+2^100&amp;quot;,0,null],[&amp;quot;+2^101&amp;quot;,0,null],[&amp;quot;+2^102&amp;quot;,0,null],[&amp;quot;+2^103&amp;quot;,0,null],[&amp;quot;+2^104&amp;quot;,0,null],[&amp;quot;+2^105&amp;quot;,0,null],[&amp;quot;+2^106&amp;quot;,0,null],[&amp;quot;+2^107&amp;quot;,0,null],[&amp;quot;+2^108&amp;quot;,0,null],[&amp;quot;+2^109&amp;quot;,0,null],[&amp;quot;+2^110&amp;quot;,0,null],[&amp;quot;+2^111&amp;quot;,0,null],[&amp;quot;+2^112&amp;quot;,0,null],[&amp;quot;+2^113&amp;quot;,0,null],[&amp;quot;+2^114&amp;quot;,0,null],[&amp;quot;+2^115&amp;quot;,0,null],[&amp;quot;+2^116&amp;quot;,0,null],[&amp;quot;+2^117&amp;quot;,0,null],[&amp;quot;+2^118&amp;quot;,0,null],[&amp;quot;+2^119&amp;quot;,0,null],[&amp;quot;+2^120&amp;quot;,0,null],[&amp;quot;+2^121&amp;quot;,0,null],[&amp;quot;+2^122&amp;quot;,0,null],[&amp;quot;+2^123&amp;quot;,0,null],[&amp;quot;+2^124&amp;quot;,0,null],[&amp;quot;+2^125&amp;quot;,0,null],[&amp;quot;+2^126&amp;quot;,0,null],[&amp;quot;+2^127&amp;quot;,0,null] ]&lt;br /&gt;
 }}&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Key observation:&#039;&#039;&#039;  &lt;br /&gt;
For &amp;lt;math&amp;gt;|x| \ll 1&amp;lt;/math&amp;gt;, the ULP error grows rapidly and reaches extremely large values due to loss of significance. &lt;br /&gt;
For sufficiently large &amp;lt;math&amp;gt;x&amp;lt;/math&amp;gt;, both numerator and denominator overflow, leading to NaN results.&lt;br /&gt;
&lt;br /&gt;
=== expm1 ===&lt;br /&gt;
&lt;br /&gt;
This graph visualizes the ULP error of the &amp;lt;code&amp;gt;expm1&amp;lt;/code&amp;gt;-based formulation.&lt;br /&gt;
Using &amp;lt;code&amp;gt;expm1&amp;lt;/code&amp;gt; significantly improves numerical accuracy for small input values by avoiding catastrophic cancellation in the expression &amp;lt;math&amp;gt;e^{2x} - 1&amp;lt;/math&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
{{#widget:FloatErrorGraph|id=tanhf_expm1&lt;br /&gt;
  |data=[ [&amp;quot;-2^127&amp;quot;,0,1.17549e-38],[&amp;quot;-2^126&amp;quot;,0,1.17549e-38],[&amp;quot;-2^125&amp;quot;,0,1.17549e-38],[&amp;quot;-2^124&amp;quot;,0,1.17549e-38],[&amp;quot;-2^123&amp;quot;,0,1.17549e-38],[&amp;quot;-2^122&amp;quot;,0,1.17549e-38],[&amp;quot;-2^121&amp;quot;,0,1.17549e-38],[&amp;quot;-2^120&amp;quot;,0,1.17549e-38],[&amp;quot;-2^119&amp;quot;,0,1.17549e-38],[&amp;quot;-2^118&amp;quot;,0,1.17549e-38],[&amp;quot;-2^117&amp;quot;,0,1.17549e-38],[&amp;quot;-2^116&amp;quot;,0,1.17549e-38],[&amp;quot;-2^115&amp;quot;,0,1.17549e-38],[&amp;quot;-2^114&amp;quot;,0,1.17549e-38],[&amp;quot;-2^113&amp;quot;,0,1.17549e-38],[&amp;quot;-2^112&amp;quot;,0,1.17549e-38],[&amp;quot;-2^111&amp;quot;,0,1.17549e-38],[&amp;quot;-2^110&amp;quot;,0,1.17549e-38],[&amp;quot;-2^109&amp;quot;,0,1.17549e-38],[&amp;quot;-2^108&amp;quot;,0,1.17549e-38],[&amp;quot;-2^107&amp;quot;,0,1.17549e-38],[&amp;quot;-2^106&amp;quot;,0,1.17549e-38],[&amp;quot;-2^105&amp;quot;,0,1.17549e-38],[&amp;quot;-2^104&amp;quot;,0,1.17549e-38],[&amp;quot;-2^103&amp;quot;,0,1.17549e-38],[&amp;quot;-2^102&amp;quot;,0,1.17549e-38],[&amp;quot;-2^101&amp;quot;,0,1.17549e-38],[&amp;quot;-2^100&amp;quot;,0,1.17549e-38],[&amp;quot;-2^99&amp;quot;,0,1.17549e-38],[&amp;quot;-2^98&amp;quot;,0,1.17549e-38],[&amp;quot;-2^97&amp;quot;,0,1.17549e-38],[&amp;quot;-2^96&amp;quot;,0,1.17549e-38],[&amp;quot;-2^95&amp;quot;,0,1.17549e-38],[&amp;quot;-2^94&amp;quot;,0,1.17549e-38],[&amp;quot;-2^93&amp;quot;,0,1.17549e-38],[&amp;quot;-2^92&amp;quot;,0,1.17549e-38],[&amp;quot;-2^91&amp;quot;,0,1.17549e-38],[&amp;quot;-2^90&amp;quot;,0,1.17549e-38],[&amp;quot;-2^89&amp;quot;,0,1.17549e-38],[&amp;quot;-2^88&amp;quot;,0,1.17549e-38],[&amp;quot;-2^87&amp;quot;,0,1.17549e-38],[&amp;quot;-2^86&amp;quot;,0,1.17549e-38],[&amp;quot;-2^85&amp;quot;,0,1.17549e-38],[&amp;quot;-2^84&amp;quot;,0,1.17549e-38],[&amp;quot;-2^83&amp;quot;,0,1.17549e-38],[&amp;quot;-2^82&amp;quot;,0,1.17549e-38],[&amp;quot;-2^81&amp;quot;,0,1.17549e-38],[&amp;quot;-2^80&amp;quot;,0,1.17549e-38],[&amp;quot;-2^79&amp;quot;,0,1.17549e-38],[&amp;quot;-2^78&amp;quot;,0,1.17549e-38],[&amp;quot;-2^77&amp;quot;,0,1.17549e-38],[&amp;quot;-2^76&amp;quot;,0,1.17549e-38],[&amp;quot;-2^75&amp;quot;,0,1.17549e-38],[&amp;quot;-2^74&amp;quot;,0,1.17549e-38],[&amp;quot;-2^73&amp;quot;,0,1.17549e-38],[&amp;quot;-2^72&amp;quot;,0,1.17549e-38],[&amp;quot;-2^71&amp;quot;,0,1.17549e-38],[&amp;quot;-2^70&amp;quot;,0,1.17549e-38],[&amp;quot;-2^69&amp;quot;,0,1.17549e-38],[&amp;quot;-2^68&amp;quot;,0,1.17549e-38],[&amp;quot;-2^67&amp;quot;,0,1.17549e-38],[&amp;quot;-2^66&amp;quot;,0,1.17549e-38],[&amp;quot;-2^65&amp;quot;,0,1.17549e-38],[&amp;quot;-2^64&amp;quot;,0,1.17549e-38],[&amp;quot;-2^63&amp;quot;,0,1.17549e-38],[&amp;quot;-2^62&amp;quot;,0,1.17549e-38],[&amp;quot;-2^61&amp;quot;,0,1.17549e-38],[&amp;quot;-2^60&amp;quot;,0,1.17549e-38],[&amp;quot;-2^59&amp;quot;,0,1.17549e-38],[&amp;quot;-2^58&amp;quot;,0,1.17549e-38],[&amp;quot;-2^57&amp;quot;,0,1.17549e-38],[&amp;quot;-2^56&amp;quot;,0,1.17549e-38],[&amp;quot;-2^55&amp;quot;,0,1.17549e-38],[&amp;quot;-2^54&amp;quot;,0,1.17549e-38],[&amp;quot;-2^53&amp;quot;,0,1.17549e-38],[&amp;quot;-2^52&amp;quot;,0,1.17549e-38],[&amp;quot;-2^51&amp;quot;,0,1.17549e-38],[&amp;quot;-2^50&amp;quot;,0,1.17549e-38],[&amp;quot;-2^49&amp;quot;,0,1.17549e-38],[&amp;quot;-2^48&amp;quot;,0,1.17549e-38],[&amp;quot;-2^47&amp;quot;,0,1.17549e-38],[&amp;quot;-2^46&amp;quot;,0,1.17549e-38],[&amp;quot;-2^45&amp;quot;,0,1.17549e-38],[&amp;quot;-2^44&amp;quot;,0,1.17549e-38],[&amp;quot;-2^43&amp;quot;,0,1.17549e-38],[&amp;quot;-2^42&amp;quot;,0,1.17549e-38],[&amp;quot;-2^41&amp;quot;,0,1.17549e-38],[&amp;quot;-2^40&amp;quot;,0,1.17549e-38],[&amp;quot;-2^39&amp;quot;,0,1.17549e-38],[&amp;quot;-2^38&amp;quot;,0,1.17549e-38],[&amp;quot;-2^37&amp;quot;,0,1.17549e-38],[&amp;quot;-2^36&amp;quot;,0,1.17549e-38],[&amp;quot;-2^35&amp;quot;,0,1.17549e-38],[&amp;quot;-2^34&amp;quot;,0,1.17549e-38],[&amp;quot;-2^33&amp;quot;,0,1.17549e-38],[&amp;quot;-2^32&amp;quot;,0,1.17549e-38],[&amp;quot;-2^31&amp;quot;,0,1.17549e-38],[&amp;quot;-2^30&amp;quot;,0,1.17549e-38],[&amp;quot;-2^29&amp;quot;,0,1.17549e-38],[&amp;quot;-2^28&amp;quot;,0,1.17549e-38],[&amp;quot;-2^27&amp;quot;,0,1.17549e-38],[&amp;quot;-2^26&amp;quot;,0,1.17549e-38],[&amp;quot;-2^25&amp;quot;,0,1.17549e-38],[&amp;quot;-2^24&amp;quot;,0,1.17549e-38],[&amp;quot;-2^23&amp;quot;,0,1.17549e-38],[&amp;quot;-2^22&amp;quot;,0,1.17549e-38],[&amp;quot;-2^21&amp;quot;,0,1.17549e-38],[&amp;quot;-2^20&amp;quot;,0,1.17549e-38],[&amp;quot;-2^19&amp;quot;,0,1.17549e-38],[&amp;quot;-2^18&amp;quot;,0,1.17549e-38],[&amp;quot;-2^17&amp;quot;,0,1.17549e-38],[&amp;quot;-2^16&amp;quot;,0,1.17549e-38],[&amp;quot;-2^15&amp;quot;,0,1.17549e-38],[&amp;quot;-2^14&amp;quot;,0,1.17549e-38],[&amp;quot;-2^13&amp;quot;,0,1.17549e-38],[&amp;quot;-2^12&amp;quot;,0,1.17549e-38],[&amp;quot;-2^11&amp;quot;,0,1.17549e-38],[&amp;quot;-2^10&amp;quot;,0,1.17549e-38],[&amp;quot;-2^9&amp;quot;,0,1.17549e-38],[&amp;quot;-2^8&amp;quot;,0,1.17549e-38],[&amp;quot;-2^7&amp;quot;,0,1.17549e-38],[&amp;quot;-2^6&amp;quot;,0,1.17549e-38],[&amp;quot;-2^5&amp;quot;,0,1.17549e-38],[&amp;quot;-2^4&amp;quot;,2.53131e-14,4.24683e-07],[&amp;quot;-2^3&amp;quot;,1.19138e-07,1.9988],[&amp;quot;-2^2&amp;quot;,1.46791e-07,2.46274],[&amp;quot;-2^1&amp;quot;,1.47723e-07,2.47839],[&amp;quot;-2^0&amp;quot;,1.40758e-07,2.36153],[&amp;quot;-2^-1&amp;quot;,1.13422e-07,2.23445],[&amp;quot;-2^-2&amp;quot;,6.38012e-08,2.14081],[&amp;quot;-2^-3&amp;quot;,2.69538e-08,1.80884],[&amp;quot;-2^-4&amp;quot;,1.21308e-08,1.62817],[&amp;quot;-2^-5&amp;quot;,5.59535e-09,1.50199],[&amp;quot;-2^-6&amp;quot;,2.69177e-09,1.44513],[&amp;quot;-2^-7&amp;quot;,1.31289e-09,1.4097],[&amp;quot;-2^-8&amp;quot;,6.49688e-10,1.3952],[&amp;quot;-2^-9&amp;quot;,3.32043e-10,1.42611],[&amp;quot;-2^-10&amp;quot;,1.66275e-10,1.4283],[&amp;quot;-2^-11&amp;quot;,8.08595e-11,1.38916],[&amp;quot;-2^-12&amp;quot;,3.8939e-11,1.33794],[&amp;quot;-2^-13&amp;quot;,1.9401e-11,1.33323],[&amp;quot;-2^-14&amp;quot;,7.88212e-12,1.08331],[&amp;quot;-2^-15&amp;quot;,3.71372e-12,1.02082],[&amp;quot;-2^-16&amp;quot;,1.82845e-12,1.0052],[&amp;quot;-2^-17&amp;quot;,9.10676e-13,1.0013],[&amp;quot;-2^-18&amp;quot;,4.54895e-13,1.00032],[&amp;quot;-2^-19&amp;quot;,2.27392e-13,1.00008],[&amp;quot;-2^-20&amp;quot;,1.13689e-13,1.00002],[&amp;quot;-2^-21&amp;quot;,5.68437e-14,1],[&amp;quot;-2^-22&amp;quot;,2.84217e-14,1],[&amp;quot;-2^-23&amp;quot;,1.42109e-14,1],[&amp;quot;-2^-24&amp;quot;,7.10543e-15,1],[&amp;quot;-2^-25&amp;quot;,3.55271e-15,1],[&amp;quot;-2^-26&amp;quot;,9.92617e-24,5.58794e-09],[&amp;quot;-2^-27&amp;quot;,1.65436e-24,1.86265e-09],[&amp;quot;-2^-28&amp;quot;,8.27181e-25,1.86265e-09],[&amp;quot;-2^-29&amp;quot;,4.1359e-25,1.86265e-09],[&amp;quot;-2^-30&amp;quot;,2.06795e-25,1.86265e-09],[&amp;quot;-2^-31&amp;quot;,1.03398e-25,1.86265e-09],[&amp;quot;-2^-32&amp;quot;,5.16988e-26,1.86265e-09],[&amp;quot;-2^-33&amp;quot;,2.58494e-26,1.86265e-09],[&amp;quot;-2^-34&amp;quot;,1.29247e-26,1.86265e-09],[&amp;quot;-2^-35&amp;quot;,6.46235e-27,1.86265e-09],[&amp;quot;-2^-36&amp;quot;,3.23117e-27,1.86265e-09],[&amp;quot;-2^-37&amp;quot;,1.61559e-27,1.86265e-09],[&amp;quot;-2^-38&amp;quot;,8.07794e-28,1.86265e-09],[&amp;quot;-2^-39&amp;quot;,4.03897e-28,1.86265e-09],[&amp;quot;-2^-40&amp;quot;,2.01948e-28,1.86265e-09],[&amp;quot;-2^-41&amp;quot;,1.00974e-28,1.86265e-09],[&amp;quot;-2^-42&amp;quot;,5.04871e-29,1.86265e-09],[&amp;quot;-2^-43&amp;quot;,2.52435e-29,1.86265e-09],[&amp;quot;-2^-44&amp;quot;,1.26218e-29,1.86265e-09],[&amp;quot;-2^-45&amp;quot;,6.31089e-30,1.86265e-09],[&amp;quot;-2^-46&amp;quot;,3.15544e-30,1.86265e-09],[&amp;quot;-2^-47&amp;quot;,1.57772e-30,1.86265e-09],[&amp;quot;-2^-48&amp;quot;,7.88861e-31,1.86265e-09],[&amp;quot;-2^-49&amp;quot;,3.9443e-31,1.86265e-09],[&amp;quot;-2^-50&amp;quot;,1.97215e-31,1.86265e-09],[&amp;quot;-2^-51&amp;quot;,9.86076e-32,1.86265e-09],[&amp;quot;-2^-52&amp;quot;,4.93038e-32,1.86265e-09],[&amp;quot;-2^-53&amp;quot;,2.46519e-32,1.86265e-09],[&amp;quot;-2^-54&amp;quot;,1.2326e-32,1.86265e-09],[&amp;quot;-2^-55&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-56&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-57&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-58&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-59&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-60&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-61&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-62&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-63&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-64&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-65&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-66&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-67&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-68&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-69&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-70&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-71&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-72&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-73&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-74&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-75&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-76&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-77&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-78&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-79&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-80&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-81&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-82&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-83&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-84&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-85&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-86&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-87&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-88&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-89&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-90&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-91&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-92&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-93&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-94&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-95&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-96&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-97&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-98&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-99&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-100&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-101&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-102&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-103&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-104&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-105&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-106&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-107&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-108&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-109&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-110&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-111&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-112&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-113&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-114&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-115&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-116&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-117&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-118&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-119&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-120&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-121&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-122&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-123&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-124&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-125&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-126&amp;quot;,0,1.17549e-38],[&amp;quot;-SN&amp;quot;,0,1.17549e-38],[&amp;quot;0&amp;quot;,0,1.17549e-38],[&amp;quot;+SN&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-126&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-125&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-124&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-123&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-122&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-121&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-120&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-119&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-118&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-117&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-116&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-115&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-114&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-113&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-112&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-111&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-110&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-109&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-108&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-107&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-106&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-105&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-104&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-103&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-102&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-101&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-100&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-99&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-98&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-97&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-96&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-95&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-94&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-93&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-92&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-91&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-90&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-89&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-88&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-87&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-86&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-85&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-84&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-83&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-82&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-81&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-80&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-79&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-78&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-77&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-76&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-75&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-74&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-73&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-72&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-71&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-70&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-69&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-68&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-67&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-66&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-65&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-64&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-63&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-62&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-61&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-60&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-59&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-58&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-57&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-56&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-55&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-54&amp;quot;,1.2326e-32,1.86265e-09],[&amp;quot;+2^-53&amp;quot;,2.46519e-32,1.86265e-09],[&amp;quot;+2^-52&amp;quot;,4.93038e-32,1.86265e-09],[&amp;quot;+2^-51&amp;quot;,9.86076e-32,1.86265e-09],[&amp;quot;+2^-50&amp;quot;,1.97215e-31,1.86265e-09],[&amp;quot;+2^-49&amp;quot;,3.9443e-31,1.86265e-09],[&amp;quot;+2^-48&amp;quot;,7.88861e-31,1.86265e-09],[&amp;quot;+2^-47&amp;quot;,1.57772e-30,1.86265e-09],[&amp;quot;+2^-46&amp;quot;,3.15544e-30,1.86265e-09],[&amp;quot;+2^-45&amp;quot;,6.31089e-30,1.86265e-09],[&amp;quot;+2^-44&amp;quot;,1.26218e-29,1.86265e-09],[&amp;quot;+2^-43&amp;quot;,2.52435e-29,1.86265e-09],[&amp;quot;+2^-42&amp;quot;,5.04871e-29,1.86265e-09],[&amp;quot;+2^-41&amp;quot;,1.00974e-28,1.86265e-09],[&amp;quot;+2^-40&amp;quot;,2.01948e-28,1.86265e-09],[&amp;quot;+2^-39&amp;quot;,4.03897e-28,1.86265e-09],[&amp;quot;+2^-38&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-37&amp;quot;,1.61559e-27,1.86265e-09],[&amp;quot;+2^-36&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-35&amp;quot;,6.46235e-27,1.86265e-09],[&amp;quot;+2^-34&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-33&amp;quot;,2.58494e-26,1.86265e-09],[&amp;quot;+2^-32&amp;quot;,5.16988e-26,1.86265e-09],[&amp;quot;+2^-31&amp;quot;,1.03398e-25,1.86265e-09],[&amp;quot;+2^-30&amp;quot;,2.06795e-25,1.86265e-09],[&amp;quot;+2^-29&amp;quot;,4.1359e-25,1.86265e-09],[&amp;quot;+2^-28&amp;quot;,8.27181e-25,1.86265e-09],[&amp;quot;+2^-27&amp;quot;,1.65436e-24,1.86265e-09],[&amp;quot;+2^-26&amp;quot;,9.92617e-24,5.58794e-09],[&amp;quot;+2^-25&amp;quot;,3.55271e-15,1],[&amp;quot;+2^-24&amp;quot;,7.10543e-15,1],[&amp;quot;+2^-23&amp;quot;,1.42109e-14,1],[&amp;quot;+2^-22&amp;quot;,2.84217e-14,1],[&amp;quot;+2^-21&amp;quot;,5.68437e-14,1],[&amp;quot;+2^-20&amp;quot;,1.13689e-13,1.00002],[&amp;quot;+2^-19&amp;quot;,2.27391e-13,1.00008],[&amp;quot;+2^-18&amp;quot;,4.54892e-13,1.00032],[&amp;quot;+2^-17&amp;quot;,9.10665e-13,1.00129],[&amp;quot;+2^-16&amp;quot;,1.82841e-12,1.00518],[&amp;quot;+2^-15&amp;quot;,3.71353e-12,1.02077],[&amp;quot;+2^-14&amp;quot;,1.39798e-11,1.92136],[&amp;quot;+2^-13&amp;quot;,2.63239e-11,1.80896],[&amp;quot;+2^-12&amp;quot;,5.37712e-11,1.84757],[&amp;quot;+2^-11&amp;quot;,1.09932e-10,1.88861],[&amp;quot;+2^-10&amp;quot;,2.24787e-10,1.93091],[&amp;quot;+2^-9&amp;quot;,4.3452e-10,1.86625],[&amp;quot;+2^-8&amp;quot;,1.01783e-09,2.18576],[&amp;quot;+2^-7&amp;quot;,2.10562e-09,2.26089],[&amp;quot;+2^-6&amp;quot;,4.03954e-09,2.16871],[&amp;quot;+2^-5&amp;quot;,8.15746e-09,2.18975],[&amp;quot;+2^-4&amp;quot;,1.5545e-08,2.08641],[&amp;quot;+2^-3&amp;quot;,2.62153e-08,2.05864],[&amp;quot;+2^-2&amp;quot;,4.09295e-08,1.73804],[&amp;quot;+2^-1&amp;quot;,8.02409e-08,1.34622],[&amp;quot;+2^0&amp;quot;,8.66201e-08,1.45324],[&amp;quot;+2^1&amp;quot;,8.8654e-08,1.48737],[&amp;quot;+2^2&amp;quot;,6.7976e-08,1.14045],[&amp;quot;+2^3&amp;quot;,7.44117e-08,1.24842],[&amp;quot;+2^4&amp;quot;,2.53131e-14,4.24683e-07],[&amp;quot;+2^5&amp;quot;,0,null],[&amp;quot;+2^6&amp;quot;,0,null],[&amp;quot;+2^7&amp;quot;,0,null],[&amp;quot;+2^8&amp;quot;,0,null],[&amp;quot;+2^9&amp;quot;,0,null],[&amp;quot;+2^10&amp;quot;,0,null],[&amp;quot;+2^11&amp;quot;,0,null],[&amp;quot;+2^12&amp;quot;,0,null],[&amp;quot;+2^13&amp;quot;,0,null],[&amp;quot;+2^14&amp;quot;,0,null],[&amp;quot;+2^15&amp;quot;,0,null],[&amp;quot;+2^16&amp;quot;,0,null],[&amp;quot;+2^17&amp;quot;,0,null],[&amp;quot;+2^18&amp;quot;,0,null],[&amp;quot;+2^19&amp;quot;,0,null],[&amp;quot;+2^20&amp;quot;,0,null],[&amp;quot;+2^21&amp;quot;,0,null],[&amp;quot;+2^22&amp;quot;,0,null],[&amp;quot;+2^23&amp;quot;,0,null],[&amp;quot;+2^24&amp;quot;,0,null],[&amp;quot;+2^25&amp;quot;,0,null],[&amp;quot;+2^26&amp;quot;,0,null],[&amp;quot;+2^27&amp;quot;,0,null],[&amp;quot;+2^28&amp;quot;,0,null],[&amp;quot;+2^29&amp;quot;,0,null],[&amp;quot;+2^30&amp;quot;,0,null],[&amp;quot;+2^31&amp;quot;,0,null],[&amp;quot;+2^32&amp;quot;,0,null],[&amp;quot;+2^33&amp;quot;,0,null],[&amp;quot;+2^34&amp;quot;,0,null],[&amp;quot;+2^35&amp;quot;,0,null],[&amp;quot;+2^36&amp;quot;,0,null],[&amp;quot;+2^37&amp;quot;,0,null],[&amp;quot;+2^38&amp;quot;,0,null],[&amp;quot;+2^39&amp;quot;,0,null],[&amp;quot;+2^40&amp;quot;,0,null],[&amp;quot;+2^41&amp;quot;,0,null],[&amp;quot;+2^42&amp;quot;,0,null],[&amp;quot;+2^43&amp;quot;,0,null],[&amp;quot;+2^44&amp;quot;,0,null],[&amp;quot;+2^45&amp;quot;,0,null],[&amp;quot;+2^46&amp;quot;,0,null],[&amp;quot;+2^47&amp;quot;,0,null],[&amp;quot;+2^48&amp;quot;,0,null],[&amp;quot;+2^49&amp;quot;,0,null],[&amp;quot;+2^50&amp;quot;,0,null],[&amp;quot;+2^51&amp;quot;,0,null],[&amp;quot;+2^52&amp;quot;,0,null],[&amp;quot;+2^53&amp;quot;,0,null],[&amp;quot;+2^54&amp;quot;,0,null],[&amp;quot;+2^55&amp;quot;,0,null],[&amp;quot;+2^56&amp;quot;,0,null],[&amp;quot;+2^57&amp;quot;,0,null],[&amp;quot;+2^58&amp;quot;,0,null],[&amp;quot;+2^59&amp;quot;,0,null],[&amp;quot;+2^60&amp;quot;,0,null],[&amp;quot;+2^61&amp;quot;,0,null],[&amp;quot;+2^62&amp;quot;,0,null],[&amp;quot;+2^63&amp;quot;,0,null],[&amp;quot;+2^64&amp;quot;,0,null],[&amp;quot;+2^65&amp;quot;,0,null],[&amp;quot;+2^66&amp;quot;,0,null],[&amp;quot;+2^67&amp;quot;,0,null],[&amp;quot;+2^68&amp;quot;,0,null],[&amp;quot;+2^69&amp;quot;,0,null],[&amp;quot;+2^70&amp;quot;,0,null],[&amp;quot;+2^71&amp;quot;,0,null],[&amp;quot;+2^72&amp;quot;,0,null],[&amp;quot;+2^73&amp;quot;,0,null],[&amp;quot;+2^74&amp;quot;,0,null],[&amp;quot;+2^75&amp;quot;,0,null],[&amp;quot;+2^76&amp;quot;,0,null],[&amp;quot;+2^77&amp;quot;,0,null],[&amp;quot;+2^78&amp;quot;,0,null],[&amp;quot;+2^79&amp;quot;,0,null],[&amp;quot;+2^80&amp;quot;,0,null],[&amp;quot;+2^81&amp;quot;,0,null],[&amp;quot;+2^82&amp;quot;,0,null],[&amp;quot;+2^83&amp;quot;,0,null],[&amp;quot;+2^84&amp;quot;,0,null],[&amp;quot;+2^85&amp;quot;,0,null],[&amp;quot;+2^86&amp;quot;,0,null],[&amp;quot;+2^87&amp;quot;,0,null],[&amp;quot;+2^88&amp;quot;,0,null],[&amp;quot;+2^89&amp;quot;,0,null],[&amp;quot;+2^90&amp;quot;,0,null],[&amp;quot;+2^91&amp;quot;,0,null],[&amp;quot;+2^92&amp;quot;,0,null],[&amp;quot;+2^93&amp;quot;,0,null],[&amp;quot;+2^94&amp;quot;,0,null],[&amp;quot;+2^95&amp;quot;,0,null],[&amp;quot;+2^96&amp;quot;,0,null],[&amp;quot;+2^97&amp;quot;,0,null],[&amp;quot;+2^98&amp;quot;,0,null],[&amp;quot;+2^99&amp;quot;,0,null],[&amp;quot;+2^100&amp;quot;,0,null],[&amp;quot;+2^101&amp;quot;,0,null],[&amp;quot;+2^102&amp;quot;,0,null],[&amp;quot;+2^103&amp;quot;,0,null],[&amp;quot;+2^104&amp;quot;,0,null],[&amp;quot;+2^105&amp;quot;,0,null],[&amp;quot;+2^106&amp;quot;,0,null],[&amp;quot;+2^107&amp;quot;,0,null],[&amp;quot;+2^108&amp;quot;,0,null],[&amp;quot;+2^109&amp;quot;,0,null],[&amp;quot;+2^110&amp;quot;,0,null],[&amp;quot;+2^111&amp;quot;,0,null],[&amp;quot;+2^112&amp;quot;,0,null],[&amp;quot;+2^113&amp;quot;,0,null],[&amp;quot;+2^114&amp;quot;,0,null],[&amp;quot;+2^115&amp;quot;,0,null],[&amp;quot;+2^116&amp;quot;,0,null],[&amp;quot;+2^117&amp;quot;,0,null],[&amp;quot;+2^118&amp;quot;,0,null],[&amp;quot;+2^119&amp;quot;,0,null],[&amp;quot;+2^120&amp;quot;,0,null],[&amp;quot;+2^121&amp;quot;,0,null],[&amp;quot;+2^122&amp;quot;,0,null],[&amp;quot;+2^123&amp;quot;,0,null],[&amp;quot;+2^124&amp;quot;,0,null],[&amp;quot;+2^125&amp;quot;,0,null],[&amp;quot;+2^126&amp;quot;,0,null],[&amp;quot;+2^127&amp;quot;,0,null] ]&lt;br /&gt;
 }}&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Key observation:&#039;&#039;&#039;  &lt;br /&gt;
Compared to direct exponential formulations, the &amp;lt;code&amp;gt;expm1&amp;lt;/code&amp;gt; approach maintains low ULP error for small and medium values of &amp;lt;math&amp;gt;x&amp;lt;/math&amp;gt;. &lt;br /&gt;
Overflow is still present for very large inputs, but the overall error behavior is substantially more stable.&lt;br /&gt;
&lt;br /&gt;
== Numerical Issues and Error Metrics ==&lt;br /&gt;
&lt;br /&gt;
To evaluate and compare different software implementations of &amp;lt;code&amp;gt;tanh&amp;lt;/code&amp;gt;, several numerical error metrics are used throughout this article. These metrics make it possible to reason about accuracy, robustness, and failure modes in floating-point arithmetic.&lt;br /&gt;
&lt;br /&gt;
=== ULP Error ===&lt;br /&gt;
ULP (Unit in the Last Place) measures the distance between two adjacent representable floating-point numbers. The ULP error of a computed result is defined as the difference, measured in ULPs, between the computed value and the correctly rounded reference value.&lt;br /&gt;
&lt;br /&gt;
An error of &amp;lt;code&amp;gt;ULP ≤ 2&amp;lt;/code&amp;gt; is commonly considered acceptable for single-precision transcendental functions, as it indicates that the result is very close to the correctly rounded value.&lt;br /&gt;
&lt;br /&gt;
Large ULP errors typically indicate catastrophic cancellation, loss of significant bits, or severe rounding effects.&lt;br /&gt;
&lt;br /&gt;
=== NaN and Overflow ===&lt;br /&gt;
NaN (Not a Number) results occur when undefined floating-point operations are performed, such as &amp;lt;code&amp;gt;inf / inf&amp;lt;/code&amp;gt;. In &amp;lt;code&amp;gt;tanh&amp;lt;/code&amp;gt; implementations, this often happens when both the numerator and denominator of a formulation overflow to infinity.&lt;br /&gt;
&lt;br /&gt;
Tracking NaN generation is critical, as NaNs can silently propagate through numerical pipelines and invalidate downstream results.&lt;br /&gt;
&lt;br /&gt;
=== Error Graphs ===&lt;br /&gt;
The error graphs shown in the following sections visualize ULP error across the full &amp;lt;code&amp;gt;float32&amp;lt;/code&amp;gt; input domain. They make it possible to identify:&lt;br /&gt;
* regions with catastrophic cancellation,&lt;br /&gt;
* input ranges that trigger overflow or NaNs,&lt;br /&gt;
* and numerical stability differences between formulations.&lt;br /&gt;
&lt;br /&gt;
These graphs are a key tool for understanding not only the maximum error, but also how errors are distributed across different input magnitudes.&lt;br /&gt;
&lt;br /&gt;
== Taylor Expansion (Local Approximation) ==&lt;br /&gt;
&lt;br /&gt;
Taylor series approximations of &amp;lt;code&amp;gt;tanh(x)&amp;lt;/code&amp;gt; expand the function around &amp;lt;math&amp;gt;x = 0&amp;lt;/math&amp;gt; and therefore provide a very accurate approximation only for sufficiently small input magnitudes.&lt;br /&gt;
&lt;br /&gt;
For small &amp;lt;math&amp;gt;|x|&amp;lt;/math&amp;gt;, the hyperbolic tangent behaves approximately linear (&amp;lt;math&amp;gt;\tanh(x) \approx x&amp;lt;/math&amp;gt;), and low-order Taylor polynomials can achieve excellent accuracy with minimal computational cost. This makes Taylor-based approximations attractive as a fast path in range-reduced implementations.&lt;br /&gt;
&lt;br /&gt;
However, Taylor expansions have a strictly limited radius of convergence. Outside a narrow neighborhood around zero, the approximation error grows extremely rapidly and quickly becomes unusable. As a result, Taylor polynomials are not suitable as standalone implementations of &amp;lt;code&amp;gt;tanh&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
{{#widget:FloatErrorGraph|id=taylor&lt;br /&gt;
  |data=[ [&amp;quot;-2^127&amp;quot;,0,0],[&amp;quot;-2^126&amp;quot;,0,0],[&amp;quot;-2^125&amp;quot;,0,0],[&amp;quot;-2^124&amp;quot;,0,0],[&amp;quot;-2^123&amp;quot;,0,0],[&amp;quot;-2^122&amp;quot;,0,0],[&amp;quot;-2^121&amp;quot;,0,0],[&amp;quot;-2^120&amp;quot;,0,0],[&amp;quot;-2^119&amp;quot;,0,0],[&amp;quot;-2^118&amp;quot;,0,0],[&amp;quot;-2^117&amp;quot;,0,0],[&amp;quot;-2^116&amp;quot;,0,0],[&amp;quot;-2^115&amp;quot;,0,0],[&amp;quot;-2^114&amp;quot;,0,0],[&amp;quot;-2^113&amp;quot;,0,0],[&amp;quot;-2^112&amp;quot;,0,0],[&amp;quot;-2^111&amp;quot;,0,0],[&amp;quot;-2^110&amp;quot;,0,0],[&amp;quot;-2^109&amp;quot;,0,0],[&amp;quot;-2^108&amp;quot;,0,0],[&amp;quot;-2^107&amp;quot;,0,0],[&amp;quot;-2^106&amp;quot;,0,0],[&amp;quot;-2^105&amp;quot;,0,0],[&amp;quot;-2^104&amp;quot;,0,0],[&amp;quot;-2^103&amp;quot;,0,0],[&amp;quot;-2^102&amp;quot;,0,0],[&amp;quot;-2^101&amp;quot;,0,0],[&amp;quot;-2^100&amp;quot;,0,0],[&amp;quot;-2^99&amp;quot;,0,0],[&amp;quot;-2^98&amp;quot;,0,0],[&amp;quot;-2^97&amp;quot;,0,0],[&amp;quot;-2^96&amp;quot;,0,0],[&amp;quot;-2^95&amp;quot;,0,0],[&amp;quot;-2^94&amp;quot;,0,0],[&amp;quot;-2^93&amp;quot;,0,0],[&amp;quot;-2^92&amp;quot;,0,0],[&amp;quot;-2^91&amp;quot;,0,0],[&amp;quot;-2^90&amp;quot;,0,0],[&amp;quot;-2^89&amp;quot;,0,0],[&amp;quot;-2^88&amp;quot;,0,0],[&amp;quot;-2^87&amp;quot;,0,0],[&amp;quot;-2^86&amp;quot;,0,0],[&amp;quot;-2^85&amp;quot;,0,0],[&amp;quot;-2^84&amp;quot;,0,0],[&amp;quot;-2^83&amp;quot;,0,0],[&amp;quot;-2^82&amp;quot;,0,0],[&amp;quot;-2^81&amp;quot;,0,0],[&amp;quot;-2^80&amp;quot;,0,0],[&amp;quot;-2^79&amp;quot;,0,0],[&amp;quot;-2^78&amp;quot;,0,0],[&amp;quot;-2^77&amp;quot;,0,0],[&amp;quot;-2^76&amp;quot;,0,0],[&amp;quot;-2^75&amp;quot;,0,0],[&amp;quot;-2^74&amp;quot;,0,0],[&amp;quot;-2^73&amp;quot;,0,0],[&amp;quot;-2^72&amp;quot;,0,0],[&amp;quot;-2^71&amp;quot;,0,0],[&amp;quot;-2^70&amp;quot;,0,0],[&amp;quot;-2^69&amp;quot;,0,0],[&amp;quot;-2^68&amp;quot;,0,0],[&amp;quot;-2^67&amp;quot;,0,0],[&amp;quot;-2^66&amp;quot;,0,0],[&amp;quot;-2^65&amp;quot;,0,0],[&amp;quot;-2^64&amp;quot;,0,0],[&amp;quot;-2^63&amp;quot;,1.84467e+19,3.09485e+26],[&amp;quot;-2^62&amp;quot;,9.22336e+18,1.54742e+26],[&amp;quot;-2^61&amp;quot;,4.61168e+18,7.73712e+25],[&amp;quot;-2^60&amp;quot;,2.30584e+18,3.86856e+25],[&amp;quot;-2^59&amp;quot;,1.15292e+18,1.93428e+25],[&amp;quot;-2^58&amp;quot;,5.7646e+17,9.6714e+24],[&amp;quot;-2^57&amp;quot;,2.8823e+17,4.8357e+24],[&amp;quot;-2^56&amp;quot;,1.44115e+17,2.41785e+24],[&amp;quot;-2^55&amp;quot;,7.20575e+16,1.20892e+24],[&amp;quot;-2^54&amp;quot;,3.60288e+16,6.04462e+23],[&amp;quot;-2^53&amp;quot;,1.80144e+16,3.02231e+23],[&amp;quot;-2^52&amp;quot;,9.00719e+15,1.51116e+23],[&amp;quot;-2^51&amp;quot;,4.5036e+15,7.55578e+22],[&amp;quot;-2^50&amp;quot;,2.2518e+15,3.77789e+22],[&amp;quot;-2^49&amp;quot;,1.1259e+15,1.88894e+22],[&amp;quot;-2^48&amp;quot;,5.62949e+14,9.44472e+21],[&amp;quot;-2^47&amp;quot;,2.81475e+14,4.72236e+21],[&amp;quot;-2^46&amp;quot;,1.40737e+14,2.36118e+21],[&amp;quot;-2^45&amp;quot;,7.03687e+13,1.18059e+21],[&amp;quot;-2^44&amp;quot;,3.51843e+13,5.90295e+20],[&amp;quot;-2^43&amp;quot;,1.75922e+13,2.95148e+20],[&amp;quot;-2^42&amp;quot;,8.79608e+12,1.47574e+20],[&amp;quot;-2^41&amp;quot;,4.39804e+12,7.37869e+19],[&amp;quot;-2^40&amp;quot;,2.19902e+12,3.68935e+19],[&amp;quot;-2^39&amp;quot;,1.09951e+12,1.84467e+19],[&amp;quot;-2^38&amp;quot;,5.49755e+11,9.22336e+18],[&amp;quot;-2^37&amp;quot;,2.74878e+11,4.61168e+18],[&amp;quot;-2^36&amp;quot;,1.37439e+11,2.30584e+18],[&amp;quot;-2^35&amp;quot;,6.87194e+10,1.15292e+18],[&amp;quot;-2^34&amp;quot;,3.43597e+10,5.7646e+17],[&amp;quot;-2^33&amp;quot;,1.71799e+10,2.8823e+17],[&amp;quot;-2^32&amp;quot;,8.58993e+09,1.44115e+17],[&amp;quot;-2^31&amp;quot;,4.29085e+09,7.19885e+16],[&amp;quot;-2^30&amp;quot;,2.14542e+09,3.59943e+16],[&amp;quot;-2^29&amp;quot;,1.07271e+09,1.79971e+16],[&amp;quot;-2^28&amp;quot;,5.36356e+08,8.99856e+15],[&amp;quot;-2^27&amp;quot;,2.68178e+08,4.49928e+15],[&amp;quot;-2^26&amp;quot;,1.34089e+08,2.24964e+15],[&amp;quot;-2^25&amp;quot;,6.70445e+07,1.12482e+15],[&amp;quot;-2^24&amp;quot;,3.35223e+07,5.6241e+14],[&amp;quot;-2^23&amp;quot;,1.67611e+07,2.81205e+14],[&amp;quot;-2^22&amp;quot;,8.38056e+06,1.40603e+14],[&amp;quot;-2^21&amp;quot;,4.19028e+06,7.03013e+13],[&amp;quot;-2^20&amp;quot;,2.09514e+06,3.51506e+13],[&amp;quot;-2^19&amp;quot;,1.04757e+06,1.75753e+13],[&amp;quot;-2^18&amp;quot;,523784,8.78764e+12],[&amp;quot;-2^17&amp;quot;,261892,4.39381e+12],[&amp;quot;-2^16&amp;quot;,130945,2.1969e+12],[&amp;quot;-2^15&amp;quot;,65472.2,1.09844e+12],[&amp;quot;-2^14&amp;quot;,32735.6,5.49212e+11],[&amp;quot;-2^13&amp;quot;,16367.3,2.74598e+11],[&amp;quot;-2^12&amp;quot;,8183.15,1.3729e+11],[&amp;quot;-2^11&amp;quot;,4091.07,6.86368e+10],[&amp;quot;-2^10&amp;quot;,2045.04,3.431e+10],[&amp;quot;-2^9&amp;quot;,1022.02,1.71466e+10],[&amp;quot;-2^8&amp;quot;,510.509,8.56492e+09],[&amp;quot;-2^7&amp;quot;,254.754,4.27406e+09],[&amp;quot;-2^6&amp;quot;,126.876,2.12862e+09],[&amp;quot;-2^5&amp;quot;,62.9354,1.05588e+09],[&amp;quot;-2^4&amp;quot;,30.9629,5.19471e+08],[&amp;quot;-2^3&amp;quot;,14.9718,2.51185e+08],[&amp;quot;-2^2&amp;quot;,6.96672,1.16882e+08],[&amp;quot;-2^1&amp;quot;,2.94633,4.94312e+07],[&amp;quot;-2^0&amp;quot;,0.940715,1.57826e+07],[&amp;quot;-2^-1&amp;quot;,0.127635,2.14135e+06],[&amp;quot;-2^-2&amp;quot;,0.00318735,106950],[&amp;quot;-2^-3&amp;quot;,3.11475e-05,2090.28],[&amp;quot;-2^-4&amp;quot;,2.57737e-07,34.5929],[&amp;quot;-2^-5&amp;quot;,3.89213e-09,1.04478],[&amp;quot;-2^-6&amp;quot;,9.51472e-10,0.510818],[&amp;quot;-2^-7&amp;quot;,4.66402e-10,0.500795],[&amp;quot;-2^-8&amp;quot;,2.32917e-10,0.500185],[&amp;quot;-2^-9&amp;quot;,1.16423e-10,0.500033],[&amp;quot;-2^-10&amp;quot;,5.82082e-11,0.500004],[&amp;quot;-2^-11&amp;quot;,2.91037e-11,0.499998],[&amp;quot;-2^-12&amp;quot;,1.45519e-11,0.5],[&amp;quot;-2^-13&amp;quot;,4.85062e-12,0.333332],[&amp;quot;-2^-14&amp;quot;,6.06328e-13,0.0833331],[&amp;quot;-2^-15&amp;quot;,7.5791e-14,0.0208333],[&amp;quot;-2^-16&amp;quot;,9.47387e-15,0.00520832],[&amp;quot;-2^-17&amp;quot;,1.18424e-15,0.00130208],[&amp;quot;-2^-18&amp;quot;,1.48029e-16,0.00032552],[&amp;quot;-2^-19&amp;quot;,1.85039e-17,8.13808e-05],[&amp;quot;-2^-20&amp;quot;,2.31304e-18,2.03457e-05],[&amp;quot;-2^-21&amp;quot;,2.89156e-19,5.08688e-06],[&amp;quot;-2^-22&amp;quot;,3.61577e-20,1.27219e-06],[&amp;quot;-2^-23&amp;quot;,4.52633e-21,3.18512e-07],[&amp;quot;-2^-24&amp;quot;,5.691e-22,8.00937e-08],[&amp;quot;-2^-25&amp;quot;,7.27919e-23,2.04891e-08],[&amp;quot;-2^-26&amp;quot;,9.92617e-24,5.58794e-09],[&amp;quot;-2^-27&amp;quot;,1.65436e-24,1.86265e-09],[&amp;quot;-2^-28&amp;quot;,8.27181e-25,1.86265e-09],[&amp;quot;-2^-29&amp;quot;,4.1359e-25,1.86265e-09],[&amp;quot;-2^-30&amp;quot;,2.06795e-25,1.86265e-09],[&amp;quot;-2^-31&amp;quot;,1.03398e-25,1.86265e-09],[&amp;quot;-2^-32&amp;quot;,5.16988e-26,1.86265e-09],[&amp;quot;-2^-33&amp;quot;,2.58494e-26,1.86265e-09],[&amp;quot;-2^-34&amp;quot;,1.29247e-26,1.86265e-09],[&amp;quot;-2^-35&amp;quot;,6.46235e-27,1.86265e-09],[&amp;quot;-2^-36&amp;quot;,3.23117e-27,1.86265e-09],[&amp;quot;-2^-37&amp;quot;,1.61559e-27,1.86265e-09],[&amp;quot;-2^-38&amp;quot;,8.07794e-28,1.86265e-09],[&amp;quot;-2^-39&amp;quot;,4.03897e-28,1.86265e-09],[&amp;quot;-2^-40&amp;quot;,2.01948e-28,1.86265e-09],[&amp;quot;-2^-41&amp;quot;,1.00974e-28,1.86265e-09],[&amp;quot;-2^-42&amp;quot;,5.04871e-29,1.86265e-09],[&amp;quot;-2^-43&amp;quot;,2.52435e-29,1.86265e-09],[&amp;quot;-2^-44&amp;quot;,1.26218e-29,1.86265e-09],[&amp;quot;-2^-45&amp;quot;,6.31089e-30,1.86265e-09],[&amp;quot;-2^-46&amp;quot;,3.15544e-30,1.86265e-09],[&amp;quot;-2^-47&amp;quot;,1.57772e-30,1.86265e-09],[&amp;quot;-2^-48&amp;quot;,7.88861e-31,1.86265e-09],[&amp;quot;-2^-49&amp;quot;,3.9443e-31,1.86265e-09],[&amp;quot;-2^-50&amp;quot;,1.97215e-31,1.86265e-09],[&amp;quot;-2^-51&amp;quot;,9.86076e-32,1.86265e-09],[&amp;quot;-2^-52&amp;quot;,4.93038e-32,1.86265e-09],[&amp;quot;-2^-53&amp;quot;,2.46519e-32,1.86265e-09],[&amp;quot;-2^-54&amp;quot;,1.2326e-32,1.86265e-09],[&amp;quot;-2^-55&amp;quot;,0,0],[&amp;quot;-2^-56&amp;quot;,0,0],[&amp;quot;-2^-57&amp;quot;,0,0],[&amp;quot;-2^-58&amp;quot;,0,0],[&amp;quot;-2^-59&amp;quot;,0,0],[&amp;quot;-2^-60&amp;quot;,0,0],[&amp;quot;-2^-61&amp;quot;,0,0],[&amp;quot;-2^-62&amp;quot;,0,0],[&amp;quot;-2^-63&amp;quot;,0,0],[&amp;quot;-2^-64&amp;quot;,0,0],[&amp;quot;-2^-65&amp;quot;,0,0],[&amp;quot;-2^-66&amp;quot;,0,0],[&amp;quot;-2^-67&amp;quot;,0,0],[&amp;quot;-2^-68&amp;quot;,0,0],[&amp;quot;-2^-69&amp;quot;,0,0],[&amp;quot;-2^-70&amp;quot;,0,0],[&amp;quot;-2^-71&amp;quot;,0,0],[&amp;quot;-2^-72&amp;quot;,0,0],[&amp;quot;-2^-73&amp;quot;,0,0],[&amp;quot;-2^-74&amp;quot;,0,0],[&amp;quot;-2^-75&amp;quot;,0,0],[&amp;quot;-2^-76&amp;quot;,0,0],[&amp;quot;-2^-77&amp;quot;,0,0],[&amp;quot;-2^-78&amp;quot;,0,0],[&amp;quot;-2^-79&amp;quot;,0,0],[&amp;quot;-2^-80&amp;quot;,0,0],[&amp;quot;-2^-81&amp;quot;,0,0],[&amp;quot;-2^-82&amp;quot;,0,0],[&amp;quot;-2^-83&amp;quot;,0,0],[&amp;quot;-2^-84&amp;quot;,0,0],[&amp;quot;-2^-85&amp;quot;,0,0],[&amp;quot;-2^-86&amp;quot;,0,0],[&amp;quot;-2^-87&amp;quot;,0,0],[&amp;quot;-2^-88&amp;quot;,0,0],[&amp;quot;-2^-89&amp;quot;,0,0],[&amp;quot;-2^-90&amp;quot;,0,0],[&amp;quot;-2^-91&amp;quot;,0,0],[&amp;quot;-2^-92&amp;quot;,0,0],[&amp;quot;-2^-93&amp;quot;,0,0],[&amp;quot;-2^-94&amp;quot;,0,0],[&amp;quot;-2^-95&amp;quot;,0,0],[&amp;quot;-2^-96&amp;quot;,0,0],[&amp;quot;-2^-97&amp;quot;,0,0],[&amp;quot;-2^-98&amp;quot;,0,0],[&amp;quot;-2^-99&amp;quot;,0,0],[&amp;quot;-2^-100&amp;quot;,0,0],[&amp;quot;-2^-101&amp;quot;,0,0],[&amp;quot;-2^-102&amp;quot;,0,0],[&amp;quot;-2^-103&amp;quot;,0,0],[&amp;quot;-2^-104&amp;quot;,0,0],[&amp;quot;-2^-105&amp;quot;,0,0],[&amp;quot;-2^-106&amp;quot;,0,0],[&amp;quot;-2^-107&amp;quot;,0,0],[&amp;quot;-2^-108&amp;quot;,0,0],[&amp;quot;-2^-109&amp;quot;,0,0],[&amp;quot;-2^-110&amp;quot;,0,0],[&amp;quot;-2^-111&amp;quot;,0,0],[&amp;quot;-2^-112&amp;quot;,0,0],[&amp;quot;-2^-113&amp;quot;,0,0],[&amp;quot;-2^-114&amp;quot;,0,0],[&amp;quot;-2^-115&amp;quot;,0,0],[&amp;quot;-2^-116&amp;quot;,0,0],[&amp;quot;-2^-117&amp;quot;,0,0],[&amp;quot;-2^-118&amp;quot;,0,0],[&amp;quot;-2^-119&amp;quot;,0,0],[&amp;quot;-2^-120&amp;quot;,0,0],[&amp;quot;-2^-121&amp;quot;,0,0],[&amp;quot;-2^-122&amp;quot;,0,0],[&amp;quot;-2^-123&amp;quot;,0,0],[&amp;quot;-2^-124&amp;quot;,0,0],[&amp;quot;-2^-125&amp;quot;,0,0],[&amp;quot;-2^-126&amp;quot;,0,0],[&amp;quot;-SN&amp;quot;,0,0],[&amp;quot;0&amp;quot;,0,0],[&amp;quot;+SN&amp;quot;,0,0],[&amp;quot;+2^-126&amp;quot;,0,0],[&amp;quot;+2^-125&amp;quot;,0,0],[&amp;quot;+2^-124&amp;quot;,0,0],[&amp;quot;+2^-123&amp;quot;,0,0],[&amp;quot;+2^-122&amp;quot;,0,0],[&amp;quot;+2^-121&amp;quot;,0,0],[&amp;quot;+2^-120&amp;quot;,0,0],[&amp;quot;+2^-119&amp;quot;,0,0],[&amp;quot;+2^-118&amp;quot;,0,0],[&amp;quot;+2^-117&amp;quot;,0,0],[&amp;quot;+2^-116&amp;quot;,0,0],[&amp;quot;+2^-115&amp;quot;,0,0],[&amp;quot;+2^-114&amp;quot;,0,0],[&amp;quot;+2^-113&amp;quot;,0,0],[&amp;quot;+2^-112&amp;quot;,0,0],[&amp;quot;+2^-111&amp;quot;,0,0],[&amp;quot;+2^-110&amp;quot;,0,0],[&amp;quot;+2^-109&amp;quot;,0,0],[&amp;quot;+2^-108&amp;quot;,0,0],[&amp;quot;+2^-107&amp;quot;,0,0],[&amp;quot;+2^-106&amp;quot;,0,0],[&amp;quot;+2^-105&amp;quot;,0,0],[&amp;quot;+2^-104&amp;quot;,0,0],[&amp;quot;+2^-103&amp;quot;,0,0],[&amp;quot;+2^-102&amp;quot;,0,0],[&amp;quot;+2^-101&amp;quot;,0,0],[&amp;quot;+2^-100&amp;quot;,0,0],[&amp;quot;+2^-99&amp;quot;,0,0],[&amp;quot;+2^-98&amp;quot;,0,0],[&amp;quot;+2^-97&amp;quot;,0,0],[&amp;quot;+2^-96&amp;quot;,0,0],[&amp;quot;+2^-95&amp;quot;,0,0],[&amp;quot;+2^-94&amp;quot;,0,0],[&amp;quot;+2^-93&amp;quot;,0,0],[&amp;quot;+2^-92&amp;quot;,0,0],[&amp;quot;+2^-91&amp;quot;,0,0],[&amp;quot;+2^-90&amp;quot;,0,0],[&amp;quot;+2^-89&amp;quot;,0,0],[&amp;quot;+2^-88&amp;quot;,0,0],[&amp;quot;+2^-87&amp;quot;,0,0],[&amp;quot;+2^-86&amp;quot;,0,0],[&amp;quot;+2^-85&amp;quot;,0,0],[&amp;quot;+2^-84&amp;quot;,0,0],[&amp;quot;+2^-83&amp;quot;,0,0],[&amp;quot;+2^-82&amp;quot;,0,0],[&amp;quot;+2^-81&amp;quot;,0,0],[&amp;quot;+2^-80&amp;quot;,0,0],[&amp;quot;+2^-79&amp;quot;,0,0],[&amp;quot;+2^-78&amp;quot;,0,0],[&amp;quot;+2^-77&amp;quot;,0,0],[&amp;quot;+2^-76&amp;quot;,0,0],[&amp;quot;+2^-75&amp;quot;,0,0],[&amp;quot;+2^-74&amp;quot;,0,0],[&amp;quot;+2^-73&amp;quot;,0,0],[&amp;quot;+2^-72&amp;quot;,0,0],[&amp;quot;+2^-71&amp;quot;,0,0],[&amp;quot;+2^-70&amp;quot;,0,0],[&amp;quot;+2^-69&amp;quot;,0,0],[&amp;quot;+2^-68&amp;quot;,0,0],[&amp;quot;+2^-67&amp;quot;,0,0],[&amp;quot;+2^-66&amp;quot;,0,0],[&amp;quot;+2^-65&amp;quot;,0,0],[&amp;quot;+2^-64&amp;quot;,0,0],[&amp;quot;+2^-63&amp;quot;,0,0],[&amp;quot;+2^-62&amp;quot;,0,0],[&amp;quot;+2^-61&amp;quot;,0,0],[&amp;quot;+2^-60&amp;quot;,0,0],[&amp;quot;+2^-59&amp;quot;,0,0],[&amp;quot;+2^-58&amp;quot;,0,0],[&amp;quot;+2^-57&amp;quot;,0,0],[&amp;quot;+2^-56&amp;quot;,0,0],[&amp;quot;+2^-55&amp;quot;,0,0],[&amp;quot;+2^-54&amp;quot;,1.2326e-32,1.86265e-09],[&amp;quot;+2^-53&amp;quot;,2.46519e-32,1.86265e-09],[&amp;quot;+2^-52&amp;quot;,4.93038e-32,1.86265e-09],[&amp;quot;+2^-51&amp;quot;,9.86076e-32,1.86265e-09],[&amp;quot;+2^-50&amp;quot;,1.97215e-31,1.86265e-09],[&amp;quot;+2^-49&amp;quot;,3.9443e-31,1.86265e-09],[&amp;quot;+2^-48&amp;quot;,7.88861e-31,1.86265e-09],[&amp;quot;+2^-47&amp;quot;,1.57772e-30,1.86265e-09],[&amp;quot;+2^-46&amp;quot;,3.15544e-30,1.86265e-09],[&amp;quot;+2^-45&amp;quot;,6.31089e-30,1.86265e-09],[&amp;quot;+2^-44&amp;quot;,1.26218e-29,1.86265e-09],[&amp;quot;+2^-43&amp;quot;,2.52435e-29,1.86265e-09],[&amp;quot;+2^-42&amp;quot;,5.04871e-29,1.86265e-09],[&amp;quot;+2^-41&amp;quot;,1.00974e-28,1.86265e-09],[&amp;quot;+2^-40&amp;quot;,2.01948e-28,1.86265e-09],[&amp;quot;+2^-39&amp;quot;,4.03897e-28,1.86265e-09],[&amp;quot;+2^-38&amp;quot;,8.07794e-28,1.86265e-09],[&amp;quot;+2^-37&amp;quot;,1.61559e-27,1.86265e-09],[&amp;quot;+2^-36&amp;quot;,3.23117e-27,1.86265e-09],[&amp;quot;+2^-35&amp;quot;,6.46235e-27,1.86265e-09],[&amp;quot;+2^-34&amp;quot;,1.29247e-26,1.86265e-09],[&amp;quot;+2^-33&amp;quot;,2.58494e-26,1.86265e-09],[&amp;quot;+2^-32&amp;quot;,5.16988e-26,1.86265e-09],[&amp;quot;+2^-31&amp;quot;,1.03398e-25,1.86265e-09],[&amp;quot;+2^-30&amp;quot;,2.06795e-25,1.86265e-09],[&amp;quot;+2^-29&amp;quot;,4.1359e-25,1.86265e-09],[&amp;quot;+2^-28&amp;quot;,8.27181e-25,1.86265e-09],[&amp;quot;+2^-27&amp;quot;,1.65436e-24,1.86265e-09],[&amp;quot;+2^-26&amp;quot;,9.92617e-24,5.58794e-09],[&amp;quot;+2^-25&amp;quot;,7.27919e-23,2.04891e-08],[&amp;quot;+2^-24&amp;quot;,5.691e-22,8.00937e-08],[&amp;quot;+2^-23&amp;quot;,4.52633e-21,3.18512e-07],[&amp;quot;+2^-22&amp;quot;,3.61577e-20,1.27219e-06],[&amp;quot;+2^-21&amp;quot;,2.89156e-19,5.08688e-06],[&amp;quot;+2^-20&amp;quot;,2.31304e-18,2.03457e-05],[&amp;quot;+2^-19&amp;quot;,1.85039e-17,8.13808e-05],[&amp;quot;+2^-18&amp;quot;,1.48029e-16,0.00032552],[&amp;quot;+2^-17&amp;quot;,1.18424e-15,0.00130208],[&amp;quot;+2^-16&amp;quot;,9.47387e-15,0.00520832],[&amp;quot;+2^-15&amp;quot;,7.5791e-14,0.0208333],[&amp;quot;+2^-14&amp;quot;,6.06328e-13,0.0833331],[&amp;quot;+2^-13&amp;quot;,4.85062e-12,0.333332],[&amp;quot;+2^-12&amp;quot;,1.45519e-11,0.5],[&amp;quot;+2^-11&amp;quot;,2.91037e-11,0.499998],[&amp;quot;+2^-10&amp;quot;,5.82082e-11,0.500004],[&amp;quot;+2^-9&amp;quot;,1.16423e-10,0.500033],[&amp;quot;+2^-8&amp;quot;,2.32917e-10,0.500185],[&amp;quot;+2^-7&amp;quot;,4.66402e-10,0.500795],[&amp;quot;+2^-6&amp;quot;,9.51472e-10,0.510818],[&amp;quot;+2^-5&amp;quot;,3.89213e-09,1.04478],[&amp;quot;+2^-4&amp;quot;,2.57737e-07,34.5929],[&amp;quot;+2^-3&amp;quot;,3.11475e-05,2090.28],[&amp;quot;+2^-2&amp;quot;,0.00318735,106950],[&amp;quot;+2^-1&amp;quot;,0.127635,2.14135e+06],[&amp;quot;+2^0&amp;quot;,0.940715,1.57826e+07],[&amp;quot;+2^1&amp;quot;,2.94633,4.94312e+07],[&amp;quot;+2^2&amp;quot;,6.96672,1.16882e+08],[&amp;quot;+2^3&amp;quot;,14.9718,1.33874e+08],[&amp;quot;+2^4&amp;quot;,30.9629,2.59736e+08],[&amp;quot;+2^5&amp;quot;,62.9354,5.27941e+08],[&amp;quot;+2^6&amp;quot;,126.876,1.06431e+09],[&amp;quot;+2^7&amp;quot;,254.754,2.13703e+09],[&amp;quot;+2^8&amp;quot;,510.509,4.28246e+09],[&amp;quot;+2^9&amp;quot;,1022.02,8.57331e+09],[&amp;quot;+2^10&amp;quot;,2045.04,1.7155e+10],[&amp;quot;+2^11&amp;quot;,4091.07,3.43184e+10],[&amp;quot;+2^12&amp;quot;,8183.15,6.86452e+10],[&amp;quot;+2^13&amp;quot;,16367.3,1.37299e+11],[&amp;quot;+2^14&amp;quot;,32735.6,2.74606e+11],[&amp;quot;+2^15&amp;quot;,65472.2,5.4922e+11],[&amp;quot;+2^16&amp;quot;,130945,1.09845e+12],[&amp;quot;+2^17&amp;quot;,261892,2.19691e+12],[&amp;quot;+2^18&amp;quot;,523784,4.39382e+12],[&amp;quot;+2^19&amp;quot;,1.04757e+06,8.78765e+12],[&amp;quot;+2^20&amp;quot;,2.09514e+06,1.75753e+13],[&amp;quot;+2^21&amp;quot;,4.19028e+06,3.51506e+13],[&amp;quot;+2^22&amp;quot;,8.38056e+06,7.03013e+13],[&amp;quot;+2^23&amp;quot;,1.67611e+07,1.40603e+14],[&amp;quot;+2^24&amp;quot;,3.35223e+07,2.81205e+14],[&amp;quot;+2^25&amp;quot;,6.70445e+07,5.6241e+14],[&amp;quot;+2^26&amp;quot;,1.34089e+08,1.12482e+15],[&amp;quot;+2^27&amp;quot;,2.68178e+08,2.24964e+15],[&amp;quot;+2^28&amp;quot;,5.36356e+08,4.49928e+15],[&amp;quot;+2^29&amp;quot;,1.07271e+09,8.99856e+15],[&amp;quot;+2^30&amp;quot;,2.14542e+09,1.79971e+16],[&amp;quot;+2^31&amp;quot;,4.29085e+09,3.59943e+16],[&amp;quot;+2^32&amp;quot;,8.58993e+09,7.20575e+16],[&amp;quot;+2^33&amp;quot;,1.71799e+10,1.44115e+17],[&amp;quot;+2^34&amp;quot;,3.43597e+10,2.8823e+17],[&amp;quot;+2^35&amp;quot;,6.87194e+10,5.7646e+17],[&amp;quot;+2^36&amp;quot;,1.37439e+11,1.15292e+18],[&amp;quot;+2^37&amp;quot;,2.74878e+11,2.30584e+18],[&amp;quot;+2^38&amp;quot;,5.49755e+11,4.61168e+18],[&amp;quot;+2^39&amp;quot;,1.09951e+12,9.22336e+18],[&amp;quot;+2^40&amp;quot;,2.19902e+12,1.84467e+19],[&amp;quot;+2^41&amp;quot;,4.39804e+12,3.68935e+19],[&amp;quot;+2^42&amp;quot;,8.79608e+12,7.37869e+19],[&amp;quot;+2^43&amp;quot;,1.75922e+13,1.47574e+20],[&amp;quot;+2^44&amp;quot;,3.51843e+13,2.95148e+20],[&amp;quot;+2^45&amp;quot;,7.03687e+13,5.90295e+20],[&amp;quot;+2^46&amp;quot;,1.40737e+14,1.18059e+21],[&amp;quot;+2^47&amp;quot;,2.81475e+14,2.36118e+21],[&amp;quot;+2^48&amp;quot;,5.62949e+14,4.72236e+21],[&amp;quot;+2^49&amp;quot;,1.1259e+15,9.44472e+21],[&amp;quot;+2^50&amp;quot;,2.2518e+15,1.88894e+22],[&amp;quot;+2^51&amp;quot;,4.5036e+15,3.77789e+22],[&amp;quot;+2^52&amp;quot;,9.00719e+15,7.55578e+22],[&amp;quot;+2^53&amp;quot;,1.80144e+16,1.51116e+23],[&amp;quot;+2^54&amp;quot;,3.60288e+16,3.02231e+23],[&amp;quot;+2^55&amp;quot;,7.20575e+16,6.04462e+23],[&amp;quot;+2^56&amp;quot;,1.44115e+17,1.20892e+24],[&amp;quot;+2^57&amp;quot;,2.8823e+17,2.41785e+24],[&amp;quot;+2^58&amp;quot;,5.7646e+17,4.8357e+24],[&amp;quot;+2^59&amp;quot;,1.15292e+18,9.6714e+24],[&amp;quot;+2^60&amp;quot;,2.30584e+18,1.93428e+25],[&amp;quot;+2^61&amp;quot;,4.61168e+18,3.86856e+25],[&amp;quot;+2^62&amp;quot;,9.22336e+18,7.73712e+25],[&amp;quot;+2^63&amp;quot;,1.84467e+19,1.54742e+26],[&amp;quot;+2^64&amp;quot;,0,0],[&amp;quot;+2^65&amp;quot;,0,0],[&amp;quot;+2^66&amp;quot;,0,0],[&amp;quot;+2^67&amp;quot;,0,0],[&amp;quot;+2^68&amp;quot;,0,0],[&amp;quot;+2^69&amp;quot;,0,0],[&amp;quot;+2^70&amp;quot;,0,0],[&amp;quot;+2^71&amp;quot;,0,0],[&amp;quot;+2^72&amp;quot;,0,0],[&amp;quot;+2^73&amp;quot;,0,0],[&amp;quot;+2^74&amp;quot;,0,0],[&amp;quot;+2^75&amp;quot;,0,0],[&amp;quot;+2^76&amp;quot;,0,0],[&amp;quot;+2^77&amp;quot;,0,0],[&amp;quot;+2^78&amp;quot;,0,0],[&amp;quot;+2^79&amp;quot;,0,0],[&amp;quot;+2^80&amp;quot;,0,0],[&amp;quot;+2^81&amp;quot;,0,0],[&amp;quot;+2^82&amp;quot;,0,0],[&amp;quot;+2^83&amp;quot;,0,0],[&amp;quot;+2^84&amp;quot;,0,0],[&amp;quot;+2^85&amp;quot;,0,0],[&amp;quot;+2^86&amp;quot;,0,0],[&amp;quot;+2^87&amp;quot;,0,0],[&amp;quot;+2^88&amp;quot;,0,0],[&amp;quot;+2^89&amp;quot;,0,0],[&amp;quot;+2^90&amp;quot;,0,0],[&amp;quot;+2^91&amp;quot;,0,0],[&amp;quot;+2^92&amp;quot;,0,0],[&amp;quot;+2^93&amp;quot;,0,0],[&amp;quot;+2^94&amp;quot;,0,0],[&amp;quot;+2^95&amp;quot;,0,0],[&amp;quot;+2^96&amp;quot;,0,0],[&amp;quot;+2^97&amp;quot;,0,0],[&amp;quot;+2^98&amp;quot;,0,0],[&amp;quot;+2^99&amp;quot;,0,0],[&amp;quot;+2^100&amp;quot;,0,0],[&amp;quot;+2^101&amp;quot;,0,0],[&amp;quot;+2^102&amp;quot;,0,0],[&amp;quot;+2^103&amp;quot;,0,0],[&amp;quot;+2^104&amp;quot;,0,0],[&amp;quot;+2^105&amp;quot;,0,0],[&amp;quot;+2^106&amp;quot;,0,0],[&amp;quot;+2^107&amp;quot;,0,0],[&amp;quot;+2^108&amp;quot;,0,0],[&amp;quot;+2^109&amp;quot;,0,0],[&amp;quot;+2^110&amp;quot;,0,0],[&amp;quot;+2^111&amp;quot;,0,0],[&amp;quot;+2^112&amp;quot;,0,0],[&amp;quot;+2^113&amp;quot;,0,0],[&amp;quot;+2^114&amp;quot;,0,0],[&amp;quot;+2^115&amp;quot;,0,0],[&amp;quot;+2^116&amp;quot;,0,0],[&amp;quot;+2^117&amp;quot;,0,0],[&amp;quot;+2^118&amp;quot;,0,0],[&amp;quot;+2^119&amp;quot;,0,0],[&amp;quot;+2^120&amp;quot;,0,0],[&amp;quot;+2^121&amp;quot;,0,0],[&amp;quot;+2^122&amp;quot;,0,0],[&amp;quot;+2^123&amp;quot;,0,0],[&amp;quot;+2^124&amp;quot;,0,0],[&amp;quot;+2^125&amp;quot;,0,0],[&amp;quot;+2^126&amp;quot;,0,0],[&amp;quot;+2^127&amp;quot;,0,0] ]&lt;br /&gt;
 }}&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Key observation:&#039;&#039;&#039;  &lt;br /&gt;
The Taylor approximation is highly accurate near zero but diverges extremely fast outside its convergence region.  &lt;br /&gt;
This makes it unsuitable as a standalone &amp;lt;code&amp;gt;tanh&amp;lt;/code&amp;gt; implementation, but very effective as a local approximation when combined with range-based dispatching.&lt;br /&gt;
&lt;br /&gt;
== Localized Approximation Functions ==&lt;br /&gt;
&lt;br /&gt;
Localized approximation functions are designed to provide highly accurate and computationally efficient evaluations of &amp;lt;code&amp;gt;tanh(x)&amp;lt;/code&amp;gt; within a restricted input range. &lt;br /&gt;
They are typically used as building blocks in range-based dispatch strategies, where different approximations are selected depending on the magnitude of the input value.&lt;br /&gt;
&lt;br /&gt;
By limiting the valid input range, these approximations can achieve very low ULP error with simple polynomial or rational expressions, avoiding expensive transcendental functions.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
!Id&lt;br /&gt;
!Formula&lt;br /&gt;
!ULP &amp;lt;= 2.0&lt;br /&gt;
!NaN&lt;br /&gt;
|-&lt;br /&gt;
|signum&lt;br /&gt;
|&amp;lt;math&amp;gt;\sgn{x}&amp;lt;/math&amp;gt;&lt;br /&gt;
| -inf &amp;lt; x &amp;lt; -8.31763&lt;br /&gt;
8.31763 &amp;lt; x &amp;lt; inf&lt;br /&gt;
| -&lt;br /&gt;
|-&lt;br /&gt;
|poly1&lt;br /&gt;
|&amp;lt;math&amp;gt;x&amp;lt;/math&amp;gt;&lt;br /&gt;
| -0.000704229 &amp;lt; x &amp;lt; 0.000704229&lt;br /&gt;
| -&lt;br /&gt;
|-&lt;br /&gt;
|poly3&lt;br /&gt;
|&amp;lt;math&amp;gt;x - \frac{1}{3} x^3&amp;lt;/math&amp;gt;&lt;br /&gt;
| -0.0291781 &amp;lt; x &amp;lt; 0.0291781 &lt;br /&gt;
| -&lt;br /&gt;
|-&lt;br /&gt;
|lambert7&lt;br /&gt;
|&amp;lt;math&amp;gt;\frac{x \cdot (135135 + x^2 \cdot (17325 + x^2 \cdot (378 + x^2)))}{135135 + x^2 \cdot (62370 + x^2 \cdot (3150 + 28 \cdot x^2))}&lt;br /&gt;
&amp;lt;/math&amp;gt;&lt;br /&gt;
| -0.000947416 &amp;lt; x &amp;lt; 0.000947207&lt;br /&gt;
| -inf &amp;lt; x &amp;lt; -1.51629e+06&lt;br /&gt;
1.51629e+06 &amp;lt; x &amp;lt; inf&lt;br /&gt;
|-&lt;br /&gt;
|pade&lt;br /&gt;
|&amp;lt;math&amp;gt;x + x \cdot \frac{x^2 \cdot \left( p_1 \cdot x^2 + p_0 \right)}{\left( x^2 + q_1 \right) \cdot x^2 + q_0} &lt;br /&gt;
&lt;br /&gt;
&amp;lt;/math&amp;gt; &amp;lt;math&amp;gt;\begin{align}&lt;br /&gt;
p_0 &amp;amp;= -0.2059432032 \\&lt;br /&gt;
p_1 &amp;amp;= -0.0009577527 \\&lt;br /&gt;
q_0 &amp;amp;= 0.6178299136 \\ &lt;br /&gt;
q_1 &amp;amp;= 0.25&lt;br /&gt;
\end{align}&lt;br /&gt;
&amp;lt;/math&amp;gt;&lt;br /&gt;
| -0.0016973 &amp;lt; x &amp;lt; 0.0016973 &lt;br /&gt;
| -inf &amp;lt; x &amp;lt; -6.14896e+18&lt;br /&gt;
6.14896e+18 &amp;lt; x &amp;lt; inf&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
The functions listed above are not intended to be used independently over the full input domain. &lt;br /&gt;
Instead, they are commonly combined with explicit range checks and sign handling to form a complete &amp;lt;code&amp;gt;tanh&amp;lt;/code&amp;gt; implementation.&lt;br /&gt;
&lt;br /&gt;
For example, low-order polynomials are well suited for very small &amp;lt;math&amp;gt;|x|&amp;lt;/math&amp;gt;, while rational or higher-order approximations extend the usable range before switching to exponential-based formulations or saturation.&lt;br /&gt;
&lt;br /&gt;
==Recommended Implementation Strategy==&lt;br /&gt;
A robust and high-performance &amp;lt;code&amp;gt;tanh(x)&amp;lt;/code&amp;gt; implementation typically combines multiple techniques, each optimized for a specific input range. No single formulation provides optimal accuracy and performance across the entire floating-point domain.&lt;br /&gt;
&lt;br /&gt;
A common strategy is based on range-dependent dispatching:&lt;br /&gt;
*&#039;&#039;&#039;Sign handling&#039;&#039;&#039;&lt;br /&gt;
: Use the odd symmetry of &amp;lt;code&amp;gt;tanh&amp;lt;/code&amp;gt; (&amp;lt;math&amp;gt;\tanh(-x) = -\tanh(x)&amp;lt;/math&amp;gt;) to reduce the implementation to non-negative inputs.*&#039;&#039;&#039;Very small inputs (&amp;lt;math&amp;gt;|x| \ll 1&amp;lt;/math&amp;gt;)&#039;&#039;&#039;&lt;br /&gt;
: Use a low-order polynomial or Taylor approximation, exploiting &amp;lt;math&amp;gt;\tanh(x) \approx x&amp;lt;/math&amp;gt; to avoid cancellation and expensive transcendental functions.&lt;br /&gt;
*&#039;&#039;&#039;Small to medium inputs&#039;&#039;&#039;&lt;br /&gt;
: Apply a localized polynomial or rational approximation with bounded ULP error.&lt;br /&gt;
*&#039;&#039;&#039;Medium to large inputs&#039;&#039;&#039;&lt;br /&gt;
: Use an &amp;lt;code&amp;gt;expm1&amp;lt;/code&amp;gt;-based formulation to maintain numerical stability while avoiding catastrophic cancellation.&lt;br /&gt;
*&#039;&#039;&#039;Very large inputs&#039;&#039;&#039;&lt;br /&gt;
: Directly return &amp;lt;code&amp;gt;±1&amp;lt;/code&amp;gt;, as &amp;lt;code&amp;gt;tanh(x)&amp;lt;/code&amp;gt; saturates rapidly and additional computation provides no meaningful benefit.&lt;br /&gt;
This hybrid approach minimizes ULP error, avoids NaN generation, and allows fine-grained control over the accuracy–performance trade-off. It is widely used in optimized math libraries and hardware-accelerated implementations.&lt;br /&gt;
&lt;br /&gt;
== See Also ==&lt;br /&gt;
&lt;br /&gt;
* [[ULP Difference of Float Numbers]]&lt;br /&gt;
&lt;br /&gt;
== External Links ==&lt;br /&gt;
&lt;br /&gt;
* https://yaikhom.com/2020-04-28-localised-approximation-of-hyperbolic-tangents.html#mjx-eqn-eqn7-th%20degree%20lambert%20approximant&lt;br /&gt;
*https://github.com/llvm/llvm-project/blob/main/libc/src/math/generic/tanhf.cpp&lt;br /&gt;
*https://forums.developer.nvidia.com/t/hardware-accelerated-tanh-on-turing/173291&lt;br /&gt;
[[Category:Numerical Precision]]&lt;/div&gt;</summary>
		<author><name>Timo.stripf</name></author>
	</entry>
	<entry>
		<id>https://www.emmtrix.com/w139/index.php?title=tanh_Software_Implementation&amp;diff=2937</id>
		<title>tanh Software Implementation</title>
		<link rel="alternate" type="text/html" href="https://www.emmtrix.com/w139/index.php?title=tanh_Software_Implementation&amp;diff=2937"/>
		<updated>2026-02-04T12:14:40Z</updated>

		<summary type="html">&lt;p&gt;Timo.stripf: /* Recommended Implementation Strategy */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;[[File:Hyperbolic Tangent.svg|thumb]]&lt;br /&gt;
The hyperbolic tangent function, commonly referred to as &amp;lt;code&amp;gt;tanh&amp;lt;/code&amp;gt;, is a mathematical function that maps real numbers to the range &amp;lt;math&amp;gt;(-1, 1)&amp;lt;/math&amp;gt;. It is defined as:&lt;br /&gt;
&lt;br /&gt;
:&amp;lt;math&amp;gt;\tanh(x) = \frac{e^x - e^{-x}}{e^x + e^{-x}}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
From a software implementation perspective, &amp;lt;code&amp;gt;tanh&amp;lt;/code&amp;gt; is a numerically challenging function due to the use of exponential functions, which can easily overflow for large inputs and suffer from catastrophic cancellation for very small values of &amp;lt;math&amp;gt;x&amp;lt;/math&amp;gt;. These issues are particularly relevant in single-precision floating-point (&amp;lt;code&amp;gt;float32&amp;lt;/code&amp;gt;) implementations, where both accuracy and performance are critical.&lt;br /&gt;
&lt;br /&gt;
In machine learning and deep learning, &amp;lt;code&amp;gt;tanh&amp;lt;/code&amp;gt; is widely used as an activation function because it introduces non-linearity and produces outputs centered around zero, which can improve gradient-based optimization. As a result, &amp;lt;code&amp;gt;tanh&amp;lt;/code&amp;gt; is often evaluated billions of times during training and inference, making efficient and numerically stable implementations essential.&lt;br /&gt;
&lt;br /&gt;
This article focuses on practical software implementations of &amp;lt;code&amp;gt;tanh&amp;lt;/code&amp;gt;, analyzing different computational formulations with respect to floating-point accuracy, ULP error, overflow behavior, and NaN generation. The goal is to provide a foundation for designing robust &amp;lt;code&amp;gt;tanh&amp;lt;/code&amp;gt; implementations suitable for performance-critical environments such as numerical libraries, embedded systems, and AI workloads.&lt;br /&gt;
&lt;br /&gt;
== Problem Overview ==&lt;br /&gt;
The hyperbolic tangent function poses several challenges when implemented in finite-precision floating-point arithmetic. While the mathematical definition is simple, direct translations into software often suffer from numerical instability and performance issues.&lt;br /&gt;
&lt;br /&gt;
A primary challenge is the use of exponential functions. For large input values, &amp;lt;code&amp;gt;exp(x)&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;exp(-x)&amp;lt;/code&amp;gt; can overflow in single-precision floating-point arithmetic, leading to infinities and, in some formulations, NaN results. Avoiding overflow therefore requires careful reformulation or explicit range handling.&lt;br /&gt;
&lt;br /&gt;
For very small input values, catastrophic cancellation becomes the dominant issue. Expressions such as &amp;lt;math&amp;gt;e^x - e^{-x}&amp;lt;/math&amp;gt; involve the subtraction of nearly equal numbers, causing a severe loss of significant bits and large ULP errors. In these regions, naive implementations may return zero or highly inaccurate results even though &amp;lt;math&amp;gt;\tanh(x) \approx x&amp;lt;/math&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
In addition to accuracy concerns, performance is a critical factor. In many applications, especially machine learning and numerical simulations, &amp;lt;code&amp;gt;tanh&amp;lt;/code&amp;gt; is evaluated extremely frequently. This makes it necessary to balance numerical robustness with the cost of transcendental function calls, branch complexity, and instruction-level efficiency.&lt;br /&gt;
&lt;br /&gt;
==Direct Exponential Formulations==&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+&lt;br /&gt;
!Id&lt;br /&gt;
!Formula&lt;br /&gt;
!Highest ULP Error&lt;br /&gt;
!ULP &amp;lt;= 2.0&lt;br /&gt;
!NaN&lt;br /&gt;
|-&lt;br /&gt;
|exp_v1&lt;br /&gt;
|&amp;lt;math&amp;gt;\frac{e^x - e^{-x}}{e^x + e^{-x}}&amp;lt;/math&amp;gt;&lt;br /&gt;
|x &amp;lt; 0: 8.55638e+08 at -2.98023e-08 with 0x0p+0 instead of -0x1p-25&lt;br /&gt;
&lt;br /&gt;
x &amp;gt; 0: 8.55638e+08 at 2.98023e-08 with 0x0p+0 instead of 0x1p-25&lt;br /&gt;
| -88.7246 &amp;lt; x &amp;lt; -7.14197&lt;br /&gt;
7.14197 &amp;lt; x &amp;lt; 88.7246&lt;br /&gt;
| -inf &amp;lt; x &amp;lt; -88.7246&lt;br /&gt;
88.7246 &amp;lt; x &amp;lt; inf&lt;br /&gt;
|-&lt;br /&gt;
|exp_v2&lt;br /&gt;
|&amp;lt;math&amp;gt;\frac{e^{2x} - 1}{e^{2x} + 1}&amp;lt;/math&amp;gt;&lt;br /&gt;
|x &amp;lt; 0: 8.47249e+08 at -1.49012e-08 with 0x0p+0 instead of -0x1p-26&lt;br /&gt;
x &amp;gt; 0: 8.55638e+08 at 2.98023e-08 with 0x0p+0 instead of 0x1.fffffep-26&lt;br /&gt;
| -inf &amp;lt; x &amp;lt; -0.547821&lt;br /&gt;
0.254822 &amp;lt; x &amp;lt; 8.31787&lt;br /&gt;
&lt;br /&gt;
8.38379 &amp;lt; x &amp;lt; 44.3623&lt;br /&gt;
|44.3623 &amp;lt; x &amp;lt; inf&lt;br /&gt;
|-&lt;br /&gt;
|exp_v3&lt;br /&gt;
|&amp;lt;math&amp;gt;1 - \frac{2}{e^{2x} + 1}&amp;lt;/math&amp;gt;&lt;br /&gt;
|x &amp;lt; 0: 8.59832e+08 at -4.47035e-08 with 0x0p+0 instead of -0x1.8p-25&lt;br /&gt;
x &amp;gt; 0: 8.68221e+08 at 8.9407e-08 with 0x0p+0 instead of 0x1.7ffffcp-24&lt;br /&gt;
| -inf &amp;lt; x &amp;lt; -6.26233&lt;br /&gt;
0.346382 &amp;lt; x &amp;lt; inf&lt;br /&gt;
| -&lt;br /&gt;
|-&lt;br /&gt;
|expm1&lt;br /&gt;
|&amp;lt;math&amp;gt;\frac{expm1(2x)}{expm1(2x) + 2}&amp;lt;/math&amp;gt; &lt;br /&gt;
|x &amp;lt; 0: 2.49585 at -3.95627 with -0x1.ffa00ap-1 instead of -0x1.ffa00ep-1&lt;br /&gt;
x &amp;gt; 0: 2.41657 at 0.0155837 with 0x1.fe9b66p-7 instead of 0x1.fe9b62p-7&lt;br /&gt;
| -inf &amp;lt; x &amp;lt; -5.57227&lt;br /&gt;
-0.42981 &amp;lt; x &amp;lt; 0.00777948&lt;br /&gt;
&lt;br /&gt;
0.125351 &amp;lt; x &amp;lt; 44.3623&lt;br /&gt;
|44.3623 &amp;lt; x &amp;lt; inf&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
The first equation is the definition of the hyperbolic tangent function. This equation requires two exponentials with different values. Since exponentials are expensive to compute, we can use the second equation to reduce the number of exponentials to one.&lt;br /&gt;
&lt;br /&gt;
The second equation has the drawback that both the numerator and the denominator can become infinity for large values of x which results in NaN. That happens for &amp;lt;math&amp;gt;|x| &amp;gt; \log(FLT\_MAX)/2 \approx 44&amp;lt;/math&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
The third equation solves this problem but has a slightly higher error for &amp;lt;math&amp;gt;x \approx 2^{-25}&amp;lt;/math&amp;gt;. In the following graph you can see the error chart for equation 3. The error charts for the other two equations are very similar. All three equations have an error of up to 16 mio ULPs for &amp;lt;math&amp;gt;x &amp;lt; 2^{-5}&amp;lt;/math&amp;gt; which means that up to 24 bits could be wrong.&lt;br /&gt;
&lt;br /&gt;
=== exp_v2 ===&lt;br /&gt;
&lt;br /&gt;
The following error graph shows the ULP error of the &amp;lt;code&amp;gt;exp_v2&amp;lt;/code&amp;gt; formulation across the full &amp;lt;code&amp;gt;float32&amp;lt;/code&amp;gt; input range. &lt;br /&gt;
This formulation reduces the number of exponential evaluations to one, but is particularly sensitive to catastrophic cancellation for very small input values and to overflow for large &amp;lt;math&amp;gt;|x|&amp;lt;/math&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
{{#widget:FloatErrorGraph|id=tanhf_exp_v2&lt;br /&gt;
  |data=[ [&amp;quot;-2^127&amp;quot;,0,1.17549e-38],[&amp;quot;-2^126&amp;quot;,0,1.17549e-38],[&amp;quot;-2^125&amp;quot;,0,1.17549e-38],[&amp;quot;-2^124&amp;quot;,0,1.17549e-38],[&amp;quot;-2^123&amp;quot;,0,1.17549e-38],[&amp;quot;-2^122&amp;quot;,0,1.17549e-38],[&amp;quot;-2^121&amp;quot;,0,1.17549e-38],[&amp;quot;-2^120&amp;quot;,0,1.17549e-38],[&amp;quot;-2^119&amp;quot;,0,1.17549e-38],[&amp;quot;-2^118&amp;quot;,0,1.17549e-38],[&amp;quot;-2^117&amp;quot;,0,1.17549e-38],[&amp;quot;-2^116&amp;quot;,0,1.17549e-38],[&amp;quot;-2^115&amp;quot;,0,1.17549e-38],[&amp;quot;-2^114&amp;quot;,0,1.17549e-38],[&amp;quot;-2^113&amp;quot;,0,1.17549e-38],[&amp;quot;-2^112&amp;quot;,0,1.17549e-38],[&amp;quot;-2^111&amp;quot;,0,1.17549e-38],[&amp;quot;-2^110&amp;quot;,0,1.17549e-38],[&amp;quot;-2^109&amp;quot;,0,1.17549e-38],[&amp;quot;-2^108&amp;quot;,0,1.17549e-38],[&amp;quot;-2^107&amp;quot;,0,1.17549e-38],[&amp;quot;-2^106&amp;quot;,0,1.17549e-38],[&amp;quot;-2^105&amp;quot;,0,1.17549e-38],[&amp;quot;-2^104&amp;quot;,0,1.17549e-38],[&amp;quot;-2^103&amp;quot;,0,1.17549e-38],[&amp;quot;-2^102&amp;quot;,0,1.17549e-38],[&amp;quot;-2^101&amp;quot;,0,1.17549e-38],[&amp;quot;-2^100&amp;quot;,0,1.17549e-38],[&amp;quot;-2^99&amp;quot;,0,1.17549e-38],[&amp;quot;-2^98&amp;quot;,0,1.17549e-38],[&amp;quot;-2^97&amp;quot;,0,1.17549e-38],[&amp;quot;-2^96&amp;quot;,0,1.17549e-38],[&amp;quot;-2^95&amp;quot;,0,1.17549e-38],[&amp;quot;-2^94&amp;quot;,0,1.17549e-38],[&amp;quot;-2^93&amp;quot;,0,1.17549e-38],[&amp;quot;-2^92&amp;quot;,0,1.17549e-38],[&amp;quot;-2^91&amp;quot;,0,1.17549e-38],[&amp;quot;-2^90&amp;quot;,0,1.17549e-38],[&amp;quot;-2^89&amp;quot;,0,1.17549e-38],[&amp;quot;-2^88&amp;quot;,0,1.17549e-38],[&amp;quot;-2^87&amp;quot;,0,1.17549e-38],[&amp;quot;-2^86&amp;quot;,0,1.17549e-38],[&amp;quot;-2^85&amp;quot;,0,1.17549e-38],[&amp;quot;-2^84&amp;quot;,0,1.17549e-38],[&amp;quot;-2^83&amp;quot;,0,1.17549e-38],[&amp;quot;-2^82&amp;quot;,0,1.17549e-38],[&amp;quot;-2^81&amp;quot;,0,1.17549e-38],[&amp;quot;-2^80&amp;quot;,0,1.17549e-38],[&amp;quot;-2^79&amp;quot;,0,1.17549e-38],[&amp;quot;-2^78&amp;quot;,0,1.17549e-38],[&amp;quot;-2^77&amp;quot;,0,1.17549e-38],[&amp;quot;-2^76&amp;quot;,0,1.17549e-38],[&amp;quot;-2^75&amp;quot;,0,1.17549e-38],[&amp;quot;-2^74&amp;quot;,0,1.17549e-38],[&amp;quot;-2^73&amp;quot;,0,1.17549e-38],[&amp;quot;-2^72&amp;quot;,0,1.17549e-38],[&amp;quot;-2^71&amp;quot;,0,1.17549e-38],[&amp;quot;-2^70&amp;quot;,0,1.17549e-38],[&amp;quot;-2^69&amp;quot;,0,1.17549e-38],[&amp;quot;-2^68&amp;quot;,0,1.17549e-38],[&amp;quot;-2^67&amp;quot;,0,1.17549e-38],[&amp;quot;-2^66&amp;quot;,0,1.17549e-38],[&amp;quot;-2^65&amp;quot;,0,1.17549e-38],[&amp;quot;-2^64&amp;quot;,0,1.17549e-38],[&amp;quot;-2^63&amp;quot;,0,1.17549e-38],[&amp;quot;-2^62&amp;quot;,0,1.17549e-38],[&amp;quot;-2^61&amp;quot;,0,1.17549e-38],[&amp;quot;-2^60&amp;quot;,0,1.17549e-38],[&amp;quot;-2^59&amp;quot;,0,1.17549e-38],[&amp;quot;-2^58&amp;quot;,0,1.17549e-38],[&amp;quot;-2^57&amp;quot;,0,1.17549e-38],[&amp;quot;-2^56&amp;quot;,0,1.17549e-38],[&amp;quot;-2^55&amp;quot;,0,1.17549e-38],[&amp;quot;-2^54&amp;quot;,0,1.17549e-38],[&amp;quot;-2^53&amp;quot;,0,1.17549e-38],[&amp;quot;-2^52&amp;quot;,0,1.17549e-38],[&amp;quot;-2^51&amp;quot;,0,1.17549e-38],[&amp;quot;-2^50&amp;quot;,0,1.17549e-38],[&amp;quot;-2^49&amp;quot;,0,1.17549e-38],[&amp;quot;-2^48&amp;quot;,0,1.17549e-38],[&amp;quot;-2^47&amp;quot;,0,1.17549e-38],[&amp;quot;-2^46&amp;quot;,0,1.17549e-38],[&amp;quot;-2^45&amp;quot;,0,1.17549e-38],[&amp;quot;-2^44&amp;quot;,0,1.17549e-38],[&amp;quot;-2^43&amp;quot;,0,1.17549e-38],[&amp;quot;-2^42&amp;quot;,0,1.17549e-38],[&amp;quot;-2^41&amp;quot;,0,1.17549e-38],[&amp;quot;-2^40&amp;quot;,0,1.17549e-38],[&amp;quot;-2^39&amp;quot;,0,1.17549e-38],[&amp;quot;-2^38&amp;quot;,0,1.17549e-38],[&amp;quot;-2^37&amp;quot;,0,1.17549e-38],[&amp;quot;-2^36&amp;quot;,0,1.17549e-38],[&amp;quot;-2^35&amp;quot;,0,1.17549e-38],[&amp;quot;-2^34&amp;quot;,0,1.17549e-38],[&amp;quot;-2^33&amp;quot;,0,1.17549e-38],[&amp;quot;-2^32&amp;quot;,0,1.17549e-38],[&amp;quot;-2^31&amp;quot;,0,1.17549e-38],[&amp;quot;-2^30&amp;quot;,0,1.17549e-38],[&amp;quot;-2^29&amp;quot;,0,1.17549e-38],[&amp;quot;-2^28&amp;quot;,0,1.17549e-38],[&amp;quot;-2^27&amp;quot;,0,1.17549e-38],[&amp;quot;-2^26&amp;quot;,0,1.17549e-38],[&amp;quot;-2^25&amp;quot;,0,1.17549e-38],[&amp;quot;-2^24&amp;quot;,0,1.17549e-38],[&amp;quot;-2^23&amp;quot;,0,1.17549e-38],[&amp;quot;-2^22&amp;quot;,0,1.17549e-38],[&amp;quot;-2^21&amp;quot;,0,1.17549e-38],[&amp;quot;-2^20&amp;quot;,0,1.17549e-38],[&amp;quot;-2^19&amp;quot;,0,1.17549e-38],[&amp;quot;-2^18&amp;quot;,0,1.17549e-38],[&amp;quot;-2^17&amp;quot;,0,1.17549e-38],[&amp;quot;-2^16&amp;quot;,0,1.17549e-38],[&amp;quot;-2^15&amp;quot;,0,1.17549e-38],[&amp;quot;-2^14&amp;quot;,0,1.17549e-38],[&amp;quot;-2^13&amp;quot;,0,1.17549e-38],[&amp;quot;-2^12&amp;quot;,0,1.17549e-38],[&amp;quot;-2^11&amp;quot;,0,1.17549e-38],[&amp;quot;-2^10&amp;quot;,0,1.17549e-38],[&amp;quot;-2^9&amp;quot;,0,1.17549e-38],[&amp;quot;-2^8&amp;quot;,0,1.17549e-38],[&amp;quot;-2^7&amp;quot;,0,1.17549e-38],[&amp;quot;-2^6&amp;quot;,0,1.17549e-38],[&amp;quot;-2^5&amp;quot;,0,1.17549e-38],[&amp;quot;-2^4&amp;quot;,2.53131e-14,4.24683e-07],[&amp;quot;-2^3&amp;quot;,5.95888e-08,0.999734],[&amp;quot;-2^2&amp;quot;,8.86696e-08,1.48763],[&amp;quot;-2^1&amp;quot;,8.91609e-08,1.49587],[&amp;quot;-2^0&amp;quot;,8.73743e-08,1.4659],[&amp;quot;-2^-1&amp;quot;,8.65382e-08,2.13222],[&amp;quot;-2^-2&amp;quot;,6.18481e-08,2.61428],[&amp;quot;-2^-3&amp;quot;,3.88071e-08,3.08594],[&amp;quot;-2^-4&amp;quot;,2.6234e-08,4.23594],[&amp;quot;-2^-5&amp;quot;,2.02799e-08,8.19726],[&amp;quot;-2^-6&amp;quot;,1.75183e-08,12.4495],[&amp;quot;-2^-7&amp;quot;,1.61937e-08,17.3878],[&amp;quot;-2^-8&amp;quot;,1.5566e-08,33.4278],[&amp;quot;-2^-9&amp;quot;,1.51688e-08,65.1495],[&amp;quot;-2^-10&amp;quot;,1.50281e-08,129.09],[&amp;quot;-2^-11&amp;quot;,1.49613e-08,257.033],[&amp;quot;-2^-12&amp;quot;,1.49296e-08,512.977],[&amp;quot;-2^-13&amp;quot;,1.49174e-08,1025.12],[&amp;quot;-2^-14&amp;quot;,1.4909e-08,2049.08],[&amp;quot;-2^-15&amp;quot;,1.49049e-08,4097.02],[&amp;quot;-2^-16&amp;quot;,1.4903e-08,8193],[&amp;quot;-2^-17&amp;quot;,1.49021e-08,16385],[&amp;quot;-2^-18&amp;quot;,1.48862e-08,32735],[&amp;quot;-2^-19&amp;quot;,1.48973e-08,65519],[&amp;quot;-2^-20&amp;quot;,1.49001e-08,131063],[&amp;quot;-2^-21&amp;quot;,1.49009e-08,262139],[&amp;quot;-2^-22&amp;quot;,1.49011e-08,524285],[&amp;quot;-2^-23&amp;quot;,1.49011e-08,1.04857e+06],[&amp;quot;-2^-24&amp;quot;,1.49011e-08,2.09715e+06],[&amp;quot;-2^-25&amp;quot;,1.49012e-08,4.1943e+06],[&amp;quot;-2^-26&amp;quot;,1.49007e-08,8.38835e+06],[&amp;quot;-2^-27&amp;quot;,1.49012e-08,8.47249e+08],[&amp;quot;-2^-28&amp;quot;,7.45058e-09,8.38861e+08],[&amp;quot;-2^-29&amp;quot;,3.72529e-09,8.30472e+08],[&amp;quot;-2^-30&amp;quot;,1.86265e-09,8.22084e+08],[&amp;quot;-2^-31&amp;quot;,9.31323e-10,8.13695e+08],[&amp;quot;-2^-32&amp;quot;,4.65661e-10,8.05306e+08],[&amp;quot;-2^-33&amp;quot;,2.32831e-10,7.96918e+08],[&amp;quot;-2^-34&amp;quot;,1.16415e-10,7.88529e+08],[&amp;quot;-2^-35&amp;quot;,5.82077e-11,7.80141e+08],[&amp;quot;-2^-36&amp;quot;,2.91038e-11,7.71752e+08],[&amp;quot;-2^-37&amp;quot;,1.45519e-11,7.63363e+08],[&amp;quot;-2^-38&amp;quot;,7.27596e-12,7.54975e+08],[&amp;quot;-2^-39&amp;quot;,3.63798e-12,7.46586e+08],[&amp;quot;-2^-40&amp;quot;,1.81899e-12,7.38198e+08],[&amp;quot;-2^-41&amp;quot;,9.09495e-13,7.29809e+08],[&amp;quot;-2^-42&amp;quot;,4.54747e-13,7.2142e+08],[&amp;quot;-2^-43&amp;quot;,2.27374e-13,7.13032e+08],[&amp;quot;-2^-44&amp;quot;,1.13687e-13,7.04643e+08],[&amp;quot;-2^-45&amp;quot;,5.68434e-14,6.96254e+08],[&amp;quot;-2^-46&amp;quot;,2.84217e-14,6.87866e+08],[&amp;quot;-2^-47&amp;quot;,1.42109e-14,6.79477e+08],[&amp;quot;-2^-48&amp;quot;,7.10543e-15,6.71089e+08],[&amp;quot;-2^-49&amp;quot;,3.55271e-15,6.627e+08],[&amp;quot;-2^-50&amp;quot;,1.77636e-15,6.54311e+08],[&amp;quot;-2^-51&amp;quot;,8.88178e-16,6.45923e+08],[&amp;quot;-2^-52&amp;quot;,4.44089e-16,6.37534e+08],[&amp;quot;-2^-53&amp;quot;,2.22045e-16,6.29146e+08],[&amp;quot;-2^-54&amp;quot;,1.11022e-16,6.20757e+08],[&amp;quot;-2^-55&amp;quot;,5.55111e-17,6.12368e+08],[&amp;quot;-2^-56&amp;quot;,2.77556e-17,6.0398e+08],[&amp;quot;-2^-57&amp;quot;,1.38778e-17,5.95591e+08],[&amp;quot;-2^-58&amp;quot;,6.93889e-18,5.87203e+08],[&amp;quot;-2^-59&amp;quot;,3.46945e-18,5.78814e+08],[&amp;quot;-2^-60&amp;quot;,1.73472e-18,5.70425e+08],[&amp;quot;-2^-61&amp;quot;,8.67362e-19,5.62037e+08],[&amp;quot;-2^-62&amp;quot;,4.33681e-19,5.53648e+08],[&amp;quot;-2^-63&amp;quot;,2.1684e-19,5.4526e+08],[&amp;quot;-2^-64&amp;quot;,1.0842e-19,5.36871e+08],[&amp;quot;-2^-65&amp;quot;,5.42101e-20,5.28482e+08],[&amp;quot;-2^-66&amp;quot;,2.71051e-20,5.20094e+08],[&amp;quot;-2^-67&amp;quot;,1.35525e-20,5.11705e+08],[&amp;quot;-2^-68&amp;quot;,6.77626e-21,5.03316e+08],[&amp;quot;-2^-69&amp;quot;,3.38813e-21,4.94928e+08],[&amp;quot;-2^-70&amp;quot;,1.69407e-21,4.86539e+08],[&amp;quot;-2^-71&amp;quot;,8.47033e-22,4.78151e+08],[&amp;quot;-2^-72&amp;quot;,4.23516e-22,4.69762e+08],[&amp;quot;-2^-73&amp;quot;,2.11758e-22,4.61373e+08],[&amp;quot;-2^-74&amp;quot;,1.05879e-22,4.52985e+08],[&amp;quot;-2^-75&amp;quot;,5.29396e-23,4.44596e+08],[&amp;quot;-2^-76&amp;quot;,2.64698e-23,4.36208e+08],[&amp;quot;-2^-77&amp;quot;,1.32349e-23,4.27819e+08],[&amp;quot;-2^-78&amp;quot;,6.61744e-24,4.1943e+08],[&amp;quot;-2^-79&amp;quot;,3.30872e-24,4.11042e+08],[&amp;quot;-2^-80&amp;quot;,1.65436e-24,4.02653e+08],[&amp;quot;-2^-81&amp;quot;,8.27181e-25,3.94265e+08],[&amp;quot;-2^-82&amp;quot;,4.1359e-25,3.85876e+08],[&amp;quot;-2^-83&amp;quot;,2.06795e-25,3.77487e+08],[&amp;quot;-2^-84&amp;quot;,1.03398e-25,3.69099e+08],[&amp;quot;-2^-85&amp;quot;,5.16988e-26,3.6071e+08],[&amp;quot;-2^-86&amp;quot;,2.58494e-26,3.52322e+08],[&amp;quot;-2^-87&amp;quot;,1.29247e-26,3.43933e+08],[&amp;quot;-2^-88&amp;quot;,6.46235e-27,3.35544e+08],[&amp;quot;-2^-89&amp;quot;,3.23117e-27,3.27156e+08],[&amp;quot;-2^-90&amp;quot;,1.61559e-27,3.18767e+08],[&amp;quot;-2^-91&amp;quot;,8.07794e-28,3.10378e+08],[&amp;quot;-2^-92&amp;quot;,4.03897e-28,3.0199e+08],[&amp;quot;-2^-93&amp;quot;,2.01948e-28,2.93601e+08],[&amp;quot;-2^-94&amp;quot;,1.00974e-28,2.85213e+08],[&amp;quot;-2^-95&amp;quot;,5.04871e-29,2.76824e+08],[&amp;quot;-2^-96&amp;quot;,2.52435e-29,2.68435e+08],[&amp;quot;-2^-97&amp;quot;,1.26218e-29,2.60047e+08],[&amp;quot;-2^-98&amp;quot;,6.31089e-30,2.51658e+08],[&amp;quot;-2^-99&amp;quot;,3.15544e-30,2.4327e+08],[&amp;quot;-2^-100&amp;quot;,1.57772e-30,2.34881e+08],[&amp;quot;-2^-101&amp;quot;,7.88861e-31,2.26492e+08],[&amp;quot;-2^-102&amp;quot;,3.9443e-31,2.18104e+08],[&amp;quot;-2^-103&amp;quot;,1.97215e-31,2.09715e+08],[&amp;quot;-2^-104&amp;quot;,9.86076e-32,2.01327e+08],[&amp;quot;-2^-105&amp;quot;,4.93038e-32,1.92938e+08],[&amp;quot;-2^-106&amp;quot;,2.46519e-32,1.84549e+08],[&amp;quot;-2^-107&amp;quot;,1.2326e-32,1.76161e+08],[&amp;quot;-2^-108&amp;quot;,6.16298e-33,1.67772e+08],[&amp;quot;-2^-109&amp;quot;,3.08149e-33,1.59384e+08],[&amp;quot;-2^-110&amp;quot;,1.54074e-33,1.50995e+08],[&amp;quot;-2^-111&amp;quot;,7.70372e-34,1.42606e+08],[&amp;quot;-2^-112&amp;quot;,3.85186e-34,1.34218e+08],[&amp;quot;-2^-113&amp;quot;,1.92593e-34,1.25829e+08],[&amp;quot;-2^-114&amp;quot;,9.62965e-35,1.17441e+08],[&amp;quot;-2^-115&amp;quot;,4.81482e-35,1.09052e+08],[&amp;quot;-2^-116&amp;quot;,2.40741e-35,1.00663e+08],[&amp;quot;-2^-117&amp;quot;,1.20371e-35,9.22747e+07],[&amp;quot;-2^-118&amp;quot;,6.01853e-36,8.38861e+07],[&amp;quot;-2^-119&amp;quot;,3.00927e-36,7.54975e+07],[&amp;quot;-2^-120&amp;quot;,1.50463e-36,6.71089e+07],[&amp;quot;-2^-121&amp;quot;,7.52316e-37,5.87203e+07],[&amp;quot;-2^-122&amp;quot;,3.76158e-37,5.03316e+07],[&amp;quot;-2^-123&amp;quot;,1.88079e-37,4.1943e+07],[&amp;quot;-2^-124&amp;quot;,9.40395e-38,3.35544e+07],[&amp;quot;-2^-125&amp;quot;,4.70198e-38,2.51658e+07],[&amp;quot;-2^-126&amp;quot;,2.35099e-38,1.67772e+07],[&amp;quot;-SN&amp;quot;,1.17546e-38,8.38835e+06],[&amp;quot;0&amp;quot;,0,1.17549e-38],[&amp;quot;+SN&amp;quot;,1.17546e-38,8.38835e+06],[&amp;quot;+2^-126&amp;quot;,2.35095e-38,1.6777e+07],[&amp;quot;+2^-125&amp;quot;,4.70191e-38,2.51656e+07],[&amp;quot;+2^-124&amp;quot;,9.40381e-38,3.35542e+07],[&amp;quot;+2^-123&amp;quot;,1.88076e-37,4.19428e+07],[&amp;quot;+2^-122&amp;quot;,3.76152e-37,5.03314e+07],[&amp;quot;+2^-121&amp;quot;,7.52305e-37,5.872e+07],[&amp;quot;+2^-120&amp;quot;,1.50461e-36,6.71086e+07],[&amp;quot;+2^-119&amp;quot;,3.00922e-36,7.54972e+07],[&amp;quot;+2^-118&amp;quot;,6.01844e-36,8.38858e+07],[&amp;quot;+2^-117&amp;quot;,1.20369e-35,9.22744e+07],[&amp;quot;+2^-116&amp;quot;,2.40738e-35,1.00663e+08],[&amp;quot;+2^-115&amp;quot;,4.81475e-35,1.09052e+08],[&amp;quot;+2^-114&amp;quot;,9.6295e-35,1.1744e+08],[&amp;quot;+2^-113&amp;quot;,1.9259e-34,1.25829e+08],[&amp;quot;+2^-112&amp;quot;,3.8518e-34,1.34217e+08],[&amp;quot;+2^-111&amp;quot;,7.7036e-34,1.42606e+08],[&amp;quot;+2^-110&amp;quot;,1.54072e-33,1.50995e+08],[&amp;quot;+2^-109&amp;quot;,3.08144e-33,1.59383e+08],[&amp;quot;+2^-108&amp;quot;,6.16288e-33,1.67772e+08],[&amp;quot;+2^-107&amp;quot;,1.23258e-32,1.76161e+08],[&amp;quot;+2^-106&amp;quot;,2.46515e-32,1.84549e+08],[&amp;quot;+2^-105&amp;quot;,4.93031e-32,1.92938e+08],[&amp;quot;+2^-104&amp;quot;,9.86061e-32,2.01326e+08],[&amp;quot;+2^-103&amp;quot;,1.97212e-31,2.09715e+08],[&amp;quot;+2^-102&amp;quot;,3.94424e-31,2.18104e+08],[&amp;quot;+2^-101&amp;quot;,7.88849e-31,2.26492e+08],[&amp;quot;+2^-100&amp;quot;,1.5777e-30,2.34881e+08],[&amp;quot;+2^-99&amp;quot;,3.1554e-30,2.43269e+08],[&amp;quot;+2^-98&amp;quot;,6.31079e-30,2.51658e+08],[&amp;quot;+2^-97&amp;quot;,1.26216e-29,2.60047e+08],[&amp;quot;+2^-96&amp;quot;,2.52432e-29,2.68435e+08],[&amp;quot;+2^-95&amp;quot;,5.04863e-29,2.76824e+08],[&amp;quot;+2^-94&amp;quot;,1.00973e-28,2.85212e+08],[&amp;quot;+2^-93&amp;quot;,2.01945e-28,2.93601e+08],[&amp;quot;+2^-92&amp;quot;,4.03891e-28,3.0199e+08],[&amp;quot;+2^-91&amp;quot;,8.07781e-28,3.10378e+08],[&amp;quot;+2^-90&amp;quot;,1.61556e-27,3.18767e+08],[&amp;quot;+2^-89&amp;quot;,3.23112e-27,3.27155e+08],[&amp;quot;+2^-88&amp;quot;,6.46225e-27,3.35544e+08],[&amp;quot;+2^-87&amp;quot;,1.29245e-26,3.43933e+08],[&amp;quot;+2^-86&amp;quot;,2.5849e-26,3.52321e+08],[&amp;quot;+2^-85&amp;quot;,5.1698e-26,3.6071e+08],[&amp;quot;+2^-84&amp;quot;,1.03396e-25,3.69098e+08],[&amp;quot;+2^-83&amp;quot;,2.06792e-25,3.77487e+08],[&amp;quot;+2^-82&amp;quot;,4.13584e-25,3.85876e+08],[&amp;quot;+2^-81&amp;quot;,8.27168e-25,3.94264e+08],[&amp;quot;+2^-80&amp;quot;,1.65434e-24,4.02653e+08],[&amp;quot;+2^-79&amp;quot;,3.30867e-24,4.11042e+08],[&amp;quot;+2^-78&amp;quot;,6.61734e-24,4.1943e+08],[&amp;quot;+2^-77&amp;quot;,1.32347e-23,4.27819e+08],[&amp;quot;+2^-76&amp;quot;,2.64694e-23,4.36207e+08],[&amp;quot;+2^-75&amp;quot;,5.29388e-23,4.44596e+08],[&amp;quot;+2^-74&amp;quot;,1.05878e-22,4.52985e+08],[&amp;quot;+2^-73&amp;quot;,2.11755e-22,4.61373e+08],[&amp;quot;+2^-72&amp;quot;,4.2351e-22,4.69762e+08],[&amp;quot;+2^-71&amp;quot;,8.4702e-22,4.7815e+08],[&amp;quot;+2^-70&amp;quot;,1.69404e-21,4.86539e+08],[&amp;quot;+2^-69&amp;quot;,3.38808e-21,4.94928e+08],[&amp;quot;+2^-68&amp;quot;,6.77616e-21,5.03316e+08],[&amp;quot;+2^-67&amp;quot;,1.35523e-20,5.11705e+08],[&amp;quot;+2^-66&amp;quot;,2.71046e-20,5.20093e+08],[&amp;quot;+2^-65&amp;quot;,5.42093e-20,5.28482e+08],[&amp;quot;+2^-64&amp;quot;,1.08419e-19,5.36871e+08],[&amp;quot;+2^-63&amp;quot;,2.16837e-19,5.45259e+08],[&amp;quot;+2^-62&amp;quot;,4.33674e-19,5.53648e+08],[&amp;quot;+2^-61&amp;quot;,8.67349e-19,5.62036e+08],[&amp;quot;+2^-60&amp;quot;,1.7347e-18,5.70425e+08],[&amp;quot;+2^-59&amp;quot;,3.46939e-18,5.78814e+08],[&amp;quot;+2^-58&amp;quot;,6.93879e-18,5.87202e+08],[&amp;quot;+2^-57&amp;quot;,1.38776e-17,5.95591e+08],[&amp;quot;+2^-56&amp;quot;,2.77552e-17,6.0398e+08],[&amp;quot;+2^-55&amp;quot;,5.55103e-17,6.12368e+08],[&amp;quot;+2^-54&amp;quot;,1.11021e-16,6.20757e+08],[&amp;quot;+2^-53&amp;quot;,2.22041e-16,6.29145e+08],[&amp;quot;+2^-52&amp;quot;,4.44082e-16,6.37534e+08],[&amp;quot;+2^-51&amp;quot;,8.88165e-16,6.45923e+08],[&amp;quot;+2^-50&amp;quot;,1.77633e-15,6.54311e+08],[&amp;quot;+2^-49&amp;quot;,3.55266e-15,6.627e+08],[&amp;quot;+2^-48&amp;quot;,7.10532e-15,6.71088e+08],[&amp;quot;+2^-47&amp;quot;,1.42106e-14,6.79477e+08],[&amp;quot;+2^-46&amp;quot;,2.84213e-14,6.87866e+08],[&amp;quot;+2^-45&amp;quot;,5.68426e-14,6.96254e+08],[&amp;quot;+2^-44&amp;quot;,1.13685e-13,7.04643e+08],[&amp;quot;+2^-43&amp;quot;,2.2737e-13,7.13031e+08],[&amp;quot;+2^-42&amp;quot;,4.5474e-13,7.2142e+08],[&amp;quot;+2^-41&amp;quot;,9.09481e-13,7.29809e+08],[&amp;quot;+2^-40&amp;quot;,1.81896e-12,7.38197e+08],[&amp;quot;+2^-39&amp;quot;,3.63792e-12,7.46586e+08],[&amp;quot;+2^-38&amp;quot;,7.27585e-12,7.54974e+08],[&amp;quot;+2^-37&amp;quot;,1.45517e-11,7.63363e+08],[&amp;quot;+2^-36&amp;quot;,2.91034e-11,7.71752e+08],[&amp;quot;+2^-35&amp;quot;,5.82068e-11,7.8014e+08],[&amp;quot;+2^-34&amp;quot;,1.16414e-10,7.88529e+08],[&amp;quot;+2^-33&amp;quot;,2.32827e-10,7.96918e+08],[&amp;quot;+2^-32&amp;quot;,4.65654e-10,8.05306e+08],[&amp;quot;+2^-31&amp;quot;,9.31308e-10,8.13695e+08],[&amp;quot;+2^-30&amp;quot;,1.86262e-09,8.22083e+08],[&amp;quot;+2^-29&amp;quot;,3.72523e-09,8.30472e+08],[&amp;quot;+2^-28&amp;quot;,7.45047e-09,8.38861e+08],[&amp;quot;+2^-27&amp;quot;,1.49009e-08,8.47249e+08],[&amp;quot;+2^-26&amp;quot;,2.98019e-08,8.55638e+08],[&amp;quot;+2^-25&amp;quot;,2.98023e-08,8.38861e+06],[&amp;quot;+2^-24&amp;quot;,2.98023e-08,4.1943e+06],[&amp;quot;+2^-23&amp;quot;,2.98023e-08,2.09715e+06],[&amp;quot;+2^-22&amp;quot;,2.98023e-08,1.04857e+06],[&amp;quot;+2^-21&amp;quot;,2.98021e-08,524284],[&amp;quot;+2^-20&amp;quot;,2.98014e-08,262136],[&amp;quot;+2^-19&amp;quot;,2.97987e-08,131056],[&amp;quot;+2^-18&amp;quot;,2.97878e-08,65504],[&amp;quot;+2^-17&amp;quot;,2.98014e-08,32767],[&amp;quot;+2^-16&amp;quot;,2.98005e-08,16384],[&amp;quot;+2^-15&amp;quot;,2.98023e-08,8192],[&amp;quot;+2^-14&amp;quot;,2.98025e-08,4351.99],[&amp;quot;+2^-13&amp;quot;,2.98044e-08,2815.96],[&amp;quot;+2^-12&amp;quot;,2.98091e-08,1024.23],[&amp;quot;+2^-11&amp;quot;,2.9812e-08,512.167],[&amp;quot;+2^-10&amp;quot;,2.98154e-08,256.113],[&amp;quot;+2^-9&amp;quot;,2.98591e-08,128.244],[&amp;quot;+2^-8&amp;quot;,3.00204e-08,85.3328],[&amp;quot;+2^-7&amp;quot;,3.01947e-08,43.675],[&amp;quot;+2^-6&amp;quot;,3.05713e-08,30.45],[&amp;quot;+2^-5&amp;quot;,3.14319e-08,8.43743],[&amp;quot;+2^-4&amp;quot;,3.31291e-08,7.77058],[&amp;quot;+2^-3&amp;quot;,3.58597e-08,3.99659],[&amp;quot;+2^-2&amp;quot;,4.14639e-08,2.29246],[&amp;quot;+2^-1&amp;quot;,8.02409e-08,1.34622],[&amp;quot;+2^0&amp;quot;,8.66201e-08,1.45324],[&amp;quot;+2^1&amp;quot;,8.8654e-08,1.48737],[&amp;quot;+2^2&amp;quot;,6.7976e-08,1.14045],[&amp;quot;+2^3&amp;quot;,1.33955e-07,2.2474],[&amp;quot;+2^4&amp;quot;,2.53131e-14,4.24683e-07],[&amp;quot;+2^5&amp;quot;,0,null],[&amp;quot;+2^6&amp;quot;,0,null],[&amp;quot;+2^7&amp;quot;,0,null],[&amp;quot;+2^8&amp;quot;,0,null],[&amp;quot;+2^9&amp;quot;,0,null],[&amp;quot;+2^10&amp;quot;,0,null],[&amp;quot;+2^11&amp;quot;,0,null],[&amp;quot;+2^12&amp;quot;,0,null],[&amp;quot;+2^13&amp;quot;,0,null],[&amp;quot;+2^14&amp;quot;,0,null],[&amp;quot;+2^15&amp;quot;,0,null],[&amp;quot;+2^16&amp;quot;,0,null],[&amp;quot;+2^17&amp;quot;,0,null],[&amp;quot;+2^18&amp;quot;,0,null],[&amp;quot;+2^19&amp;quot;,0,null],[&amp;quot;+2^20&amp;quot;,0,null],[&amp;quot;+2^21&amp;quot;,0,null],[&amp;quot;+2^22&amp;quot;,0,null],[&amp;quot;+2^23&amp;quot;,0,null],[&amp;quot;+2^24&amp;quot;,0,null],[&amp;quot;+2^25&amp;quot;,0,null],[&amp;quot;+2^26&amp;quot;,0,null],[&amp;quot;+2^27&amp;quot;,0,null],[&amp;quot;+2^28&amp;quot;,0,null],[&amp;quot;+2^29&amp;quot;,0,null],[&amp;quot;+2^30&amp;quot;,0,null],[&amp;quot;+2^31&amp;quot;,0,null],[&amp;quot;+2^32&amp;quot;,0,null],[&amp;quot;+2^33&amp;quot;,0,null],[&amp;quot;+2^34&amp;quot;,0,null],[&amp;quot;+2^35&amp;quot;,0,null],[&amp;quot;+2^36&amp;quot;,0,null],[&amp;quot;+2^37&amp;quot;,0,null],[&amp;quot;+2^38&amp;quot;,0,null],[&amp;quot;+2^39&amp;quot;,0,null],[&amp;quot;+2^40&amp;quot;,0,null],[&amp;quot;+2^41&amp;quot;,0,null],[&amp;quot;+2^42&amp;quot;,0,null],[&amp;quot;+2^43&amp;quot;,0,null],[&amp;quot;+2^44&amp;quot;,0,null],[&amp;quot;+2^45&amp;quot;,0,null],[&amp;quot;+2^46&amp;quot;,0,null],[&amp;quot;+2^47&amp;quot;,0,null],[&amp;quot;+2^48&amp;quot;,0,null],[&amp;quot;+2^49&amp;quot;,0,null],[&amp;quot;+2^50&amp;quot;,0,null],[&amp;quot;+2^51&amp;quot;,0,null],[&amp;quot;+2^52&amp;quot;,0,null],[&amp;quot;+2^53&amp;quot;,0,null],[&amp;quot;+2^54&amp;quot;,0,null],[&amp;quot;+2^55&amp;quot;,0,null],[&amp;quot;+2^56&amp;quot;,0,null],[&amp;quot;+2^57&amp;quot;,0,null],[&amp;quot;+2^58&amp;quot;,0,null],[&amp;quot;+2^59&amp;quot;,0,null],[&amp;quot;+2^60&amp;quot;,0,null],[&amp;quot;+2^61&amp;quot;,0,null],[&amp;quot;+2^62&amp;quot;,0,null],[&amp;quot;+2^63&amp;quot;,0,null],[&amp;quot;+2^64&amp;quot;,0,null],[&amp;quot;+2^65&amp;quot;,0,null],[&amp;quot;+2^66&amp;quot;,0,null],[&amp;quot;+2^67&amp;quot;,0,null],[&amp;quot;+2^68&amp;quot;,0,null],[&amp;quot;+2^69&amp;quot;,0,null],[&amp;quot;+2^70&amp;quot;,0,null],[&amp;quot;+2^71&amp;quot;,0,null],[&amp;quot;+2^72&amp;quot;,0,null],[&amp;quot;+2^73&amp;quot;,0,null],[&amp;quot;+2^74&amp;quot;,0,null],[&amp;quot;+2^75&amp;quot;,0,null],[&amp;quot;+2^76&amp;quot;,0,null],[&amp;quot;+2^77&amp;quot;,0,null],[&amp;quot;+2^78&amp;quot;,0,null],[&amp;quot;+2^79&amp;quot;,0,null],[&amp;quot;+2^80&amp;quot;,0,null],[&amp;quot;+2^81&amp;quot;,0,null],[&amp;quot;+2^82&amp;quot;,0,null],[&amp;quot;+2^83&amp;quot;,0,null],[&amp;quot;+2^84&amp;quot;,0,null],[&amp;quot;+2^85&amp;quot;,0,null],[&amp;quot;+2^86&amp;quot;,0,null],[&amp;quot;+2^87&amp;quot;,0,null],[&amp;quot;+2^88&amp;quot;,0,null],[&amp;quot;+2^89&amp;quot;,0,null],[&amp;quot;+2^90&amp;quot;,0,null],[&amp;quot;+2^91&amp;quot;,0,null],[&amp;quot;+2^92&amp;quot;,0,null],[&amp;quot;+2^93&amp;quot;,0,null],[&amp;quot;+2^94&amp;quot;,0,null],[&amp;quot;+2^95&amp;quot;,0,null],[&amp;quot;+2^96&amp;quot;,0,null],[&amp;quot;+2^97&amp;quot;,0,null],[&amp;quot;+2^98&amp;quot;,0,null],[&amp;quot;+2^99&amp;quot;,0,null],[&amp;quot;+2^100&amp;quot;,0,null],[&amp;quot;+2^101&amp;quot;,0,null],[&amp;quot;+2^102&amp;quot;,0,null],[&amp;quot;+2^103&amp;quot;,0,null],[&amp;quot;+2^104&amp;quot;,0,null],[&amp;quot;+2^105&amp;quot;,0,null],[&amp;quot;+2^106&amp;quot;,0,null],[&amp;quot;+2^107&amp;quot;,0,null],[&amp;quot;+2^108&amp;quot;,0,null],[&amp;quot;+2^109&amp;quot;,0,null],[&amp;quot;+2^110&amp;quot;,0,null],[&amp;quot;+2^111&amp;quot;,0,null],[&amp;quot;+2^112&amp;quot;,0,null],[&amp;quot;+2^113&amp;quot;,0,null],[&amp;quot;+2^114&amp;quot;,0,null],[&amp;quot;+2^115&amp;quot;,0,null],[&amp;quot;+2^116&amp;quot;,0,null],[&amp;quot;+2^117&amp;quot;,0,null],[&amp;quot;+2^118&amp;quot;,0,null],[&amp;quot;+2^119&amp;quot;,0,null],[&amp;quot;+2^120&amp;quot;,0,null],[&amp;quot;+2^121&amp;quot;,0,null],[&amp;quot;+2^122&amp;quot;,0,null],[&amp;quot;+2^123&amp;quot;,0,null],[&amp;quot;+2^124&amp;quot;,0,null],[&amp;quot;+2^125&amp;quot;,0,null],[&amp;quot;+2^126&amp;quot;,0,null],[&amp;quot;+2^127&amp;quot;,0,null] ]&lt;br /&gt;
 }}&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Key observation:&#039;&#039;&#039;  &lt;br /&gt;
For &amp;lt;math&amp;gt;|x| \ll 1&amp;lt;/math&amp;gt;, the ULP error grows rapidly and reaches extremely large values due to loss of significance. &lt;br /&gt;
For sufficiently large &amp;lt;math&amp;gt;x&amp;lt;/math&amp;gt;, both numerator and denominator overflow, leading to NaN results.&lt;br /&gt;
&lt;br /&gt;
=== expm1 ===&lt;br /&gt;
&lt;br /&gt;
This graph visualizes the ULP error of the &amp;lt;code&amp;gt;expm1&amp;lt;/code&amp;gt;-based formulation.&lt;br /&gt;
Using &amp;lt;code&amp;gt;expm1&amp;lt;/code&amp;gt; significantly improves numerical accuracy for small input values by avoiding catastrophic cancellation in the expression &amp;lt;math&amp;gt;e^{2x} - 1&amp;lt;/math&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
{{#widget:FloatErrorGraph|id=tanhf_expm1&lt;br /&gt;
  |data=[ [&amp;quot;-2^127&amp;quot;,0,1.17549e-38],[&amp;quot;-2^126&amp;quot;,0,1.17549e-38],[&amp;quot;-2^125&amp;quot;,0,1.17549e-38],[&amp;quot;-2^124&amp;quot;,0,1.17549e-38],[&amp;quot;-2^123&amp;quot;,0,1.17549e-38],[&amp;quot;-2^122&amp;quot;,0,1.17549e-38],[&amp;quot;-2^121&amp;quot;,0,1.17549e-38],[&amp;quot;-2^120&amp;quot;,0,1.17549e-38],[&amp;quot;-2^119&amp;quot;,0,1.17549e-38],[&amp;quot;-2^118&amp;quot;,0,1.17549e-38],[&amp;quot;-2^117&amp;quot;,0,1.17549e-38],[&amp;quot;-2^116&amp;quot;,0,1.17549e-38],[&amp;quot;-2^115&amp;quot;,0,1.17549e-38],[&amp;quot;-2^114&amp;quot;,0,1.17549e-38],[&amp;quot;-2^113&amp;quot;,0,1.17549e-38],[&amp;quot;-2^112&amp;quot;,0,1.17549e-38],[&amp;quot;-2^111&amp;quot;,0,1.17549e-38],[&amp;quot;-2^110&amp;quot;,0,1.17549e-38],[&amp;quot;-2^109&amp;quot;,0,1.17549e-38],[&amp;quot;-2^108&amp;quot;,0,1.17549e-38],[&amp;quot;-2^107&amp;quot;,0,1.17549e-38],[&amp;quot;-2^106&amp;quot;,0,1.17549e-38],[&amp;quot;-2^105&amp;quot;,0,1.17549e-38],[&amp;quot;-2^104&amp;quot;,0,1.17549e-38],[&amp;quot;-2^103&amp;quot;,0,1.17549e-38],[&amp;quot;-2^102&amp;quot;,0,1.17549e-38],[&amp;quot;-2^101&amp;quot;,0,1.17549e-38],[&amp;quot;-2^100&amp;quot;,0,1.17549e-38],[&amp;quot;-2^99&amp;quot;,0,1.17549e-38],[&amp;quot;-2^98&amp;quot;,0,1.17549e-38],[&amp;quot;-2^97&amp;quot;,0,1.17549e-38],[&amp;quot;-2^96&amp;quot;,0,1.17549e-38],[&amp;quot;-2^95&amp;quot;,0,1.17549e-38],[&amp;quot;-2^94&amp;quot;,0,1.17549e-38],[&amp;quot;-2^93&amp;quot;,0,1.17549e-38],[&amp;quot;-2^92&amp;quot;,0,1.17549e-38],[&amp;quot;-2^91&amp;quot;,0,1.17549e-38],[&amp;quot;-2^90&amp;quot;,0,1.17549e-38],[&amp;quot;-2^89&amp;quot;,0,1.17549e-38],[&amp;quot;-2^88&amp;quot;,0,1.17549e-38],[&amp;quot;-2^87&amp;quot;,0,1.17549e-38],[&amp;quot;-2^86&amp;quot;,0,1.17549e-38],[&amp;quot;-2^85&amp;quot;,0,1.17549e-38],[&amp;quot;-2^84&amp;quot;,0,1.17549e-38],[&amp;quot;-2^83&amp;quot;,0,1.17549e-38],[&amp;quot;-2^82&amp;quot;,0,1.17549e-38],[&amp;quot;-2^81&amp;quot;,0,1.17549e-38],[&amp;quot;-2^80&amp;quot;,0,1.17549e-38],[&amp;quot;-2^79&amp;quot;,0,1.17549e-38],[&amp;quot;-2^78&amp;quot;,0,1.17549e-38],[&amp;quot;-2^77&amp;quot;,0,1.17549e-38],[&amp;quot;-2^76&amp;quot;,0,1.17549e-38],[&amp;quot;-2^75&amp;quot;,0,1.17549e-38],[&amp;quot;-2^74&amp;quot;,0,1.17549e-38],[&amp;quot;-2^73&amp;quot;,0,1.17549e-38],[&amp;quot;-2^72&amp;quot;,0,1.17549e-38],[&amp;quot;-2^71&amp;quot;,0,1.17549e-38],[&amp;quot;-2^70&amp;quot;,0,1.17549e-38],[&amp;quot;-2^69&amp;quot;,0,1.17549e-38],[&amp;quot;-2^68&amp;quot;,0,1.17549e-38],[&amp;quot;-2^67&amp;quot;,0,1.17549e-38],[&amp;quot;-2^66&amp;quot;,0,1.17549e-38],[&amp;quot;-2^65&amp;quot;,0,1.17549e-38],[&amp;quot;-2^64&amp;quot;,0,1.17549e-38],[&amp;quot;-2^63&amp;quot;,0,1.17549e-38],[&amp;quot;-2^62&amp;quot;,0,1.17549e-38],[&amp;quot;-2^61&amp;quot;,0,1.17549e-38],[&amp;quot;-2^60&amp;quot;,0,1.17549e-38],[&amp;quot;-2^59&amp;quot;,0,1.17549e-38],[&amp;quot;-2^58&amp;quot;,0,1.17549e-38],[&amp;quot;-2^57&amp;quot;,0,1.17549e-38],[&amp;quot;-2^56&amp;quot;,0,1.17549e-38],[&amp;quot;-2^55&amp;quot;,0,1.17549e-38],[&amp;quot;-2^54&amp;quot;,0,1.17549e-38],[&amp;quot;-2^53&amp;quot;,0,1.17549e-38],[&amp;quot;-2^52&amp;quot;,0,1.17549e-38],[&amp;quot;-2^51&amp;quot;,0,1.17549e-38],[&amp;quot;-2^50&amp;quot;,0,1.17549e-38],[&amp;quot;-2^49&amp;quot;,0,1.17549e-38],[&amp;quot;-2^48&amp;quot;,0,1.17549e-38],[&amp;quot;-2^47&amp;quot;,0,1.17549e-38],[&amp;quot;-2^46&amp;quot;,0,1.17549e-38],[&amp;quot;-2^45&amp;quot;,0,1.17549e-38],[&amp;quot;-2^44&amp;quot;,0,1.17549e-38],[&amp;quot;-2^43&amp;quot;,0,1.17549e-38],[&amp;quot;-2^42&amp;quot;,0,1.17549e-38],[&amp;quot;-2^41&amp;quot;,0,1.17549e-38],[&amp;quot;-2^40&amp;quot;,0,1.17549e-38],[&amp;quot;-2^39&amp;quot;,0,1.17549e-38],[&amp;quot;-2^38&amp;quot;,0,1.17549e-38],[&amp;quot;-2^37&amp;quot;,0,1.17549e-38],[&amp;quot;-2^36&amp;quot;,0,1.17549e-38],[&amp;quot;-2^35&amp;quot;,0,1.17549e-38],[&amp;quot;-2^34&amp;quot;,0,1.17549e-38],[&amp;quot;-2^33&amp;quot;,0,1.17549e-38],[&amp;quot;-2^32&amp;quot;,0,1.17549e-38],[&amp;quot;-2^31&amp;quot;,0,1.17549e-38],[&amp;quot;-2^30&amp;quot;,0,1.17549e-38],[&amp;quot;-2^29&amp;quot;,0,1.17549e-38],[&amp;quot;-2^28&amp;quot;,0,1.17549e-38],[&amp;quot;-2^27&amp;quot;,0,1.17549e-38],[&amp;quot;-2^26&amp;quot;,0,1.17549e-38],[&amp;quot;-2^25&amp;quot;,0,1.17549e-38],[&amp;quot;-2^24&amp;quot;,0,1.17549e-38],[&amp;quot;-2^23&amp;quot;,0,1.17549e-38],[&amp;quot;-2^22&amp;quot;,0,1.17549e-38],[&amp;quot;-2^21&amp;quot;,0,1.17549e-38],[&amp;quot;-2^20&amp;quot;,0,1.17549e-38],[&amp;quot;-2^19&amp;quot;,0,1.17549e-38],[&amp;quot;-2^18&amp;quot;,0,1.17549e-38],[&amp;quot;-2^17&amp;quot;,0,1.17549e-38],[&amp;quot;-2^16&amp;quot;,0,1.17549e-38],[&amp;quot;-2^15&amp;quot;,0,1.17549e-38],[&amp;quot;-2^14&amp;quot;,0,1.17549e-38],[&amp;quot;-2^13&amp;quot;,0,1.17549e-38],[&amp;quot;-2^12&amp;quot;,0,1.17549e-38],[&amp;quot;-2^11&amp;quot;,0,1.17549e-38],[&amp;quot;-2^10&amp;quot;,0,1.17549e-38],[&amp;quot;-2^9&amp;quot;,0,1.17549e-38],[&amp;quot;-2^8&amp;quot;,0,1.17549e-38],[&amp;quot;-2^7&amp;quot;,0,1.17549e-38],[&amp;quot;-2^6&amp;quot;,0,1.17549e-38],[&amp;quot;-2^5&amp;quot;,0,1.17549e-38],[&amp;quot;-2^4&amp;quot;,2.53131e-14,4.24683e-07],[&amp;quot;-2^3&amp;quot;,1.19138e-07,1.9988],[&amp;quot;-2^2&amp;quot;,1.46791e-07,2.46274],[&amp;quot;-2^1&amp;quot;,1.47723e-07,2.47839],[&amp;quot;-2^0&amp;quot;,1.40758e-07,2.36153],[&amp;quot;-2^-1&amp;quot;,1.13422e-07,2.23445],[&amp;quot;-2^-2&amp;quot;,6.38012e-08,2.14081],[&amp;quot;-2^-3&amp;quot;,2.69538e-08,1.80884],[&amp;quot;-2^-4&amp;quot;,1.21308e-08,1.62817],[&amp;quot;-2^-5&amp;quot;,5.59535e-09,1.50199],[&amp;quot;-2^-6&amp;quot;,2.69177e-09,1.44513],[&amp;quot;-2^-7&amp;quot;,1.31289e-09,1.4097],[&amp;quot;-2^-8&amp;quot;,6.49688e-10,1.3952],[&amp;quot;-2^-9&amp;quot;,3.32043e-10,1.42611],[&amp;quot;-2^-10&amp;quot;,1.66275e-10,1.4283],[&amp;quot;-2^-11&amp;quot;,8.08595e-11,1.38916],[&amp;quot;-2^-12&amp;quot;,3.8939e-11,1.33794],[&amp;quot;-2^-13&amp;quot;,1.9401e-11,1.33323],[&amp;quot;-2^-14&amp;quot;,7.88212e-12,1.08331],[&amp;quot;-2^-15&amp;quot;,3.71372e-12,1.02082],[&amp;quot;-2^-16&amp;quot;,1.82845e-12,1.0052],[&amp;quot;-2^-17&amp;quot;,9.10676e-13,1.0013],[&amp;quot;-2^-18&amp;quot;,4.54895e-13,1.00032],[&amp;quot;-2^-19&amp;quot;,2.27392e-13,1.00008],[&amp;quot;-2^-20&amp;quot;,1.13689e-13,1.00002],[&amp;quot;-2^-21&amp;quot;,5.68437e-14,1],[&amp;quot;-2^-22&amp;quot;,2.84217e-14,1],[&amp;quot;-2^-23&amp;quot;,1.42109e-14,1],[&amp;quot;-2^-24&amp;quot;,7.10543e-15,1],[&amp;quot;-2^-25&amp;quot;,3.55271e-15,1],[&amp;quot;-2^-26&amp;quot;,9.92617e-24,5.58794e-09],[&amp;quot;-2^-27&amp;quot;,1.65436e-24,1.86265e-09],[&amp;quot;-2^-28&amp;quot;,8.27181e-25,1.86265e-09],[&amp;quot;-2^-29&amp;quot;,4.1359e-25,1.86265e-09],[&amp;quot;-2^-30&amp;quot;,2.06795e-25,1.86265e-09],[&amp;quot;-2^-31&amp;quot;,1.03398e-25,1.86265e-09],[&amp;quot;-2^-32&amp;quot;,5.16988e-26,1.86265e-09],[&amp;quot;-2^-33&amp;quot;,2.58494e-26,1.86265e-09],[&amp;quot;-2^-34&amp;quot;,1.29247e-26,1.86265e-09],[&amp;quot;-2^-35&amp;quot;,6.46235e-27,1.86265e-09],[&amp;quot;-2^-36&amp;quot;,3.23117e-27,1.86265e-09],[&amp;quot;-2^-37&amp;quot;,1.61559e-27,1.86265e-09],[&amp;quot;-2^-38&amp;quot;,8.07794e-28,1.86265e-09],[&amp;quot;-2^-39&amp;quot;,4.03897e-28,1.86265e-09],[&amp;quot;-2^-40&amp;quot;,2.01948e-28,1.86265e-09],[&amp;quot;-2^-41&amp;quot;,1.00974e-28,1.86265e-09],[&amp;quot;-2^-42&amp;quot;,5.04871e-29,1.86265e-09],[&amp;quot;-2^-43&amp;quot;,2.52435e-29,1.86265e-09],[&amp;quot;-2^-44&amp;quot;,1.26218e-29,1.86265e-09],[&amp;quot;-2^-45&amp;quot;,6.31089e-30,1.86265e-09],[&amp;quot;-2^-46&amp;quot;,3.15544e-30,1.86265e-09],[&amp;quot;-2^-47&amp;quot;,1.57772e-30,1.86265e-09],[&amp;quot;-2^-48&amp;quot;,7.88861e-31,1.86265e-09],[&amp;quot;-2^-49&amp;quot;,3.9443e-31,1.86265e-09],[&amp;quot;-2^-50&amp;quot;,1.97215e-31,1.86265e-09],[&amp;quot;-2^-51&amp;quot;,9.86076e-32,1.86265e-09],[&amp;quot;-2^-52&amp;quot;,4.93038e-32,1.86265e-09],[&amp;quot;-2^-53&amp;quot;,2.46519e-32,1.86265e-09],[&amp;quot;-2^-54&amp;quot;,1.2326e-32,1.86265e-09],[&amp;quot;-2^-55&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-56&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-57&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-58&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-59&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-60&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-61&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-62&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-63&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-64&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-65&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-66&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-67&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-68&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-69&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-70&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-71&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-72&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-73&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-74&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-75&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-76&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-77&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-78&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-79&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-80&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-81&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-82&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-83&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-84&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-85&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-86&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-87&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-88&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-89&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-90&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-91&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-92&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-93&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-94&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-95&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-96&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-97&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-98&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-99&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-100&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-101&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-102&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-103&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-104&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-105&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-106&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-107&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-108&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-109&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-110&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-111&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-112&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-113&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-114&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-115&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-116&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-117&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-118&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-119&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-120&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-121&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-122&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-123&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-124&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-125&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-126&amp;quot;,0,1.17549e-38],[&amp;quot;-SN&amp;quot;,0,1.17549e-38],[&amp;quot;0&amp;quot;,0,1.17549e-38],[&amp;quot;+SN&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-126&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-125&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-124&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-123&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-122&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-121&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-120&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-119&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-118&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-117&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-116&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-115&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-114&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-113&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-112&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-111&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-110&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-109&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-108&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-107&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-106&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-105&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-104&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-103&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-102&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-101&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-100&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-99&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-98&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-97&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-96&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-95&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-94&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-93&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-92&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-91&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-90&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-89&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-88&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-87&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-86&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-85&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-84&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-83&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-82&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-81&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-80&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-79&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-78&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-77&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-76&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-75&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-74&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-73&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-72&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-71&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-70&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-69&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-68&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-67&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-66&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-65&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-64&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-63&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-62&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-61&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-60&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-59&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-58&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-57&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-56&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-55&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-54&amp;quot;,1.2326e-32,1.86265e-09],[&amp;quot;+2^-53&amp;quot;,2.46519e-32,1.86265e-09],[&amp;quot;+2^-52&amp;quot;,4.93038e-32,1.86265e-09],[&amp;quot;+2^-51&amp;quot;,9.86076e-32,1.86265e-09],[&amp;quot;+2^-50&amp;quot;,1.97215e-31,1.86265e-09],[&amp;quot;+2^-49&amp;quot;,3.9443e-31,1.86265e-09],[&amp;quot;+2^-48&amp;quot;,7.88861e-31,1.86265e-09],[&amp;quot;+2^-47&amp;quot;,1.57772e-30,1.86265e-09],[&amp;quot;+2^-46&amp;quot;,3.15544e-30,1.86265e-09],[&amp;quot;+2^-45&amp;quot;,6.31089e-30,1.86265e-09],[&amp;quot;+2^-44&amp;quot;,1.26218e-29,1.86265e-09],[&amp;quot;+2^-43&amp;quot;,2.52435e-29,1.86265e-09],[&amp;quot;+2^-42&amp;quot;,5.04871e-29,1.86265e-09],[&amp;quot;+2^-41&amp;quot;,1.00974e-28,1.86265e-09],[&amp;quot;+2^-40&amp;quot;,2.01948e-28,1.86265e-09],[&amp;quot;+2^-39&amp;quot;,4.03897e-28,1.86265e-09],[&amp;quot;+2^-38&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-37&amp;quot;,1.61559e-27,1.86265e-09],[&amp;quot;+2^-36&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-35&amp;quot;,6.46235e-27,1.86265e-09],[&amp;quot;+2^-34&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-33&amp;quot;,2.58494e-26,1.86265e-09],[&amp;quot;+2^-32&amp;quot;,5.16988e-26,1.86265e-09],[&amp;quot;+2^-31&amp;quot;,1.03398e-25,1.86265e-09],[&amp;quot;+2^-30&amp;quot;,2.06795e-25,1.86265e-09],[&amp;quot;+2^-29&amp;quot;,4.1359e-25,1.86265e-09],[&amp;quot;+2^-28&amp;quot;,8.27181e-25,1.86265e-09],[&amp;quot;+2^-27&amp;quot;,1.65436e-24,1.86265e-09],[&amp;quot;+2^-26&amp;quot;,9.92617e-24,5.58794e-09],[&amp;quot;+2^-25&amp;quot;,3.55271e-15,1],[&amp;quot;+2^-24&amp;quot;,7.10543e-15,1],[&amp;quot;+2^-23&amp;quot;,1.42109e-14,1],[&amp;quot;+2^-22&amp;quot;,2.84217e-14,1],[&amp;quot;+2^-21&amp;quot;,5.68437e-14,1],[&amp;quot;+2^-20&amp;quot;,1.13689e-13,1.00002],[&amp;quot;+2^-19&amp;quot;,2.27391e-13,1.00008],[&amp;quot;+2^-18&amp;quot;,4.54892e-13,1.00032],[&amp;quot;+2^-17&amp;quot;,9.10665e-13,1.00129],[&amp;quot;+2^-16&amp;quot;,1.82841e-12,1.00518],[&amp;quot;+2^-15&amp;quot;,3.71353e-12,1.02077],[&amp;quot;+2^-14&amp;quot;,1.39798e-11,1.92136],[&amp;quot;+2^-13&amp;quot;,2.63239e-11,1.80896],[&amp;quot;+2^-12&amp;quot;,5.37712e-11,1.84757],[&amp;quot;+2^-11&amp;quot;,1.09932e-10,1.88861],[&amp;quot;+2^-10&amp;quot;,2.24787e-10,1.93091],[&amp;quot;+2^-9&amp;quot;,4.3452e-10,1.86625],[&amp;quot;+2^-8&amp;quot;,1.01783e-09,2.18576],[&amp;quot;+2^-7&amp;quot;,2.10562e-09,2.26089],[&amp;quot;+2^-6&amp;quot;,4.03954e-09,2.16871],[&amp;quot;+2^-5&amp;quot;,8.15746e-09,2.18975],[&amp;quot;+2^-4&amp;quot;,1.5545e-08,2.08641],[&amp;quot;+2^-3&amp;quot;,2.62153e-08,2.05864],[&amp;quot;+2^-2&amp;quot;,4.09295e-08,1.73804],[&amp;quot;+2^-1&amp;quot;,8.02409e-08,1.34622],[&amp;quot;+2^0&amp;quot;,8.66201e-08,1.45324],[&amp;quot;+2^1&amp;quot;,8.8654e-08,1.48737],[&amp;quot;+2^2&amp;quot;,6.7976e-08,1.14045],[&amp;quot;+2^3&amp;quot;,7.44117e-08,1.24842],[&amp;quot;+2^4&amp;quot;,2.53131e-14,4.24683e-07],[&amp;quot;+2^5&amp;quot;,0,null],[&amp;quot;+2^6&amp;quot;,0,null],[&amp;quot;+2^7&amp;quot;,0,null],[&amp;quot;+2^8&amp;quot;,0,null],[&amp;quot;+2^9&amp;quot;,0,null],[&amp;quot;+2^10&amp;quot;,0,null],[&amp;quot;+2^11&amp;quot;,0,null],[&amp;quot;+2^12&amp;quot;,0,null],[&amp;quot;+2^13&amp;quot;,0,null],[&amp;quot;+2^14&amp;quot;,0,null],[&amp;quot;+2^15&amp;quot;,0,null],[&amp;quot;+2^16&amp;quot;,0,null],[&amp;quot;+2^17&amp;quot;,0,null],[&amp;quot;+2^18&amp;quot;,0,null],[&amp;quot;+2^19&amp;quot;,0,null],[&amp;quot;+2^20&amp;quot;,0,null],[&amp;quot;+2^21&amp;quot;,0,null],[&amp;quot;+2^22&amp;quot;,0,null],[&amp;quot;+2^23&amp;quot;,0,null],[&amp;quot;+2^24&amp;quot;,0,null],[&amp;quot;+2^25&amp;quot;,0,null],[&amp;quot;+2^26&amp;quot;,0,null],[&amp;quot;+2^27&amp;quot;,0,null],[&amp;quot;+2^28&amp;quot;,0,null],[&amp;quot;+2^29&amp;quot;,0,null],[&amp;quot;+2^30&amp;quot;,0,null],[&amp;quot;+2^31&amp;quot;,0,null],[&amp;quot;+2^32&amp;quot;,0,null],[&amp;quot;+2^33&amp;quot;,0,null],[&amp;quot;+2^34&amp;quot;,0,null],[&amp;quot;+2^35&amp;quot;,0,null],[&amp;quot;+2^36&amp;quot;,0,null],[&amp;quot;+2^37&amp;quot;,0,null],[&amp;quot;+2^38&amp;quot;,0,null],[&amp;quot;+2^39&amp;quot;,0,null],[&amp;quot;+2^40&amp;quot;,0,null],[&amp;quot;+2^41&amp;quot;,0,null],[&amp;quot;+2^42&amp;quot;,0,null],[&amp;quot;+2^43&amp;quot;,0,null],[&amp;quot;+2^44&amp;quot;,0,null],[&amp;quot;+2^45&amp;quot;,0,null],[&amp;quot;+2^46&amp;quot;,0,null],[&amp;quot;+2^47&amp;quot;,0,null],[&amp;quot;+2^48&amp;quot;,0,null],[&amp;quot;+2^49&amp;quot;,0,null],[&amp;quot;+2^50&amp;quot;,0,null],[&amp;quot;+2^51&amp;quot;,0,null],[&amp;quot;+2^52&amp;quot;,0,null],[&amp;quot;+2^53&amp;quot;,0,null],[&amp;quot;+2^54&amp;quot;,0,null],[&amp;quot;+2^55&amp;quot;,0,null],[&amp;quot;+2^56&amp;quot;,0,null],[&amp;quot;+2^57&amp;quot;,0,null],[&amp;quot;+2^58&amp;quot;,0,null],[&amp;quot;+2^59&amp;quot;,0,null],[&amp;quot;+2^60&amp;quot;,0,null],[&amp;quot;+2^61&amp;quot;,0,null],[&amp;quot;+2^62&amp;quot;,0,null],[&amp;quot;+2^63&amp;quot;,0,null],[&amp;quot;+2^64&amp;quot;,0,null],[&amp;quot;+2^65&amp;quot;,0,null],[&amp;quot;+2^66&amp;quot;,0,null],[&amp;quot;+2^67&amp;quot;,0,null],[&amp;quot;+2^68&amp;quot;,0,null],[&amp;quot;+2^69&amp;quot;,0,null],[&amp;quot;+2^70&amp;quot;,0,null],[&amp;quot;+2^71&amp;quot;,0,null],[&amp;quot;+2^72&amp;quot;,0,null],[&amp;quot;+2^73&amp;quot;,0,null],[&amp;quot;+2^74&amp;quot;,0,null],[&amp;quot;+2^75&amp;quot;,0,null],[&amp;quot;+2^76&amp;quot;,0,null],[&amp;quot;+2^77&amp;quot;,0,null],[&amp;quot;+2^78&amp;quot;,0,null],[&amp;quot;+2^79&amp;quot;,0,null],[&amp;quot;+2^80&amp;quot;,0,null],[&amp;quot;+2^81&amp;quot;,0,null],[&amp;quot;+2^82&amp;quot;,0,null],[&amp;quot;+2^83&amp;quot;,0,null],[&amp;quot;+2^84&amp;quot;,0,null],[&amp;quot;+2^85&amp;quot;,0,null],[&amp;quot;+2^86&amp;quot;,0,null],[&amp;quot;+2^87&amp;quot;,0,null],[&amp;quot;+2^88&amp;quot;,0,null],[&amp;quot;+2^89&amp;quot;,0,null],[&amp;quot;+2^90&amp;quot;,0,null],[&amp;quot;+2^91&amp;quot;,0,null],[&amp;quot;+2^92&amp;quot;,0,null],[&amp;quot;+2^93&amp;quot;,0,null],[&amp;quot;+2^94&amp;quot;,0,null],[&amp;quot;+2^95&amp;quot;,0,null],[&amp;quot;+2^96&amp;quot;,0,null],[&amp;quot;+2^97&amp;quot;,0,null],[&amp;quot;+2^98&amp;quot;,0,null],[&amp;quot;+2^99&amp;quot;,0,null],[&amp;quot;+2^100&amp;quot;,0,null],[&amp;quot;+2^101&amp;quot;,0,null],[&amp;quot;+2^102&amp;quot;,0,null],[&amp;quot;+2^103&amp;quot;,0,null],[&amp;quot;+2^104&amp;quot;,0,null],[&amp;quot;+2^105&amp;quot;,0,null],[&amp;quot;+2^106&amp;quot;,0,null],[&amp;quot;+2^107&amp;quot;,0,null],[&amp;quot;+2^108&amp;quot;,0,null],[&amp;quot;+2^109&amp;quot;,0,null],[&amp;quot;+2^110&amp;quot;,0,null],[&amp;quot;+2^111&amp;quot;,0,null],[&amp;quot;+2^112&amp;quot;,0,null],[&amp;quot;+2^113&amp;quot;,0,null],[&amp;quot;+2^114&amp;quot;,0,null],[&amp;quot;+2^115&amp;quot;,0,null],[&amp;quot;+2^116&amp;quot;,0,null],[&amp;quot;+2^117&amp;quot;,0,null],[&amp;quot;+2^118&amp;quot;,0,null],[&amp;quot;+2^119&amp;quot;,0,null],[&amp;quot;+2^120&amp;quot;,0,null],[&amp;quot;+2^121&amp;quot;,0,null],[&amp;quot;+2^122&amp;quot;,0,null],[&amp;quot;+2^123&amp;quot;,0,null],[&amp;quot;+2^124&amp;quot;,0,null],[&amp;quot;+2^125&amp;quot;,0,null],[&amp;quot;+2^126&amp;quot;,0,null],[&amp;quot;+2^127&amp;quot;,0,null] ]&lt;br /&gt;
 }}&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Key observation:&#039;&#039;&#039;  &lt;br /&gt;
Compared to direct exponential formulations, the &amp;lt;code&amp;gt;expm1&amp;lt;/code&amp;gt; approach maintains low ULP error for small and medium values of &amp;lt;math&amp;gt;x&amp;lt;/math&amp;gt;. &lt;br /&gt;
Overflow is still present for very large inputs, but the overall error behavior is substantially more stable.&lt;br /&gt;
&lt;br /&gt;
== Numerical Issues and Error Metrics ==&lt;br /&gt;
&lt;br /&gt;
To evaluate and compare different software implementations of &amp;lt;code&amp;gt;tanh&amp;lt;/code&amp;gt;, several numerical error metrics are used throughout this article. These metrics make it possible to reason about accuracy, robustness, and failure modes in floating-point arithmetic.&lt;br /&gt;
&lt;br /&gt;
=== ULP Error ===&lt;br /&gt;
ULP (Unit in the Last Place) measures the distance between two adjacent representable floating-point numbers. The ULP error of a computed result is defined as the difference, measured in ULPs, between the computed value and the correctly rounded reference value.&lt;br /&gt;
&lt;br /&gt;
An error of &amp;lt;code&amp;gt;ULP ≤ 2&amp;lt;/code&amp;gt; is commonly considered acceptable for single-precision transcendental functions, as it indicates that the result is very close to the correctly rounded value.&lt;br /&gt;
&lt;br /&gt;
Large ULP errors typically indicate catastrophic cancellation, loss of significant bits, or severe rounding effects.&lt;br /&gt;
&lt;br /&gt;
=== NaN and Overflow ===&lt;br /&gt;
NaN (Not a Number) results occur when undefined floating-point operations are performed, such as &amp;lt;code&amp;gt;inf / inf&amp;lt;/code&amp;gt;. In &amp;lt;code&amp;gt;tanh&amp;lt;/code&amp;gt; implementations, this often happens when both the numerator and denominator of a formulation overflow to infinity.&lt;br /&gt;
&lt;br /&gt;
Tracking NaN generation is critical, as NaNs can silently propagate through numerical pipelines and invalidate downstream results.&lt;br /&gt;
&lt;br /&gt;
=== Error Graphs ===&lt;br /&gt;
The error graphs shown in the following sections visualize ULP error across the full &amp;lt;code&amp;gt;float32&amp;lt;/code&amp;gt; input domain. They make it possible to identify:&lt;br /&gt;
* regions with catastrophic cancellation,&lt;br /&gt;
* input ranges that trigger overflow or NaNs,&lt;br /&gt;
* and numerical stability differences between formulations.&lt;br /&gt;
&lt;br /&gt;
These graphs are a key tool for understanding not only the maximum error, but also how errors are distributed across different input magnitudes.&lt;br /&gt;
&lt;br /&gt;
== Taylor Expansion (Local Approximation) ==&lt;br /&gt;
&lt;br /&gt;
Taylor series approximations of &amp;lt;code&amp;gt;tanh(x)&amp;lt;/code&amp;gt; expand the function around &amp;lt;math&amp;gt;x = 0&amp;lt;/math&amp;gt; and therefore provide a very accurate approximation only for sufficiently small input magnitudes.&lt;br /&gt;
&lt;br /&gt;
For small &amp;lt;math&amp;gt;|x|&amp;lt;/math&amp;gt;, the hyperbolic tangent behaves approximately linear (&amp;lt;math&amp;gt;\tanh(x) \approx x&amp;lt;/math&amp;gt;), and low-order Taylor polynomials can achieve excellent accuracy with minimal computational cost. This makes Taylor-based approximations attractive as a fast path in range-reduced implementations.&lt;br /&gt;
&lt;br /&gt;
However, Taylor expansions have a strictly limited radius of convergence. Outside a narrow neighborhood around zero, the approximation error grows extremely rapidly and quickly becomes unusable. As a result, Taylor polynomials are not suitable as standalone implementations of &amp;lt;code&amp;gt;tanh&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
{{#widget:FloatErrorGraph|id=taylor&lt;br /&gt;
  |data=[ [&amp;quot;-2^127&amp;quot;,0,0],[&amp;quot;-2^126&amp;quot;,0,0],[&amp;quot;-2^125&amp;quot;,0,0],[&amp;quot;-2^124&amp;quot;,0,0],[&amp;quot;-2^123&amp;quot;,0,0],[&amp;quot;-2^122&amp;quot;,0,0],[&amp;quot;-2^121&amp;quot;,0,0],[&amp;quot;-2^120&amp;quot;,0,0],[&amp;quot;-2^119&amp;quot;,0,0],[&amp;quot;-2^118&amp;quot;,0,0],[&amp;quot;-2^117&amp;quot;,0,0],[&amp;quot;-2^116&amp;quot;,0,0],[&amp;quot;-2^115&amp;quot;,0,0],[&amp;quot;-2^114&amp;quot;,0,0],[&amp;quot;-2^113&amp;quot;,0,0],[&amp;quot;-2^112&amp;quot;,0,0],[&amp;quot;-2^111&amp;quot;,0,0],[&amp;quot;-2^110&amp;quot;,0,0],[&amp;quot;-2^109&amp;quot;,0,0],[&amp;quot;-2^108&amp;quot;,0,0],[&amp;quot;-2^107&amp;quot;,0,0],[&amp;quot;-2^106&amp;quot;,0,0],[&amp;quot;-2^105&amp;quot;,0,0],[&amp;quot;-2^104&amp;quot;,0,0],[&amp;quot;-2^103&amp;quot;,0,0],[&amp;quot;-2^102&amp;quot;,0,0],[&amp;quot;-2^101&amp;quot;,0,0],[&amp;quot;-2^100&amp;quot;,0,0],[&amp;quot;-2^99&amp;quot;,0,0],[&amp;quot;-2^98&amp;quot;,0,0],[&amp;quot;-2^97&amp;quot;,0,0],[&amp;quot;-2^96&amp;quot;,0,0],[&amp;quot;-2^95&amp;quot;,0,0],[&amp;quot;-2^94&amp;quot;,0,0],[&amp;quot;-2^93&amp;quot;,0,0],[&amp;quot;-2^92&amp;quot;,0,0],[&amp;quot;-2^91&amp;quot;,0,0],[&amp;quot;-2^90&amp;quot;,0,0],[&amp;quot;-2^89&amp;quot;,0,0],[&amp;quot;-2^88&amp;quot;,0,0],[&amp;quot;-2^87&amp;quot;,0,0],[&amp;quot;-2^86&amp;quot;,0,0],[&amp;quot;-2^85&amp;quot;,0,0],[&amp;quot;-2^84&amp;quot;,0,0],[&amp;quot;-2^83&amp;quot;,0,0],[&amp;quot;-2^82&amp;quot;,0,0],[&amp;quot;-2^81&amp;quot;,0,0],[&amp;quot;-2^80&amp;quot;,0,0],[&amp;quot;-2^79&amp;quot;,0,0],[&amp;quot;-2^78&amp;quot;,0,0],[&amp;quot;-2^77&amp;quot;,0,0],[&amp;quot;-2^76&amp;quot;,0,0],[&amp;quot;-2^75&amp;quot;,0,0],[&amp;quot;-2^74&amp;quot;,0,0],[&amp;quot;-2^73&amp;quot;,0,0],[&amp;quot;-2^72&amp;quot;,0,0],[&amp;quot;-2^71&amp;quot;,0,0],[&amp;quot;-2^70&amp;quot;,0,0],[&amp;quot;-2^69&amp;quot;,0,0],[&amp;quot;-2^68&amp;quot;,0,0],[&amp;quot;-2^67&amp;quot;,0,0],[&amp;quot;-2^66&amp;quot;,0,0],[&amp;quot;-2^65&amp;quot;,0,0],[&amp;quot;-2^64&amp;quot;,0,0],[&amp;quot;-2^63&amp;quot;,1.84467e+19,3.09485e+26],[&amp;quot;-2^62&amp;quot;,9.22336e+18,1.54742e+26],[&amp;quot;-2^61&amp;quot;,4.61168e+18,7.73712e+25],[&amp;quot;-2^60&amp;quot;,2.30584e+18,3.86856e+25],[&amp;quot;-2^59&amp;quot;,1.15292e+18,1.93428e+25],[&amp;quot;-2^58&amp;quot;,5.7646e+17,9.6714e+24],[&amp;quot;-2^57&amp;quot;,2.8823e+17,4.8357e+24],[&amp;quot;-2^56&amp;quot;,1.44115e+17,2.41785e+24],[&amp;quot;-2^55&amp;quot;,7.20575e+16,1.20892e+24],[&amp;quot;-2^54&amp;quot;,3.60288e+16,6.04462e+23],[&amp;quot;-2^53&amp;quot;,1.80144e+16,3.02231e+23],[&amp;quot;-2^52&amp;quot;,9.00719e+15,1.51116e+23],[&amp;quot;-2^51&amp;quot;,4.5036e+15,7.55578e+22],[&amp;quot;-2^50&amp;quot;,2.2518e+15,3.77789e+22],[&amp;quot;-2^49&amp;quot;,1.1259e+15,1.88894e+22],[&amp;quot;-2^48&amp;quot;,5.62949e+14,9.44472e+21],[&amp;quot;-2^47&amp;quot;,2.81475e+14,4.72236e+21],[&amp;quot;-2^46&amp;quot;,1.40737e+14,2.36118e+21],[&amp;quot;-2^45&amp;quot;,7.03687e+13,1.18059e+21],[&amp;quot;-2^44&amp;quot;,3.51843e+13,5.90295e+20],[&amp;quot;-2^43&amp;quot;,1.75922e+13,2.95148e+20],[&amp;quot;-2^42&amp;quot;,8.79608e+12,1.47574e+20],[&amp;quot;-2^41&amp;quot;,4.39804e+12,7.37869e+19],[&amp;quot;-2^40&amp;quot;,2.19902e+12,3.68935e+19],[&amp;quot;-2^39&amp;quot;,1.09951e+12,1.84467e+19],[&amp;quot;-2^38&amp;quot;,5.49755e+11,9.22336e+18],[&amp;quot;-2^37&amp;quot;,2.74878e+11,4.61168e+18],[&amp;quot;-2^36&amp;quot;,1.37439e+11,2.30584e+18],[&amp;quot;-2^35&amp;quot;,6.87194e+10,1.15292e+18],[&amp;quot;-2^34&amp;quot;,3.43597e+10,5.7646e+17],[&amp;quot;-2^33&amp;quot;,1.71799e+10,2.8823e+17],[&amp;quot;-2^32&amp;quot;,8.58993e+09,1.44115e+17],[&amp;quot;-2^31&amp;quot;,4.29085e+09,7.19885e+16],[&amp;quot;-2^30&amp;quot;,2.14542e+09,3.59943e+16],[&amp;quot;-2^29&amp;quot;,1.07271e+09,1.79971e+16],[&amp;quot;-2^28&amp;quot;,5.36356e+08,8.99856e+15],[&amp;quot;-2^27&amp;quot;,2.68178e+08,4.49928e+15],[&amp;quot;-2^26&amp;quot;,1.34089e+08,2.24964e+15],[&amp;quot;-2^25&amp;quot;,6.70445e+07,1.12482e+15],[&amp;quot;-2^24&amp;quot;,3.35223e+07,5.6241e+14],[&amp;quot;-2^23&amp;quot;,1.67611e+07,2.81205e+14],[&amp;quot;-2^22&amp;quot;,8.38056e+06,1.40603e+14],[&amp;quot;-2^21&amp;quot;,4.19028e+06,7.03013e+13],[&amp;quot;-2^20&amp;quot;,2.09514e+06,3.51506e+13],[&amp;quot;-2^19&amp;quot;,1.04757e+06,1.75753e+13],[&amp;quot;-2^18&amp;quot;,523784,8.78764e+12],[&amp;quot;-2^17&amp;quot;,261892,4.39381e+12],[&amp;quot;-2^16&amp;quot;,130945,2.1969e+12],[&amp;quot;-2^15&amp;quot;,65472.2,1.09844e+12],[&amp;quot;-2^14&amp;quot;,32735.6,5.49212e+11],[&amp;quot;-2^13&amp;quot;,16367.3,2.74598e+11],[&amp;quot;-2^12&amp;quot;,8183.15,1.3729e+11],[&amp;quot;-2^11&amp;quot;,4091.07,6.86368e+10],[&amp;quot;-2^10&amp;quot;,2045.04,3.431e+10],[&amp;quot;-2^9&amp;quot;,1022.02,1.71466e+10],[&amp;quot;-2^8&amp;quot;,510.509,8.56492e+09],[&amp;quot;-2^7&amp;quot;,254.754,4.27406e+09],[&amp;quot;-2^6&amp;quot;,126.876,2.12862e+09],[&amp;quot;-2^5&amp;quot;,62.9354,1.05588e+09],[&amp;quot;-2^4&amp;quot;,30.9629,5.19471e+08],[&amp;quot;-2^3&amp;quot;,14.9718,2.51185e+08],[&amp;quot;-2^2&amp;quot;,6.96672,1.16882e+08],[&amp;quot;-2^1&amp;quot;,2.94633,4.94312e+07],[&amp;quot;-2^0&amp;quot;,0.940715,1.57826e+07],[&amp;quot;-2^-1&amp;quot;,0.127635,2.14135e+06],[&amp;quot;-2^-2&amp;quot;,0.00318735,106950],[&amp;quot;-2^-3&amp;quot;,3.11475e-05,2090.28],[&amp;quot;-2^-4&amp;quot;,2.57737e-07,34.5929],[&amp;quot;-2^-5&amp;quot;,3.89213e-09,1.04478],[&amp;quot;-2^-6&amp;quot;,9.51472e-10,0.510818],[&amp;quot;-2^-7&amp;quot;,4.66402e-10,0.500795],[&amp;quot;-2^-8&amp;quot;,2.32917e-10,0.500185],[&amp;quot;-2^-9&amp;quot;,1.16423e-10,0.500033],[&amp;quot;-2^-10&amp;quot;,5.82082e-11,0.500004],[&amp;quot;-2^-11&amp;quot;,2.91037e-11,0.499998],[&amp;quot;-2^-12&amp;quot;,1.45519e-11,0.5],[&amp;quot;-2^-13&amp;quot;,4.85062e-12,0.333332],[&amp;quot;-2^-14&amp;quot;,6.06328e-13,0.0833331],[&amp;quot;-2^-15&amp;quot;,7.5791e-14,0.0208333],[&amp;quot;-2^-16&amp;quot;,9.47387e-15,0.00520832],[&amp;quot;-2^-17&amp;quot;,1.18424e-15,0.00130208],[&amp;quot;-2^-18&amp;quot;,1.48029e-16,0.00032552],[&amp;quot;-2^-19&amp;quot;,1.85039e-17,8.13808e-05],[&amp;quot;-2^-20&amp;quot;,2.31304e-18,2.03457e-05],[&amp;quot;-2^-21&amp;quot;,2.89156e-19,5.08688e-06],[&amp;quot;-2^-22&amp;quot;,3.61577e-20,1.27219e-06],[&amp;quot;-2^-23&amp;quot;,4.52633e-21,3.18512e-07],[&amp;quot;-2^-24&amp;quot;,5.691e-22,8.00937e-08],[&amp;quot;-2^-25&amp;quot;,7.27919e-23,2.04891e-08],[&amp;quot;-2^-26&amp;quot;,9.92617e-24,5.58794e-09],[&amp;quot;-2^-27&amp;quot;,1.65436e-24,1.86265e-09],[&amp;quot;-2^-28&amp;quot;,8.27181e-25,1.86265e-09],[&amp;quot;-2^-29&amp;quot;,4.1359e-25,1.86265e-09],[&amp;quot;-2^-30&amp;quot;,2.06795e-25,1.86265e-09],[&amp;quot;-2^-31&amp;quot;,1.03398e-25,1.86265e-09],[&amp;quot;-2^-32&amp;quot;,5.16988e-26,1.86265e-09],[&amp;quot;-2^-33&amp;quot;,2.58494e-26,1.86265e-09],[&amp;quot;-2^-34&amp;quot;,1.29247e-26,1.86265e-09],[&amp;quot;-2^-35&amp;quot;,6.46235e-27,1.86265e-09],[&amp;quot;-2^-36&amp;quot;,3.23117e-27,1.86265e-09],[&amp;quot;-2^-37&amp;quot;,1.61559e-27,1.86265e-09],[&amp;quot;-2^-38&amp;quot;,8.07794e-28,1.86265e-09],[&amp;quot;-2^-39&amp;quot;,4.03897e-28,1.86265e-09],[&amp;quot;-2^-40&amp;quot;,2.01948e-28,1.86265e-09],[&amp;quot;-2^-41&amp;quot;,1.00974e-28,1.86265e-09],[&amp;quot;-2^-42&amp;quot;,5.04871e-29,1.86265e-09],[&amp;quot;-2^-43&amp;quot;,2.52435e-29,1.86265e-09],[&amp;quot;-2^-44&amp;quot;,1.26218e-29,1.86265e-09],[&amp;quot;-2^-45&amp;quot;,6.31089e-30,1.86265e-09],[&amp;quot;-2^-46&amp;quot;,3.15544e-30,1.86265e-09],[&amp;quot;-2^-47&amp;quot;,1.57772e-30,1.86265e-09],[&amp;quot;-2^-48&amp;quot;,7.88861e-31,1.86265e-09],[&amp;quot;-2^-49&amp;quot;,3.9443e-31,1.86265e-09],[&amp;quot;-2^-50&amp;quot;,1.97215e-31,1.86265e-09],[&amp;quot;-2^-51&amp;quot;,9.86076e-32,1.86265e-09],[&amp;quot;-2^-52&amp;quot;,4.93038e-32,1.86265e-09],[&amp;quot;-2^-53&amp;quot;,2.46519e-32,1.86265e-09],[&amp;quot;-2^-54&amp;quot;,1.2326e-32,1.86265e-09],[&amp;quot;-2^-55&amp;quot;,0,0],[&amp;quot;-2^-56&amp;quot;,0,0],[&amp;quot;-2^-57&amp;quot;,0,0],[&amp;quot;-2^-58&amp;quot;,0,0],[&amp;quot;-2^-59&amp;quot;,0,0],[&amp;quot;-2^-60&amp;quot;,0,0],[&amp;quot;-2^-61&amp;quot;,0,0],[&amp;quot;-2^-62&amp;quot;,0,0],[&amp;quot;-2^-63&amp;quot;,0,0],[&amp;quot;-2^-64&amp;quot;,0,0],[&amp;quot;-2^-65&amp;quot;,0,0],[&amp;quot;-2^-66&amp;quot;,0,0],[&amp;quot;-2^-67&amp;quot;,0,0],[&amp;quot;-2^-68&amp;quot;,0,0],[&amp;quot;-2^-69&amp;quot;,0,0],[&amp;quot;-2^-70&amp;quot;,0,0],[&amp;quot;-2^-71&amp;quot;,0,0],[&amp;quot;-2^-72&amp;quot;,0,0],[&amp;quot;-2^-73&amp;quot;,0,0],[&amp;quot;-2^-74&amp;quot;,0,0],[&amp;quot;-2^-75&amp;quot;,0,0],[&amp;quot;-2^-76&amp;quot;,0,0],[&amp;quot;-2^-77&amp;quot;,0,0],[&amp;quot;-2^-78&amp;quot;,0,0],[&amp;quot;-2^-79&amp;quot;,0,0],[&amp;quot;-2^-80&amp;quot;,0,0],[&amp;quot;-2^-81&amp;quot;,0,0],[&amp;quot;-2^-82&amp;quot;,0,0],[&amp;quot;-2^-83&amp;quot;,0,0],[&amp;quot;-2^-84&amp;quot;,0,0],[&amp;quot;-2^-85&amp;quot;,0,0],[&amp;quot;-2^-86&amp;quot;,0,0],[&amp;quot;-2^-87&amp;quot;,0,0],[&amp;quot;-2^-88&amp;quot;,0,0],[&amp;quot;-2^-89&amp;quot;,0,0],[&amp;quot;-2^-90&amp;quot;,0,0],[&amp;quot;-2^-91&amp;quot;,0,0],[&amp;quot;-2^-92&amp;quot;,0,0],[&amp;quot;-2^-93&amp;quot;,0,0],[&amp;quot;-2^-94&amp;quot;,0,0],[&amp;quot;-2^-95&amp;quot;,0,0],[&amp;quot;-2^-96&amp;quot;,0,0],[&amp;quot;-2^-97&amp;quot;,0,0],[&amp;quot;-2^-98&amp;quot;,0,0],[&amp;quot;-2^-99&amp;quot;,0,0],[&amp;quot;-2^-100&amp;quot;,0,0],[&amp;quot;-2^-101&amp;quot;,0,0],[&amp;quot;-2^-102&amp;quot;,0,0],[&amp;quot;-2^-103&amp;quot;,0,0],[&amp;quot;-2^-104&amp;quot;,0,0],[&amp;quot;-2^-105&amp;quot;,0,0],[&amp;quot;-2^-106&amp;quot;,0,0],[&amp;quot;-2^-107&amp;quot;,0,0],[&amp;quot;-2^-108&amp;quot;,0,0],[&amp;quot;-2^-109&amp;quot;,0,0],[&amp;quot;-2^-110&amp;quot;,0,0],[&amp;quot;-2^-111&amp;quot;,0,0],[&amp;quot;-2^-112&amp;quot;,0,0],[&amp;quot;-2^-113&amp;quot;,0,0],[&amp;quot;-2^-114&amp;quot;,0,0],[&amp;quot;-2^-115&amp;quot;,0,0],[&amp;quot;-2^-116&amp;quot;,0,0],[&amp;quot;-2^-117&amp;quot;,0,0],[&amp;quot;-2^-118&amp;quot;,0,0],[&amp;quot;-2^-119&amp;quot;,0,0],[&amp;quot;-2^-120&amp;quot;,0,0],[&amp;quot;-2^-121&amp;quot;,0,0],[&amp;quot;-2^-122&amp;quot;,0,0],[&amp;quot;-2^-123&amp;quot;,0,0],[&amp;quot;-2^-124&amp;quot;,0,0],[&amp;quot;-2^-125&amp;quot;,0,0],[&amp;quot;-2^-126&amp;quot;,0,0],[&amp;quot;-SN&amp;quot;,0,0],[&amp;quot;0&amp;quot;,0,0],[&amp;quot;+SN&amp;quot;,0,0],[&amp;quot;+2^-126&amp;quot;,0,0],[&amp;quot;+2^-125&amp;quot;,0,0],[&amp;quot;+2^-124&amp;quot;,0,0],[&amp;quot;+2^-123&amp;quot;,0,0],[&amp;quot;+2^-122&amp;quot;,0,0],[&amp;quot;+2^-121&amp;quot;,0,0],[&amp;quot;+2^-120&amp;quot;,0,0],[&amp;quot;+2^-119&amp;quot;,0,0],[&amp;quot;+2^-118&amp;quot;,0,0],[&amp;quot;+2^-117&amp;quot;,0,0],[&amp;quot;+2^-116&amp;quot;,0,0],[&amp;quot;+2^-115&amp;quot;,0,0],[&amp;quot;+2^-114&amp;quot;,0,0],[&amp;quot;+2^-113&amp;quot;,0,0],[&amp;quot;+2^-112&amp;quot;,0,0],[&amp;quot;+2^-111&amp;quot;,0,0],[&amp;quot;+2^-110&amp;quot;,0,0],[&amp;quot;+2^-109&amp;quot;,0,0],[&amp;quot;+2^-108&amp;quot;,0,0],[&amp;quot;+2^-107&amp;quot;,0,0],[&amp;quot;+2^-106&amp;quot;,0,0],[&amp;quot;+2^-105&amp;quot;,0,0],[&amp;quot;+2^-104&amp;quot;,0,0],[&amp;quot;+2^-103&amp;quot;,0,0],[&amp;quot;+2^-102&amp;quot;,0,0],[&amp;quot;+2^-101&amp;quot;,0,0],[&amp;quot;+2^-100&amp;quot;,0,0],[&amp;quot;+2^-99&amp;quot;,0,0],[&amp;quot;+2^-98&amp;quot;,0,0],[&amp;quot;+2^-97&amp;quot;,0,0],[&amp;quot;+2^-96&amp;quot;,0,0],[&amp;quot;+2^-95&amp;quot;,0,0],[&amp;quot;+2^-94&amp;quot;,0,0],[&amp;quot;+2^-93&amp;quot;,0,0],[&amp;quot;+2^-92&amp;quot;,0,0],[&amp;quot;+2^-91&amp;quot;,0,0],[&amp;quot;+2^-90&amp;quot;,0,0],[&amp;quot;+2^-89&amp;quot;,0,0],[&amp;quot;+2^-88&amp;quot;,0,0],[&amp;quot;+2^-87&amp;quot;,0,0],[&amp;quot;+2^-86&amp;quot;,0,0],[&amp;quot;+2^-85&amp;quot;,0,0],[&amp;quot;+2^-84&amp;quot;,0,0],[&amp;quot;+2^-83&amp;quot;,0,0],[&amp;quot;+2^-82&amp;quot;,0,0],[&amp;quot;+2^-81&amp;quot;,0,0],[&amp;quot;+2^-80&amp;quot;,0,0],[&amp;quot;+2^-79&amp;quot;,0,0],[&amp;quot;+2^-78&amp;quot;,0,0],[&amp;quot;+2^-77&amp;quot;,0,0],[&amp;quot;+2^-76&amp;quot;,0,0],[&amp;quot;+2^-75&amp;quot;,0,0],[&amp;quot;+2^-74&amp;quot;,0,0],[&amp;quot;+2^-73&amp;quot;,0,0],[&amp;quot;+2^-72&amp;quot;,0,0],[&amp;quot;+2^-71&amp;quot;,0,0],[&amp;quot;+2^-70&amp;quot;,0,0],[&amp;quot;+2^-69&amp;quot;,0,0],[&amp;quot;+2^-68&amp;quot;,0,0],[&amp;quot;+2^-67&amp;quot;,0,0],[&amp;quot;+2^-66&amp;quot;,0,0],[&amp;quot;+2^-65&amp;quot;,0,0],[&amp;quot;+2^-64&amp;quot;,0,0],[&amp;quot;+2^-63&amp;quot;,0,0],[&amp;quot;+2^-62&amp;quot;,0,0],[&amp;quot;+2^-61&amp;quot;,0,0],[&amp;quot;+2^-60&amp;quot;,0,0],[&amp;quot;+2^-59&amp;quot;,0,0],[&amp;quot;+2^-58&amp;quot;,0,0],[&amp;quot;+2^-57&amp;quot;,0,0],[&amp;quot;+2^-56&amp;quot;,0,0],[&amp;quot;+2^-55&amp;quot;,0,0],[&amp;quot;+2^-54&amp;quot;,1.2326e-32,1.86265e-09],[&amp;quot;+2^-53&amp;quot;,2.46519e-32,1.86265e-09],[&amp;quot;+2^-52&amp;quot;,4.93038e-32,1.86265e-09],[&amp;quot;+2^-51&amp;quot;,9.86076e-32,1.86265e-09],[&amp;quot;+2^-50&amp;quot;,1.97215e-31,1.86265e-09],[&amp;quot;+2^-49&amp;quot;,3.9443e-31,1.86265e-09],[&amp;quot;+2^-48&amp;quot;,7.88861e-31,1.86265e-09],[&amp;quot;+2^-47&amp;quot;,1.57772e-30,1.86265e-09],[&amp;quot;+2^-46&amp;quot;,3.15544e-30,1.86265e-09],[&amp;quot;+2^-45&amp;quot;,6.31089e-30,1.86265e-09],[&amp;quot;+2^-44&amp;quot;,1.26218e-29,1.86265e-09],[&amp;quot;+2^-43&amp;quot;,2.52435e-29,1.86265e-09],[&amp;quot;+2^-42&amp;quot;,5.04871e-29,1.86265e-09],[&amp;quot;+2^-41&amp;quot;,1.00974e-28,1.86265e-09],[&amp;quot;+2^-40&amp;quot;,2.01948e-28,1.86265e-09],[&amp;quot;+2^-39&amp;quot;,4.03897e-28,1.86265e-09],[&amp;quot;+2^-38&amp;quot;,8.07794e-28,1.86265e-09],[&amp;quot;+2^-37&amp;quot;,1.61559e-27,1.86265e-09],[&amp;quot;+2^-36&amp;quot;,3.23117e-27,1.86265e-09],[&amp;quot;+2^-35&amp;quot;,6.46235e-27,1.86265e-09],[&amp;quot;+2^-34&amp;quot;,1.29247e-26,1.86265e-09],[&amp;quot;+2^-33&amp;quot;,2.58494e-26,1.86265e-09],[&amp;quot;+2^-32&amp;quot;,5.16988e-26,1.86265e-09],[&amp;quot;+2^-31&amp;quot;,1.03398e-25,1.86265e-09],[&amp;quot;+2^-30&amp;quot;,2.06795e-25,1.86265e-09],[&amp;quot;+2^-29&amp;quot;,4.1359e-25,1.86265e-09],[&amp;quot;+2^-28&amp;quot;,8.27181e-25,1.86265e-09],[&amp;quot;+2^-27&amp;quot;,1.65436e-24,1.86265e-09],[&amp;quot;+2^-26&amp;quot;,9.92617e-24,5.58794e-09],[&amp;quot;+2^-25&amp;quot;,7.27919e-23,2.04891e-08],[&amp;quot;+2^-24&amp;quot;,5.691e-22,8.00937e-08],[&amp;quot;+2^-23&amp;quot;,4.52633e-21,3.18512e-07],[&amp;quot;+2^-22&amp;quot;,3.61577e-20,1.27219e-06],[&amp;quot;+2^-21&amp;quot;,2.89156e-19,5.08688e-06],[&amp;quot;+2^-20&amp;quot;,2.31304e-18,2.03457e-05],[&amp;quot;+2^-19&amp;quot;,1.85039e-17,8.13808e-05],[&amp;quot;+2^-18&amp;quot;,1.48029e-16,0.00032552],[&amp;quot;+2^-17&amp;quot;,1.18424e-15,0.00130208],[&amp;quot;+2^-16&amp;quot;,9.47387e-15,0.00520832],[&amp;quot;+2^-15&amp;quot;,7.5791e-14,0.0208333],[&amp;quot;+2^-14&amp;quot;,6.06328e-13,0.0833331],[&amp;quot;+2^-13&amp;quot;,4.85062e-12,0.333332],[&amp;quot;+2^-12&amp;quot;,1.45519e-11,0.5],[&amp;quot;+2^-11&amp;quot;,2.91037e-11,0.499998],[&amp;quot;+2^-10&amp;quot;,5.82082e-11,0.500004],[&amp;quot;+2^-9&amp;quot;,1.16423e-10,0.500033],[&amp;quot;+2^-8&amp;quot;,2.32917e-10,0.500185],[&amp;quot;+2^-7&amp;quot;,4.66402e-10,0.500795],[&amp;quot;+2^-6&amp;quot;,9.51472e-10,0.510818],[&amp;quot;+2^-5&amp;quot;,3.89213e-09,1.04478],[&amp;quot;+2^-4&amp;quot;,2.57737e-07,34.5929],[&amp;quot;+2^-3&amp;quot;,3.11475e-05,2090.28],[&amp;quot;+2^-2&amp;quot;,0.00318735,106950],[&amp;quot;+2^-1&amp;quot;,0.127635,2.14135e+06],[&amp;quot;+2^0&amp;quot;,0.940715,1.57826e+07],[&amp;quot;+2^1&amp;quot;,2.94633,4.94312e+07],[&amp;quot;+2^2&amp;quot;,6.96672,1.16882e+08],[&amp;quot;+2^3&amp;quot;,14.9718,1.33874e+08],[&amp;quot;+2^4&amp;quot;,30.9629,2.59736e+08],[&amp;quot;+2^5&amp;quot;,62.9354,5.27941e+08],[&amp;quot;+2^6&amp;quot;,126.876,1.06431e+09],[&amp;quot;+2^7&amp;quot;,254.754,2.13703e+09],[&amp;quot;+2^8&amp;quot;,510.509,4.28246e+09],[&amp;quot;+2^9&amp;quot;,1022.02,8.57331e+09],[&amp;quot;+2^10&amp;quot;,2045.04,1.7155e+10],[&amp;quot;+2^11&amp;quot;,4091.07,3.43184e+10],[&amp;quot;+2^12&amp;quot;,8183.15,6.86452e+10],[&amp;quot;+2^13&amp;quot;,16367.3,1.37299e+11],[&amp;quot;+2^14&amp;quot;,32735.6,2.74606e+11],[&amp;quot;+2^15&amp;quot;,65472.2,5.4922e+11],[&amp;quot;+2^16&amp;quot;,130945,1.09845e+12],[&amp;quot;+2^17&amp;quot;,261892,2.19691e+12],[&amp;quot;+2^18&amp;quot;,523784,4.39382e+12],[&amp;quot;+2^19&amp;quot;,1.04757e+06,8.78765e+12],[&amp;quot;+2^20&amp;quot;,2.09514e+06,1.75753e+13],[&amp;quot;+2^21&amp;quot;,4.19028e+06,3.51506e+13],[&amp;quot;+2^22&amp;quot;,8.38056e+06,7.03013e+13],[&amp;quot;+2^23&amp;quot;,1.67611e+07,1.40603e+14],[&amp;quot;+2^24&amp;quot;,3.35223e+07,2.81205e+14],[&amp;quot;+2^25&amp;quot;,6.70445e+07,5.6241e+14],[&amp;quot;+2^26&amp;quot;,1.34089e+08,1.12482e+15],[&amp;quot;+2^27&amp;quot;,2.68178e+08,2.24964e+15],[&amp;quot;+2^28&amp;quot;,5.36356e+08,4.49928e+15],[&amp;quot;+2^29&amp;quot;,1.07271e+09,8.99856e+15],[&amp;quot;+2^30&amp;quot;,2.14542e+09,1.79971e+16],[&amp;quot;+2^31&amp;quot;,4.29085e+09,3.59943e+16],[&amp;quot;+2^32&amp;quot;,8.58993e+09,7.20575e+16],[&amp;quot;+2^33&amp;quot;,1.71799e+10,1.44115e+17],[&amp;quot;+2^34&amp;quot;,3.43597e+10,2.8823e+17],[&amp;quot;+2^35&amp;quot;,6.87194e+10,5.7646e+17],[&amp;quot;+2^36&amp;quot;,1.37439e+11,1.15292e+18],[&amp;quot;+2^37&amp;quot;,2.74878e+11,2.30584e+18],[&amp;quot;+2^38&amp;quot;,5.49755e+11,4.61168e+18],[&amp;quot;+2^39&amp;quot;,1.09951e+12,9.22336e+18],[&amp;quot;+2^40&amp;quot;,2.19902e+12,1.84467e+19],[&amp;quot;+2^41&amp;quot;,4.39804e+12,3.68935e+19],[&amp;quot;+2^42&amp;quot;,8.79608e+12,7.37869e+19],[&amp;quot;+2^43&amp;quot;,1.75922e+13,1.47574e+20],[&amp;quot;+2^44&amp;quot;,3.51843e+13,2.95148e+20],[&amp;quot;+2^45&amp;quot;,7.03687e+13,5.90295e+20],[&amp;quot;+2^46&amp;quot;,1.40737e+14,1.18059e+21],[&amp;quot;+2^47&amp;quot;,2.81475e+14,2.36118e+21],[&amp;quot;+2^48&amp;quot;,5.62949e+14,4.72236e+21],[&amp;quot;+2^49&amp;quot;,1.1259e+15,9.44472e+21],[&amp;quot;+2^50&amp;quot;,2.2518e+15,1.88894e+22],[&amp;quot;+2^51&amp;quot;,4.5036e+15,3.77789e+22],[&amp;quot;+2^52&amp;quot;,9.00719e+15,7.55578e+22],[&amp;quot;+2^53&amp;quot;,1.80144e+16,1.51116e+23],[&amp;quot;+2^54&amp;quot;,3.60288e+16,3.02231e+23],[&amp;quot;+2^55&amp;quot;,7.20575e+16,6.04462e+23],[&amp;quot;+2^56&amp;quot;,1.44115e+17,1.20892e+24],[&amp;quot;+2^57&amp;quot;,2.8823e+17,2.41785e+24],[&amp;quot;+2^58&amp;quot;,5.7646e+17,4.8357e+24],[&amp;quot;+2^59&amp;quot;,1.15292e+18,9.6714e+24],[&amp;quot;+2^60&amp;quot;,2.30584e+18,1.93428e+25],[&amp;quot;+2^61&amp;quot;,4.61168e+18,3.86856e+25],[&amp;quot;+2^62&amp;quot;,9.22336e+18,7.73712e+25],[&amp;quot;+2^63&amp;quot;,1.84467e+19,1.54742e+26],[&amp;quot;+2^64&amp;quot;,0,0],[&amp;quot;+2^65&amp;quot;,0,0],[&amp;quot;+2^66&amp;quot;,0,0],[&amp;quot;+2^67&amp;quot;,0,0],[&amp;quot;+2^68&amp;quot;,0,0],[&amp;quot;+2^69&amp;quot;,0,0],[&amp;quot;+2^70&amp;quot;,0,0],[&amp;quot;+2^71&amp;quot;,0,0],[&amp;quot;+2^72&amp;quot;,0,0],[&amp;quot;+2^73&amp;quot;,0,0],[&amp;quot;+2^74&amp;quot;,0,0],[&amp;quot;+2^75&amp;quot;,0,0],[&amp;quot;+2^76&amp;quot;,0,0],[&amp;quot;+2^77&amp;quot;,0,0],[&amp;quot;+2^78&amp;quot;,0,0],[&amp;quot;+2^79&amp;quot;,0,0],[&amp;quot;+2^80&amp;quot;,0,0],[&amp;quot;+2^81&amp;quot;,0,0],[&amp;quot;+2^82&amp;quot;,0,0],[&amp;quot;+2^83&amp;quot;,0,0],[&amp;quot;+2^84&amp;quot;,0,0],[&amp;quot;+2^85&amp;quot;,0,0],[&amp;quot;+2^86&amp;quot;,0,0],[&amp;quot;+2^87&amp;quot;,0,0],[&amp;quot;+2^88&amp;quot;,0,0],[&amp;quot;+2^89&amp;quot;,0,0],[&amp;quot;+2^90&amp;quot;,0,0],[&amp;quot;+2^91&amp;quot;,0,0],[&amp;quot;+2^92&amp;quot;,0,0],[&amp;quot;+2^93&amp;quot;,0,0],[&amp;quot;+2^94&amp;quot;,0,0],[&amp;quot;+2^95&amp;quot;,0,0],[&amp;quot;+2^96&amp;quot;,0,0],[&amp;quot;+2^97&amp;quot;,0,0],[&amp;quot;+2^98&amp;quot;,0,0],[&amp;quot;+2^99&amp;quot;,0,0],[&amp;quot;+2^100&amp;quot;,0,0],[&amp;quot;+2^101&amp;quot;,0,0],[&amp;quot;+2^102&amp;quot;,0,0],[&amp;quot;+2^103&amp;quot;,0,0],[&amp;quot;+2^104&amp;quot;,0,0],[&amp;quot;+2^105&amp;quot;,0,0],[&amp;quot;+2^106&amp;quot;,0,0],[&amp;quot;+2^107&amp;quot;,0,0],[&amp;quot;+2^108&amp;quot;,0,0],[&amp;quot;+2^109&amp;quot;,0,0],[&amp;quot;+2^110&amp;quot;,0,0],[&amp;quot;+2^111&amp;quot;,0,0],[&amp;quot;+2^112&amp;quot;,0,0],[&amp;quot;+2^113&amp;quot;,0,0],[&amp;quot;+2^114&amp;quot;,0,0],[&amp;quot;+2^115&amp;quot;,0,0],[&amp;quot;+2^116&amp;quot;,0,0],[&amp;quot;+2^117&amp;quot;,0,0],[&amp;quot;+2^118&amp;quot;,0,0],[&amp;quot;+2^119&amp;quot;,0,0],[&amp;quot;+2^120&amp;quot;,0,0],[&amp;quot;+2^121&amp;quot;,0,0],[&amp;quot;+2^122&amp;quot;,0,0],[&amp;quot;+2^123&amp;quot;,0,0],[&amp;quot;+2^124&amp;quot;,0,0],[&amp;quot;+2^125&amp;quot;,0,0],[&amp;quot;+2^126&amp;quot;,0,0],[&amp;quot;+2^127&amp;quot;,0,0] ]&lt;br /&gt;
 }}&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Key observation:&#039;&#039;&#039;  &lt;br /&gt;
The Taylor approximation is highly accurate near zero but diverges extremely fast outside its convergence region.  &lt;br /&gt;
This makes it unsuitable as a standalone &amp;lt;code&amp;gt;tanh&amp;lt;/code&amp;gt; implementation, but very effective as a local approximation when combined with range-based dispatching.&lt;br /&gt;
&lt;br /&gt;
== Localized Approximation Functions ==&lt;br /&gt;
&lt;br /&gt;
Localized approximation functions are designed to provide highly accurate and computationally efficient evaluations of &amp;lt;code&amp;gt;tanh(x)&amp;lt;/code&amp;gt; within a restricted input range. &lt;br /&gt;
They are typically used as building blocks in range-based dispatch strategies, where different approximations are selected depending on the magnitude of the input value.&lt;br /&gt;
&lt;br /&gt;
By limiting the valid input range, these approximations can achieve very low ULP error with simple polynomial or rational expressions, avoiding expensive transcendental functions.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
!Id&lt;br /&gt;
!Formula&lt;br /&gt;
!ULP &amp;lt;= 2.0&lt;br /&gt;
!NaN&lt;br /&gt;
|-&lt;br /&gt;
|signum&lt;br /&gt;
|&amp;lt;math&amp;gt;\sgn{x}&amp;lt;/math&amp;gt;&lt;br /&gt;
| -inf &amp;lt; x &amp;lt; -8.31763&lt;br /&gt;
8.31763 &amp;lt; x &amp;lt; inf&lt;br /&gt;
| -&lt;br /&gt;
|-&lt;br /&gt;
|poly1&lt;br /&gt;
|&amp;lt;math&amp;gt;x&amp;lt;/math&amp;gt;&lt;br /&gt;
| -0.000704229 &amp;lt; x &amp;lt; 0.000704229&lt;br /&gt;
| -&lt;br /&gt;
|-&lt;br /&gt;
|poly3&lt;br /&gt;
|&amp;lt;math&amp;gt;x - \frac{1}{3} x^3&amp;lt;/math&amp;gt;&lt;br /&gt;
| -0.0291781 &amp;lt; x &amp;lt; 0.0291781 &lt;br /&gt;
| -&lt;br /&gt;
|-&lt;br /&gt;
|lambert7&lt;br /&gt;
|&amp;lt;math&amp;gt;\frac{x \cdot (135135 + x^2 \cdot (17325 + x^2 \cdot (378 + x^2)))}{135135 + x^2 \cdot (62370 + x^2 \cdot (3150 + 28 \cdot x^2))}&lt;br /&gt;
&amp;lt;/math&amp;gt;&lt;br /&gt;
| -0.000947416 &amp;lt; x &amp;lt; 0.000947207&lt;br /&gt;
| -inf &amp;lt; x &amp;lt; -1.51629e+06&lt;br /&gt;
1.51629e+06 &amp;lt; x &amp;lt; inf&lt;br /&gt;
|-&lt;br /&gt;
|pade&lt;br /&gt;
|&amp;lt;math&amp;gt;x + x \cdot \frac{x^2 \cdot \left( p_1 \cdot x^2 + p_0 \right)}{\left( x^2 + q_1 \right) \cdot x^2 + q_0} &lt;br /&gt;
&lt;br /&gt;
&amp;lt;/math&amp;gt; &amp;lt;math&amp;gt;\begin{align}&lt;br /&gt;
p_0 &amp;amp;= -0.2059432032 \\&lt;br /&gt;
p_1 &amp;amp;= -0.0009577527 \\&lt;br /&gt;
q_0 &amp;amp;= 0.6178299136 \\ &lt;br /&gt;
q_1 &amp;amp;= 0.25&lt;br /&gt;
\end{align}&lt;br /&gt;
&amp;lt;/math&amp;gt;&lt;br /&gt;
| -0.0016973 &amp;lt; x &amp;lt; 0.0016973 &lt;br /&gt;
| -inf &amp;lt; x &amp;lt; -6.14896e+18&lt;br /&gt;
6.14896e+18 &amp;lt; x &amp;lt; inf&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
The functions listed above are not intended to be used independently over the full input domain. &lt;br /&gt;
Instead, they are commonly combined with explicit range checks and sign handling to form a complete &amp;lt;code&amp;gt;tanh&amp;lt;/code&amp;gt; implementation.&lt;br /&gt;
&lt;br /&gt;
For example, low-order polynomials are well suited for very small &amp;lt;math&amp;gt;|x|&amp;lt;/math&amp;gt;, while rational or higher-order approximations extend the usable range before switching to exponential-based formulations or saturation.&lt;br /&gt;
&lt;br /&gt;
==Recommended Implementation Strategy==&lt;br /&gt;
A robust and high-performance &amp;lt;code&amp;gt;tanh(x)&amp;lt;/code&amp;gt; implementation typically combines multiple techniques, each optimized for a specific input range. No single formulation provides optimal accuracy and performance across the entire floating-point domain.&lt;br /&gt;
&lt;br /&gt;
A common strategy is based on range-dependent dispatching:&lt;br /&gt;
*&#039;&#039;&#039;Sign handling&#039;&#039;&#039;&lt;br /&gt;
: Use the odd symmetry of &amp;lt;code&amp;gt;tanh&amp;lt;/code&amp;gt; (&amp;lt;math&amp;gt;\tanh(-x) = -\tanh(x)&amp;lt;/math&amp;gt;) to reduce the implementation to non-negative inputs.*&#039;&#039;&#039;Very small inputs (&amp;lt;math&amp;gt;|x| \ll 1&amp;lt;/math&amp;gt;)&#039;&#039;&#039;&lt;br /&gt;
: Use a low-order polynomial or Taylor approximation, exploiting &amp;lt;math&amp;gt;\tanh(x) \approx x&amp;lt;/math&amp;gt; to avoid cancellation and expensive transcendental functions.&lt;br /&gt;
*&#039;&#039;&#039;Small to medium inputs&#039;&#039;&#039;&lt;br /&gt;
: Apply a localized polynomial or rational approximation with bounded ULP error.&lt;br /&gt;
*&#039;&#039;&#039;Medium to large inputs&#039;&#039;&#039;&lt;br /&gt;
: Use an &amp;lt;code&amp;gt;expm1&amp;lt;/code&amp;gt;-based formulation to maintain numerical stability while avoiding catastrophic cancellation.&lt;br /&gt;
*&#039;&#039;&#039;Very large inputs&#039;&#039;&#039;&lt;br /&gt;
: Directly return &amp;lt;code&amp;gt;±1&amp;lt;/code&amp;gt;, as &amp;lt;code&amp;gt;tanh(x)&amp;lt;/code&amp;gt; saturates rapidly and additional computation provides no meaningful benefit.&lt;br /&gt;
This hybrid approach minimizes ULP error, avoids NaN generation, and allows fine-grained control over the accuracy–performance trade-off. It is widely used in optimized math libraries and hardware-accelerated implementations.&lt;br /&gt;
&lt;br /&gt;
== External Links ==&lt;br /&gt;
&lt;br /&gt;
* https://yaikhom.com/2020-04-28-localised-approximation-of-hyperbolic-tangents.html#mjx-eqn-eqn7-th%20degree%20lambert%20approximant&lt;br /&gt;
*https://github.com/llvm/llvm-project/blob/main/libc/src/math/generic/tanhf.cpp&lt;br /&gt;
*https://forums.developer.nvidia.com/t/hardware-accelerated-tanh-on-turing/173291&lt;br /&gt;
[[Category:Numerical Precision]]&lt;/div&gt;</summary>
		<author><name>Timo.stripf</name></author>
	</entry>
	<entry>
		<id>https://www.emmtrix.com/w139/index.php?title=tanh_Software_Implementation&amp;diff=2936</id>
		<title>tanh Software Implementation</title>
		<link rel="alternate" type="text/html" href="https://www.emmtrix.com/w139/index.php?title=tanh_Software_Implementation&amp;diff=2936"/>
		<updated>2026-02-04T12:14:06Z</updated>

		<summary type="html">&lt;p&gt;Timo.stripf: /* External Links */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;[[File:Hyperbolic Tangent.svg|thumb]]&lt;br /&gt;
The hyperbolic tangent function, commonly referred to as &amp;lt;code&amp;gt;tanh&amp;lt;/code&amp;gt;, is a mathematical function that maps real numbers to the range &amp;lt;math&amp;gt;(-1, 1)&amp;lt;/math&amp;gt;. It is defined as:&lt;br /&gt;
&lt;br /&gt;
:&amp;lt;math&amp;gt;\tanh(x) = \frac{e^x - e^{-x}}{e^x + e^{-x}}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
From a software implementation perspective, &amp;lt;code&amp;gt;tanh&amp;lt;/code&amp;gt; is a numerically challenging function due to the use of exponential functions, which can easily overflow for large inputs and suffer from catastrophic cancellation for very small values of &amp;lt;math&amp;gt;x&amp;lt;/math&amp;gt;. These issues are particularly relevant in single-precision floating-point (&amp;lt;code&amp;gt;float32&amp;lt;/code&amp;gt;) implementations, where both accuracy and performance are critical.&lt;br /&gt;
&lt;br /&gt;
In machine learning and deep learning, &amp;lt;code&amp;gt;tanh&amp;lt;/code&amp;gt; is widely used as an activation function because it introduces non-linearity and produces outputs centered around zero, which can improve gradient-based optimization. As a result, &amp;lt;code&amp;gt;tanh&amp;lt;/code&amp;gt; is often evaluated billions of times during training and inference, making efficient and numerically stable implementations essential.&lt;br /&gt;
&lt;br /&gt;
This article focuses on practical software implementations of &amp;lt;code&amp;gt;tanh&amp;lt;/code&amp;gt;, analyzing different computational formulations with respect to floating-point accuracy, ULP error, overflow behavior, and NaN generation. The goal is to provide a foundation for designing robust &amp;lt;code&amp;gt;tanh&amp;lt;/code&amp;gt; implementations suitable for performance-critical environments such as numerical libraries, embedded systems, and AI workloads.&lt;br /&gt;
&lt;br /&gt;
== Problem Overview ==&lt;br /&gt;
The hyperbolic tangent function poses several challenges when implemented in finite-precision floating-point arithmetic. While the mathematical definition is simple, direct translations into software often suffer from numerical instability and performance issues.&lt;br /&gt;
&lt;br /&gt;
A primary challenge is the use of exponential functions. For large input values, &amp;lt;code&amp;gt;exp(x)&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;exp(-x)&amp;lt;/code&amp;gt; can overflow in single-precision floating-point arithmetic, leading to infinities and, in some formulations, NaN results. Avoiding overflow therefore requires careful reformulation or explicit range handling.&lt;br /&gt;
&lt;br /&gt;
For very small input values, catastrophic cancellation becomes the dominant issue. Expressions such as &amp;lt;math&amp;gt;e^x - e^{-x}&amp;lt;/math&amp;gt; involve the subtraction of nearly equal numbers, causing a severe loss of significant bits and large ULP errors. In these regions, naive implementations may return zero or highly inaccurate results even though &amp;lt;math&amp;gt;\tanh(x) \approx x&amp;lt;/math&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
In addition to accuracy concerns, performance is a critical factor. In many applications, especially machine learning and numerical simulations, &amp;lt;code&amp;gt;tanh&amp;lt;/code&amp;gt; is evaluated extremely frequently. This makes it necessary to balance numerical robustness with the cost of transcendental function calls, branch complexity, and instruction-level efficiency.&lt;br /&gt;
&lt;br /&gt;
==Direct Exponential Formulations==&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+&lt;br /&gt;
!Id&lt;br /&gt;
!Formula&lt;br /&gt;
!Highest ULP Error&lt;br /&gt;
!ULP &amp;lt;= 2.0&lt;br /&gt;
!NaN&lt;br /&gt;
|-&lt;br /&gt;
|exp_v1&lt;br /&gt;
|&amp;lt;math&amp;gt;\frac{e^x - e^{-x}}{e^x + e^{-x}}&amp;lt;/math&amp;gt;&lt;br /&gt;
|x &amp;lt; 0: 8.55638e+08 at -2.98023e-08 with 0x0p+0 instead of -0x1p-25&lt;br /&gt;
&lt;br /&gt;
x &amp;gt; 0: 8.55638e+08 at 2.98023e-08 with 0x0p+0 instead of 0x1p-25&lt;br /&gt;
| -88.7246 &amp;lt; x &amp;lt; -7.14197&lt;br /&gt;
7.14197 &amp;lt; x &amp;lt; 88.7246&lt;br /&gt;
| -inf &amp;lt; x &amp;lt; -88.7246&lt;br /&gt;
88.7246 &amp;lt; x &amp;lt; inf&lt;br /&gt;
|-&lt;br /&gt;
|exp_v2&lt;br /&gt;
|&amp;lt;math&amp;gt;\frac{e^{2x} - 1}{e^{2x} + 1}&amp;lt;/math&amp;gt;&lt;br /&gt;
|x &amp;lt; 0: 8.47249e+08 at -1.49012e-08 with 0x0p+0 instead of -0x1p-26&lt;br /&gt;
x &amp;gt; 0: 8.55638e+08 at 2.98023e-08 with 0x0p+0 instead of 0x1.fffffep-26&lt;br /&gt;
| -inf &amp;lt; x &amp;lt; -0.547821&lt;br /&gt;
0.254822 &amp;lt; x &amp;lt; 8.31787&lt;br /&gt;
&lt;br /&gt;
8.38379 &amp;lt; x &amp;lt; 44.3623&lt;br /&gt;
|44.3623 &amp;lt; x &amp;lt; inf&lt;br /&gt;
|-&lt;br /&gt;
|exp_v3&lt;br /&gt;
|&amp;lt;math&amp;gt;1 - \frac{2}{e^{2x} + 1}&amp;lt;/math&amp;gt;&lt;br /&gt;
|x &amp;lt; 0: 8.59832e+08 at -4.47035e-08 with 0x0p+0 instead of -0x1.8p-25&lt;br /&gt;
x &amp;gt; 0: 8.68221e+08 at 8.9407e-08 with 0x0p+0 instead of 0x1.7ffffcp-24&lt;br /&gt;
| -inf &amp;lt; x &amp;lt; -6.26233&lt;br /&gt;
0.346382 &amp;lt; x &amp;lt; inf&lt;br /&gt;
| -&lt;br /&gt;
|-&lt;br /&gt;
|expm1&lt;br /&gt;
|&amp;lt;math&amp;gt;\frac{expm1(2x)}{expm1(2x) + 2}&amp;lt;/math&amp;gt; &lt;br /&gt;
|x &amp;lt; 0: 2.49585 at -3.95627 with -0x1.ffa00ap-1 instead of -0x1.ffa00ep-1&lt;br /&gt;
x &amp;gt; 0: 2.41657 at 0.0155837 with 0x1.fe9b66p-7 instead of 0x1.fe9b62p-7&lt;br /&gt;
| -inf &amp;lt; x &amp;lt; -5.57227&lt;br /&gt;
-0.42981 &amp;lt; x &amp;lt; 0.00777948&lt;br /&gt;
&lt;br /&gt;
0.125351 &amp;lt; x &amp;lt; 44.3623&lt;br /&gt;
|44.3623 &amp;lt; x &amp;lt; inf&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
The first equation is the definition of the hyperbolic tangent function. This equation requires two exponentials with different values. Since exponentials are expensive to compute, we can use the second equation to reduce the number of exponentials to one.&lt;br /&gt;
&lt;br /&gt;
The second equation has the drawback that both the numerator and the denominator can become infinity for large values of x which results in NaN. That happens for &amp;lt;math&amp;gt;|x| &amp;gt; \log(FLT\_MAX)/2 \approx 44&amp;lt;/math&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
The third equation solves this problem but has a slightly higher error for &amp;lt;math&amp;gt;x \approx 2^{-25}&amp;lt;/math&amp;gt;. In the following graph you can see the error chart for equation 3. The error charts for the other two equations are very similar. All three equations have an error of up to 16 mio ULPs for &amp;lt;math&amp;gt;x &amp;lt; 2^{-5}&amp;lt;/math&amp;gt; which means that up to 24 bits could be wrong.&lt;br /&gt;
&lt;br /&gt;
=== exp_v2 ===&lt;br /&gt;
&lt;br /&gt;
The following error graph shows the ULP error of the &amp;lt;code&amp;gt;exp_v2&amp;lt;/code&amp;gt; formulation across the full &amp;lt;code&amp;gt;float32&amp;lt;/code&amp;gt; input range. &lt;br /&gt;
This formulation reduces the number of exponential evaluations to one, but is particularly sensitive to catastrophic cancellation for very small input values and to overflow for large &amp;lt;math&amp;gt;|x|&amp;lt;/math&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
{{#widget:FloatErrorGraph|id=tanhf_exp_v2&lt;br /&gt;
  |data=[ [&amp;quot;-2^127&amp;quot;,0,1.17549e-38],[&amp;quot;-2^126&amp;quot;,0,1.17549e-38],[&amp;quot;-2^125&amp;quot;,0,1.17549e-38],[&amp;quot;-2^124&amp;quot;,0,1.17549e-38],[&amp;quot;-2^123&amp;quot;,0,1.17549e-38],[&amp;quot;-2^122&amp;quot;,0,1.17549e-38],[&amp;quot;-2^121&amp;quot;,0,1.17549e-38],[&amp;quot;-2^120&amp;quot;,0,1.17549e-38],[&amp;quot;-2^119&amp;quot;,0,1.17549e-38],[&amp;quot;-2^118&amp;quot;,0,1.17549e-38],[&amp;quot;-2^117&amp;quot;,0,1.17549e-38],[&amp;quot;-2^116&amp;quot;,0,1.17549e-38],[&amp;quot;-2^115&amp;quot;,0,1.17549e-38],[&amp;quot;-2^114&amp;quot;,0,1.17549e-38],[&amp;quot;-2^113&amp;quot;,0,1.17549e-38],[&amp;quot;-2^112&amp;quot;,0,1.17549e-38],[&amp;quot;-2^111&amp;quot;,0,1.17549e-38],[&amp;quot;-2^110&amp;quot;,0,1.17549e-38],[&amp;quot;-2^109&amp;quot;,0,1.17549e-38],[&amp;quot;-2^108&amp;quot;,0,1.17549e-38],[&amp;quot;-2^107&amp;quot;,0,1.17549e-38],[&amp;quot;-2^106&amp;quot;,0,1.17549e-38],[&amp;quot;-2^105&amp;quot;,0,1.17549e-38],[&amp;quot;-2^104&amp;quot;,0,1.17549e-38],[&amp;quot;-2^103&amp;quot;,0,1.17549e-38],[&amp;quot;-2^102&amp;quot;,0,1.17549e-38],[&amp;quot;-2^101&amp;quot;,0,1.17549e-38],[&amp;quot;-2^100&amp;quot;,0,1.17549e-38],[&amp;quot;-2^99&amp;quot;,0,1.17549e-38],[&amp;quot;-2^98&amp;quot;,0,1.17549e-38],[&amp;quot;-2^97&amp;quot;,0,1.17549e-38],[&amp;quot;-2^96&amp;quot;,0,1.17549e-38],[&amp;quot;-2^95&amp;quot;,0,1.17549e-38],[&amp;quot;-2^94&amp;quot;,0,1.17549e-38],[&amp;quot;-2^93&amp;quot;,0,1.17549e-38],[&amp;quot;-2^92&amp;quot;,0,1.17549e-38],[&amp;quot;-2^91&amp;quot;,0,1.17549e-38],[&amp;quot;-2^90&amp;quot;,0,1.17549e-38],[&amp;quot;-2^89&amp;quot;,0,1.17549e-38],[&amp;quot;-2^88&amp;quot;,0,1.17549e-38],[&amp;quot;-2^87&amp;quot;,0,1.17549e-38],[&amp;quot;-2^86&amp;quot;,0,1.17549e-38],[&amp;quot;-2^85&amp;quot;,0,1.17549e-38],[&amp;quot;-2^84&amp;quot;,0,1.17549e-38],[&amp;quot;-2^83&amp;quot;,0,1.17549e-38],[&amp;quot;-2^82&amp;quot;,0,1.17549e-38],[&amp;quot;-2^81&amp;quot;,0,1.17549e-38],[&amp;quot;-2^80&amp;quot;,0,1.17549e-38],[&amp;quot;-2^79&amp;quot;,0,1.17549e-38],[&amp;quot;-2^78&amp;quot;,0,1.17549e-38],[&amp;quot;-2^77&amp;quot;,0,1.17549e-38],[&amp;quot;-2^76&amp;quot;,0,1.17549e-38],[&amp;quot;-2^75&amp;quot;,0,1.17549e-38],[&amp;quot;-2^74&amp;quot;,0,1.17549e-38],[&amp;quot;-2^73&amp;quot;,0,1.17549e-38],[&amp;quot;-2^72&amp;quot;,0,1.17549e-38],[&amp;quot;-2^71&amp;quot;,0,1.17549e-38],[&amp;quot;-2^70&amp;quot;,0,1.17549e-38],[&amp;quot;-2^69&amp;quot;,0,1.17549e-38],[&amp;quot;-2^68&amp;quot;,0,1.17549e-38],[&amp;quot;-2^67&amp;quot;,0,1.17549e-38],[&amp;quot;-2^66&amp;quot;,0,1.17549e-38],[&amp;quot;-2^65&amp;quot;,0,1.17549e-38],[&amp;quot;-2^64&amp;quot;,0,1.17549e-38],[&amp;quot;-2^63&amp;quot;,0,1.17549e-38],[&amp;quot;-2^62&amp;quot;,0,1.17549e-38],[&amp;quot;-2^61&amp;quot;,0,1.17549e-38],[&amp;quot;-2^60&amp;quot;,0,1.17549e-38],[&amp;quot;-2^59&amp;quot;,0,1.17549e-38],[&amp;quot;-2^58&amp;quot;,0,1.17549e-38],[&amp;quot;-2^57&amp;quot;,0,1.17549e-38],[&amp;quot;-2^56&amp;quot;,0,1.17549e-38],[&amp;quot;-2^55&amp;quot;,0,1.17549e-38],[&amp;quot;-2^54&amp;quot;,0,1.17549e-38],[&amp;quot;-2^53&amp;quot;,0,1.17549e-38],[&amp;quot;-2^52&amp;quot;,0,1.17549e-38],[&amp;quot;-2^51&amp;quot;,0,1.17549e-38],[&amp;quot;-2^50&amp;quot;,0,1.17549e-38],[&amp;quot;-2^49&amp;quot;,0,1.17549e-38],[&amp;quot;-2^48&amp;quot;,0,1.17549e-38],[&amp;quot;-2^47&amp;quot;,0,1.17549e-38],[&amp;quot;-2^46&amp;quot;,0,1.17549e-38],[&amp;quot;-2^45&amp;quot;,0,1.17549e-38],[&amp;quot;-2^44&amp;quot;,0,1.17549e-38],[&amp;quot;-2^43&amp;quot;,0,1.17549e-38],[&amp;quot;-2^42&amp;quot;,0,1.17549e-38],[&amp;quot;-2^41&amp;quot;,0,1.17549e-38],[&amp;quot;-2^40&amp;quot;,0,1.17549e-38],[&amp;quot;-2^39&amp;quot;,0,1.17549e-38],[&amp;quot;-2^38&amp;quot;,0,1.17549e-38],[&amp;quot;-2^37&amp;quot;,0,1.17549e-38],[&amp;quot;-2^36&amp;quot;,0,1.17549e-38],[&amp;quot;-2^35&amp;quot;,0,1.17549e-38],[&amp;quot;-2^34&amp;quot;,0,1.17549e-38],[&amp;quot;-2^33&amp;quot;,0,1.17549e-38],[&amp;quot;-2^32&amp;quot;,0,1.17549e-38],[&amp;quot;-2^31&amp;quot;,0,1.17549e-38],[&amp;quot;-2^30&amp;quot;,0,1.17549e-38],[&amp;quot;-2^29&amp;quot;,0,1.17549e-38],[&amp;quot;-2^28&amp;quot;,0,1.17549e-38],[&amp;quot;-2^27&amp;quot;,0,1.17549e-38],[&amp;quot;-2^26&amp;quot;,0,1.17549e-38],[&amp;quot;-2^25&amp;quot;,0,1.17549e-38],[&amp;quot;-2^24&amp;quot;,0,1.17549e-38],[&amp;quot;-2^23&amp;quot;,0,1.17549e-38],[&amp;quot;-2^22&amp;quot;,0,1.17549e-38],[&amp;quot;-2^21&amp;quot;,0,1.17549e-38],[&amp;quot;-2^20&amp;quot;,0,1.17549e-38],[&amp;quot;-2^19&amp;quot;,0,1.17549e-38],[&amp;quot;-2^18&amp;quot;,0,1.17549e-38],[&amp;quot;-2^17&amp;quot;,0,1.17549e-38],[&amp;quot;-2^16&amp;quot;,0,1.17549e-38],[&amp;quot;-2^15&amp;quot;,0,1.17549e-38],[&amp;quot;-2^14&amp;quot;,0,1.17549e-38],[&amp;quot;-2^13&amp;quot;,0,1.17549e-38],[&amp;quot;-2^12&amp;quot;,0,1.17549e-38],[&amp;quot;-2^11&amp;quot;,0,1.17549e-38],[&amp;quot;-2^10&amp;quot;,0,1.17549e-38],[&amp;quot;-2^9&amp;quot;,0,1.17549e-38],[&amp;quot;-2^8&amp;quot;,0,1.17549e-38],[&amp;quot;-2^7&amp;quot;,0,1.17549e-38],[&amp;quot;-2^6&amp;quot;,0,1.17549e-38],[&amp;quot;-2^5&amp;quot;,0,1.17549e-38],[&amp;quot;-2^4&amp;quot;,2.53131e-14,4.24683e-07],[&amp;quot;-2^3&amp;quot;,5.95888e-08,0.999734],[&amp;quot;-2^2&amp;quot;,8.86696e-08,1.48763],[&amp;quot;-2^1&amp;quot;,8.91609e-08,1.49587],[&amp;quot;-2^0&amp;quot;,8.73743e-08,1.4659],[&amp;quot;-2^-1&amp;quot;,8.65382e-08,2.13222],[&amp;quot;-2^-2&amp;quot;,6.18481e-08,2.61428],[&amp;quot;-2^-3&amp;quot;,3.88071e-08,3.08594],[&amp;quot;-2^-4&amp;quot;,2.6234e-08,4.23594],[&amp;quot;-2^-5&amp;quot;,2.02799e-08,8.19726],[&amp;quot;-2^-6&amp;quot;,1.75183e-08,12.4495],[&amp;quot;-2^-7&amp;quot;,1.61937e-08,17.3878],[&amp;quot;-2^-8&amp;quot;,1.5566e-08,33.4278],[&amp;quot;-2^-9&amp;quot;,1.51688e-08,65.1495],[&amp;quot;-2^-10&amp;quot;,1.50281e-08,129.09],[&amp;quot;-2^-11&amp;quot;,1.49613e-08,257.033],[&amp;quot;-2^-12&amp;quot;,1.49296e-08,512.977],[&amp;quot;-2^-13&amp;quot;,1.49174e-08,1025.12],[&amp;quot;-2^-14&amp;quot;,1.4909e-08,2049.08],[&amp;quot;-2^-15&amp;quot;,1.49049e-08,4097.02],[&amp;quot;-2^-16&amp;quot;,1.4903e-08,8193],[&amp;quot;-2^-17&amp;quot;,1.49021e-08,16385],[&amp;quot;-2^-18&amp;quot;,1.48862e-08,32735],[&amp;quot;-2^-19&amp;quot;,1.48973e-08,65519],[&amp;quot;-2^-20&amp;quot;,1.49001e-08,131063],[&amp;quot;-2^-21&amp;quot;,1.49009e-08,262139],[&amp;quot;-2^-22&amp;quot;,1.49011e-08,524285],[&amp;quot;-2^-23&amp;quot;,1.49011e-08,1.04857e+06],[&amp;quot;-2^-24&amp;quot;,1.49011e-08,2.09715e+06],[&amp;quot;-2^-25&amp;quot;,1.49012e-08,4.1943e+06],[&amp;quot;-2^-26&amp;quot;,1.49007e-08,8.38835e+06],[&amp;quot;-2^-27&amp;quot;,1.49012e-08,8.47249e+08],[&amp;quot;-2^-28&amp;quot;,7.45058e-09,8.38861e+08],[&amp;quot;-2^-29&amp;quot;,3.72529e-09,8.30472e+08],[&amp;quot;-2^-30&amp;quot;,1.86265e-09,8.22084e+08],[&amp;quot;-2^-31&amp;quot;,9.31323e-10,8.13695e+08],[&amp;quot;-2^-32&amp;quot;,4.65661e-10,8.05306e+08],[&amp;quot;-2^-33&amp;quot;,2.32831e-10,7.96918e+08],[&amp;quot;-2^-34&amp;quot;,1.16415e-10,7.88529e+08],[&amp;quot;-2^-35&amp;quot;,5.82077e-11,7.80141e+08],[&amp;quot;-2^-36&amp;quot;,2.91038e-11,7.71752e+08],[&amp;quot;-2^-37&amp;quot;,1.45519e-11,7.63363e+08],[&amp;quot;-2^-38&amp;quot;,7.27596e-12,7.54975e+08],[&amp;quot;-2^-39&amp;quot;,3.63798e-12,7.46586e+08],[&amp;quot;-2^-40&amp;quot;,1.81899e-12,7.38198e+08],[&amp;quot;-2^-41&amp;quot;,9.09495e-13,7.29809e+08],[&amp;quot;-2^-42&amp;quot;,4.54747e-13,7.2142e+08],[&amp;quot;-2^-43&amp;quot;,2.27374e-13,7.13032e+08],[&amp;quot;-2^-44&amp;quot;,1.13687e-13,7.04643e+08],[&amp;quot;-2^-45&amp;quot;,5.68434e-14,6.96254e+08],[&amp;quot;-2^-46&amp;quot;,2.84217e-14,6.87866e+08],[&amp;quot;-2^-47&amp;quot;,1.42109e-14,6.79477e+08],[&amp;quot;-2^-48&amp;quot;,7.10543e-15,6.71089e+08],[&amp;quot;-2^-49&amp;quot;,3.55271e-15,6.627e+08],[&amp;quot;-2^-50&amp;quot;,1.77636e-15,6.54311e+08],[&amp;quot;-2^-51&amp;quot;,8.88178e-16,6.45923e+08],[&amp;quot;-2^-52&amp;quot;,4.44089e-16,6.37534e+08],[&amp;quot;-2^-53&amp;quot;,2.22045e-16,6.29146e+08],[&amp;quot;-2^-54&amp;quot;,1.11022e-16,6.20757e+08],[&amp;quot;-2^-55&amp;quot;,5.55111e-17,6.12368e+08],[&amp;quot;-2^-56&amp;quot;,2.77556e-17,6.0398e+08],[&amp;quot;-2^-57&amp;quot;,1.38778e-17,5.95591e+08],[&amp;quot;-2^-58&amp;quot;,6.93889e-18,5.87203e+08],[&amp;quot;-2^-59&amp;quot;,3.46945e-18,5.78814e+08],[&amp;quot;-2^-60&amp;quot;,1.73472e-18,5.70425e+08],[&amp;quot;-2^-61&amp;quot;,8.67362e-19,5.62037e+08],[&amp;quot;-2^-62&amp;quot;,4.33681e-19,5.53648e+08],[&amp;quot;-2^-63&amp;quot;,2.1684e-19,5.4526e+08],[&amp;quot;-2^-64&amp;quot;,1.0842e-19,5.36871e+08],[&amp;quot;-2^-65&amp;quot;,5.42101e-20,5.28482e+08],[&amp;quot;-2^-66&amp;quot;,2.71051e-20,5.20094e+08],[&amp;quot;-2^-67&amp;quot;,1.35525e-20,5.11705e+08],[&amp;quot;-2^-68&amp;quot;,6.77626e-21,5.03316e+08],[&amp;quot;-2^-69&amp;quot;,3.38813e-21,4.94928e+08],[&amp;quot;-2^-70&amp;quot;,1.69407e-21,4.86539e+08],[&amp;quot;-2^-71&amp;quot;,8.47033e-22,4.78151e+08],[&amp;quot;-2^-72&amp;quot;,4.23516e-22,4.69762e+08],[&amp;quot;-2^-73&amp;quot;,2.11758e-22,4.61373e+08],[&amp;quot;-2^-74&amp;quot;,1.05879e-22,4.52985e+08],[&amp;quot;-2^-75&amp;quot;,5.29396e-23,4.44596e+08],[&amp;quot;-2^-76&amp;quot;,2.64698e-23,4.36208e+08],[&amp;quot;-2^-77&amp;quot;,1.32349e-23,4.27819e+08],[&amp;quot;-2^-78&amp;quot;,6.61744e-24,4.1943e+08],[&amp;quot;-2^-79&amp;quot;,3.30872e-24,4.11042e+08],[&amp;quot;-2^-80&amp;quot;,1.65436e-24,4.02653e+08],[&amp;quot;-2^-81&amp;quot;,8.27181e-25,3.94265e+08],[&amp;quot;-2^-82&amp;quot;,4.1359e-25,3.85876e+08],[&amp;quot;-2^-83&amp;quot;,2.06795e-25,3.77487e+08],[&amp;quot;-2^-84&amp;quot;,1.03398e-25,3.69099e+08],[&amp;quot;-2^-85&amp;quot;,5.16988e-26,3.6071e+08],[&amp;quot;-2^-86&amp;quot;,2.58494e-26,3.52322e+08],[&amp;quot;-2^-87&amp;quot;,1.29247e-26,3.43933e+08],[&amp;quot;-2^-88&amp;quot;,6.46235e-27,3.35544e+08],[&amp;quot;-2^-89&amp;quot;,3.23117e-27,3.27156e+08],[&amp;quot;-2^-90&amp;quot;,1.61559e-27,3.18767e+08],[&amp;quot;-2^-91&amp;quot;,8.07794e-28,3.10378e+08],[&amp;quot;-2^-92&amp;quot;,4.03897e-28,3.0199e+08],[&amp;quot;-2^-93&amp;quot;,2.01948e-28,2.93601e+08],[&amp;quot;-2^-94&amp;quot;,1.00974e-28,2.85213e+08],[&amp;quot;-2^-95&amp;quot;,5.04871e-29,2.76824e+08],[&amp;quot;-2^-96&amp;quot;,2.52435e-29,2.68435e+08],[&amp;quot;-2^-97&amp;quot;,1.26218e-29,2.60047e+08],[&amp;quot;-2^-98&amp;quot;,6.31089e-30,2.51658e+08],[&amp;quot;-2^-99&amp;quot;,3.15544e-30,2.4327e+08],[&amp;quot;-2^-100&amp;quot;,1.57772e-30,2.34881e+08],[&amp;quot;-2^-101&amp;quot;,7.88861e-31,2.26492e+08],[&amp;quot;-2^-102&amp;quot;,3.9443e-31,2.18104e+08],[&amp;quot;-2^-103&amp;quot;,1.97215e-31,2.09715e+08],[&amp;quot;-2^-104&amp;quot;,9.86076e-32,2.01327e+08],[&amp;quot;-2^-105&amp;quot;,4.93038e-32,1.92938e+08],[&amp;quot;-2^-106&amp;quot;,2.46519e-32,1.84549e+08],[&amp;quot;-2^-107&amp;quot;,1.2326e-32,1.76161e+08],[&amp;quot;-2^-108&amp;quot;,6.16298e-33,1.67772e+08],[&amp;quot;-2^-109&amp;quot;,3.08149e-33,1.59384e+08],[&amp;quot;-2^-110&amp;quot;,1.54074e-33,1.50995e+08],[&amp;quot;-2^-111&amp;quot;,7.70372e-34,1.42606e+08],[&amp;quot;-2^-112&amp;quot;,3.85186e-34,1.34218e+08],[&amp;quot;-2^-113&amp;quot;,1.92593e-34,1.25829e+08],[&amp;quot;-2^-114&amp;quot;,9.62965e-35,1.17441e+08],[&amp;quot;-2^-115&amp;quot;,4.81482e-35,1.09052e+08],[&amp;quot;-2^-116&amp;quot;,2.40741e-35,1.00663e+08],[&amp;quot;-2^-117&amp;quot;,1.20371e-35,9.22747e+07],[&amp;quot;-2^-118&amp;quot;,6.01853e-36,8.38861e+07],[&amp;quot;-2^-119&amp;quot;,3.00927e-36,7.54975e+07],[&amp;quot;-2^-120&amp;quot;,1.50463e-36,6.71089e+07],[&amp;quot;-2^-121&amp;quot;,7.52316e-37,5.87203e+07],[&amp;quot;-2^-122&amp;quot;,3.76158e-37,5.03316e+07],[&amp;quot;-2^-123&amp;quot;,1.88079e-37,4.1943e+07],[&amp;quot;-2^-124&amp;quot;,9.40395e-38,3.35544e+07],[&amp;quot;-2^-125&amp;quot;,4.70198e-38,2.51658e+07],[&amp;quot;-2^-126&amp;quot;,2.35099e-38,1.67772e+07],[&amp;quot;-SN&amp;quot;,1.17546e-38,8.38835e+06],[&amp;quot;0&amp;quot;,0,1.17549e-38],[&amp;quot;+SN&amp;quot;,1.17546e-38,8.38835e+06],[&amp;quot;+2^-126&amp;quot;,2.35095e-38,1.6777e+07],[&amp;quot;+2^-125&amp;quot;,4.70191e-38,2.51656e+07],[&amp;quot;+2^-124&amp;quot;,9.40381e-38,3.35542e+07],[&amp;quot;+2^-123&amp;quot;,1.88076e-37,4.19428e+07],[&amp;quot;+2^-122&amp;quot;,3.76152e-37,5.03314e+07],[&amp;quot;+2^-121&amp;quot;,7.52305e-37,5.872e+07],[&amp;quot;+2^-120&amp;quot;,1.50461e-36,6.71086e+07],[&amp;quot;+2^-119&amp;quot;,3.00922e-36,7.54972e+07],[&amp;quot;+2^-118&amp;quot;,6.01844e-36,8.38858e+07],[&amp;quot;+2^-117&amp;quot;,1.20369e-35,9.22744e+07],[&amp;quot;+2^-116&amp;quot;,2.40738e-35,1.00663e+08],[&amp;quot;+2^-115&amp;quot;,4.81475e-35,1.09052e+08],[&amp;quot;+2^-114&amp;quot;,9.6295e-35,1.1744e+08],[&amp;quot;+2^-113&amp;quot;,1.9259e-34,1.25829e+08],[&amp;quot;+2^-112&amp;quot;,3.8518e-34,1.34217e+08],[&amp;quot;+2^-111&amp;quot;,7.7036e-34,1.42606e+08],[&amp;quot;+2^-110&amp;quot;,1.54072e-33,1.50995e+08],[&amp;quot;+2^-109&amp;quot;,3.08144e-33,1.59383e+08],[&amp;quot;+2^-108&amp;quot;,6.16288e-33,1.67772e+08],[&amp;quot;+2^-107&amp;quot;,1.23258e-32,1.76161e+08],[&amp;quot;+2^-106&amp;quot;,2.46515e-32,1.84549e+08],[&amp;quot;+2^-105&amp;quot;,4.93031e-32,1.92938e+08],[&amp;quot;+2^-104&amp;quot;,9.86061e-32,2.01326e+08],[&amp;quot;+2^-103&amp;quot;,1.97212e-31,2.09715e+08],[&amp;quot;+2^-102&amp;quot;,3.94424e-31,2.18104e+08],[&amp;quot;+2^-101&amp;quot;,7.88849e-31,2.26492e+08],[&amp;quot;+2^-100&amp;quot;,1.5777e-30,2.34881e+08],[&amp;quot;+2^-99&amp;quot;,3.1554e-30,2.43269e+08],[&amp;quot;+2^-98&amp;quot;,6.31079e-30,2.51658e+08],[&amp;quot;+2^-97&amp;quot;,1.26216e-29,2.60047e+08],[&amp;quot;+2^-96&amp;quot;,2.52432e-29,2.68435e+08],[&amp;quot;+2^-95&amp;quot;,5.04863e-29,2.76824e+08],[&amp;quot;+2^-94&amp;quot;,1.00973e-28,2.85212e+08],[&amp;quot;+2^-93&amp;quot;,2.01945e-28,2.93601e+08],[&amp;quot;+2^-92&amp;quot;,4.03891e-28,3.0199e+08],[&amp;quot;+2^-91&amp;quot;,8.07781e-28,3.10378e+08],[&amp;quot;+2^-90&amp;quot;,1.61556e-27,3.18767e+08],[&amp;quot;+2^-89&amp;quot;,3.23112e-27,3.27155e+08],[&amp;quot;+2^-88&amp;quot;,6.46225e-27,3.35544e+08],[&amp;quot;+2^-87&amp;quot;,1.29245e-26,3.43933e+08],[&amp;quot;+2^-86&amp;quot;,2.5849e-26,3.52321e+08],[&amp;quot;+2^-85&amp;quot;,5.1698e-26,3.6071e+08],[&amp;quot;+2^-84&amp;quot;,1.03396e-25,3.69098e+08],[&amp;quot;+2^-83&amp;quot;,2.06792e-25,3.77487e+08],[&amp;quot;+2^-82&amp;quot;,4.13584e-25,3.85876e+08],[&amp;quot;+2^-81&amp;quot;,8.27168e-25,3.94264e+08],[&amp;quot;+2^-80&amp;quot;,1.65434e-24,4.02653e+08],[&amp;quot;+2^-79&amp;quot;,3.30867e-24,4.11042e+08],[&amp;quot;+2^-78&amp;quot;,6.61734e-24,4.1943e+08],[&amp;quot;+2^-77&amp;quot;,1.32347e-23,4.27819e+08],[&amp;quot;+2^-76&amp;quot;,2.64694e-23,4.36207e+08],[&amp;quot;+2^-75&amp;quot;,5.29388e-23,4.44596e+08],[&amp;quot;+2^-74&amp;quot;,1.05878e-22,4.52985e+08],[&amp;quot;+2^-73&amp;quot;,2.11755e-22,4.61373e+08],[&amp;quot;+2^-72&amp;quot;,4.2351e-22,4.69762e+08],[&amp;quot;+2^-71&amp;quot;,8.4702e-22,4.7815e+08],[&amp;quot;+2^-70&amp;quot;,1.69404e-21,4.86539e+08],[&amp;quot;+2^-69&amp;quot;,3.38808e-21,4.94928e+08],[&amp;quot;+2^-68&amp;quot;,6.77616e-21,5.03316e+08],[&amp;quot;+2^-67&amp;quot;,1.35523e-20,5.11705e+08],[&amp;quot;+2^-66&amp;quot;,2.71046e-20,5.20093e+08],[&amp;quot;+2^-65&amp;quot;,5.42093e-20,5.28482e+08],[&amp;quot;+2^-64&amp;quot;,1.08419e-19,5.36871e+08],[&amp;quot;+2^-63&amp;quot;,2.16837e-19,5.45259e+08],[&amp;quot;+2^-62&amp;quot;,4.33674e-19,5.53648e+08],[&amp;quot;+2^-61&amp;quot;,8.67349e-19,5.62036e+08],[&amp;quot;+2^-60&amp;quot;,1.7347e-18,5.70425e+08],[&amp;quot;+2^-59&amp;quot;,3.46939e-18,5.78814e+08],[&amp;quot;+2^-58&amp;quot;,6.93879e-18,5.87202e+08],[&amp;quot;+2^-57&amp;quot;,1.38776e-17,5.95591e+08],[&amp;quot;+2^-56&amp;quot;,2.77552e-17,6.0398e+08],[&amp;quot;+2^-55&amp;quot;,5.55103e-17,6.12368e+08],[&amp;quot;+2^-54&amp;quot;,1.11021e-16,6.20757e+08],[&amp;quot;+2^-53&amp;quot;,2.22041e-16,6.29145e+08],[&amp;quot;+2^-52&amp;quot;,4.44082e-16,6.37534e+08],[&amp;quot;+2^-51&amp;quot;,8.88165e-16,6.45923e+08],[&amp;quot;+2^-50&amp;quot;,1.77633e-15,6.54311e+08],[&amp;quot;+2^-49&amp;quot;,3.55266e-15,6.627e+08],[&amp;quot;+2^-48&amp;quot;,7.10532e-15,6.71088e+08],[&amp;quot;+2^-47&amp;quot;,1.42106e-14,6.79477e+08],[&amp;quot;+2^-46&amp;quot;,2.84213e-14,6.87866e+08],[&amp;quot;+2^-45&amp;quot;,5.68426e-14,6.96254e+08],[&amp;quot;+2^-44&amp;quot;,1.13685e-13,7.04643e+08],[&amp;quot;+2^-43&amp;quot;,2.2737e-13,7.13031e+08],[&amp;quot;+2^-42&amp;quot;,4.5474e-13,7.2142e+08],[&amp;quot;+2^-41&amp;quot;,9.09481e-13,7.29809e+08],[&amp;quot;+2^-40&amp;quot;,1.81896e-12,7.38197e+08],[&amp;quot;+2^-39&amp;quot;,3.63792e-12,7.46586e+08],[&amp;quot;+2^-38&amp;quot;,7.27585e-12,7.54974e+08],[&amp;quot;+2^-37&amp;quot;,1.45517e-11,7.63363e+08],[&amp;quot;+2^-36&amp;quot;,2.91034e-11,7.71752e+08],[&amp;quot;+2^-35&amp;quot;,5.82068e-11,7.8014e+08],[&amp;quot;+2^-34&amp;quot;,1.16414e-10,7.88529e+08],[&amp;quot;+2^-33&amp;quot;,2.32827e-10,7.96918e+08],[&amp;quot;+2^-32&amp;quot;,4.65654e-10,8.05306e+08],[&amp;quot;+2^-31&amp;quot;,9.31308e-10,8.13695e+08],[&amp;quot;+2^-30&amp;quot;,1.86262e-09,8.22083e+08],[&amp;quot;+2^-29&amp;quot;,3.72523e-09,8.30472e+08],[&amp;quot;+2^-28&amp;quot;,7.45047e-09,8.38861e+08],[&amp;quot;+2^-27&amp;quot;,1.49009e-08,8.47249e+08],[&amp;quot;+2^-26&amp;quot;,2.98019e-08,8.55638e+08],[&amp;quot;+2^-25&amp;quot;,2.98023e-08,8.38861e+06],[&amp;quot;+2^-24&amp;quot;,2.98023e-08,4.1943e+06],[&amp;quot;+2^-23&amp;quot;,2.98023e-08,2.09715e+06],[&amp;quot;+2^-22&amp;quot;,2.98023e-08,1.04857e+06],[&amp;quot;+2^-21&amp;quot;,2.98021e-08,524284],[&amp;quot;+2^-20&amp;quot;,2.98014e-08,262136],[&amp;quot;+2^-19&amp;quot;,2.97987e-08,131056],[&amp;quot;+2^-18&amp;quot;,2.97878e-08,65504],[&amp;quot;+2^-17&amp;quot;,2.98014e-08,32767],[&amp;quot;+2^-16&amp;quot;,2.98005e-08,16384],[&amp;quot;+2^-15&amp;quot;,2.98023e-08,8192],[&amp;quot;+2^-14&amp;quot;,2.98025e-08,4351.99],[&amp;quot;+2^-13&amp;quot;,2.98044e-08,2815.96],[&amp;quot;+2^-12&amp;quot;,2.98091e-08,1024.23],[&amp;quot;+2^-11&amp;quot;,2.9812e-08,512.167],[&amp;quot;+2^-10&amp;quot;,2.98154e-08,256.113],[&amp;quot;+2^-9&amp;quot;,2.98591e-08,128.244],[&amp;quot;+2^-8&amp;quot;,3.00204e-08,85.3328],[&amp;quot;+2^-7&amp;quot;,3.01947e-08,43.675],[&amp;quot;+2^-6&amp;quot;,3.05713e-08,30.45],[&amp;quot;+2^-5&amp;quot;,3.14319e-08,8.43743],[&amp;quot;+2^-4&amp;quot;,3.31291e-08,7.77058],[&amp;quot;+2^-3&amp;quot;,3.58597e-08,3.99659],[&amp;quot;+2^-2&amp;quot;,4.14639e-08,2.29246],[&amp;quot;+2^-1&amp;quot;,8.02409e-08,1.34622],[&amp;quot;+2^0&amp;quot;,8.66201e-08,1.45324],[&amp;quot;+2^1&amp;quot;,8.8654e-08,1.48737],[&amp;quot;+2^2&amp;quot;,6.7976e-08,1.14045],[&amp;quot;+2^3&amp;quot;,1.33955e-07,2.2474],[&amp;quot;+2^4&amp;quot;,2.53131e-14,4.24683e-07],[&amp;quot;+2^5&amp;quot;,0,null],[&amp;quot;+2^6&amp;quot;,0,null],[&amp;quot;+2^7&amp;quot;,0,null],[&amp;quot;+2^8&amp;quot;,0,null],[&amp;quot;+2^9&amp;quot;,0,null],[&amp;quot;+2^10&amp;quot;,0,null],[&amp;quot;+2^11&amp;quot;,0,null],[&amp;quot;+2^12&amp;quot;,0,null],[&amp;quot;+2^13&amp;quot;,0,null],[&amp;quot;+2^14&amp;quot;,0,null],[&amp;quot;+2^15&amp;quot;,0,null],[&amp;quot;+2^16&amp;quot;,0,null],[&amp;quot;+2^17&amp;quot;,0,null],[&amp;quot;+2^18&amp;quot;,0,null],[&amp;quot;+2^19&amp;quot;,0,null],[&amp;quot;+2^20&amp;quot;,0,null],[&amp;quot;+2^21&amp;quot;,0,null],[&amp;quot;+2^22&amp;quot;,0,null],[&amp;quot;+2^23&amp;quot;,0,null],[&amp;quot;+2^24&amp;quot;,0,null],[&amp;quot;+2^25&amp;quot;,0,null],[&amp;quot;+2^26&amp;quot;,0,null],[&amp;quot;+2^27&amp;quot;,0,null],[&amp;quot;+2^28&amp;quot;,0,null],[&amp;quot;+2^29&amp;quot;,0,null],[&amp;quot;+2^30&amp;quot;,0,null],[&amp;quot;+2^31&amp;quot;,0,null],[&amp;quot;+2^32&amp;quot;,0,null],[&amp;quot;+2^33&amp;quot;,0,null],[&amp;quot;+2^34&amp;quot;,0,null],[&amp;quot;+2^35&amp;quot;,0,null],[&amp;quot;+2^36&amp;quot;,0,null],[&amp;quot;+2^37&amp;quot;,0,null],[&amp;quot;+2^38&amp;quot;,0,null],[&amp;quot;+2^39&amp;quot;,0,null],[&amp;quot;+2^40&amp;quot;,0,null],[&amp;quot;+2^41&amp;quot;,0,null],[&amp;quot;+2^42&amp;quot;,0,null],[&amp;quot;+2^43&amp;quot;,0,null],[&amp;quot;+2^44&amp;quot;,0,null],[&amp;quot;+2^45&amp;quot;,0,null],[&amp;quot;+2^46&amp;quot;,0,null],[&amp;quot;+2^47&amp;quot;,0,null],[&amp;quot;+2^48&amp;quot;,0,null],[&amp;quot;+2^49&amp;quot;,0,null],[&amp;quot;+2^50&amp;quot;,0,null],[&amp;quot;+2^51&amp;quot;,0,null],[&amp;quot;+2^52&amp;quot;,0,null],[&amp;quot;+2^53&amp;quot;,0,null],[&amp;quot;+2^54&amp;quot;,0,null],[&amp;quot;+2^55&amp;quot;,0,null],[&amp;quot;+2^56&amp;quot;,0,null],[&amp;quot;+2^57&amp;quot;,0,null],[&amp;quot;+2^58&amp;quot;,0,null],[&amp;quot;+2^59&amp;quot;,0,null],[&amp;quot;+2^60&amp;quot;,0,null],[&amp;quot;+2^61&amp;quot;,0,null],[&amp;quot;+2^62&amp;quot;,0,null],[&amp;quot;+2^63&amp;quot;,0,null],[&amp;quot;+2^64&amp;quot;,0,null],[&amp;quot;+2^65&amp;quot;,0,null],[&amp;quot;+2^66&amp;quot;,0,null],[&amp;quot;+2^67&amp;quot;,0,null],[&amp;quot;+2^68&amp;quot;,0,null],[&amp;quot;+2^69&amp;quot;,0,null],[&amp;quot;+2^70&amp;quot;,0,null],[&amp;quot;+2^71&amp;quot;,0,null],[&amp;quot;+2^72&amp;quot;,0,null],[&amp;quot;+2^73&amp;quot;,0,null],[&amp;quot;+2^74&amp;quot;,0,null],[&amp;quot;+2^75&amp;quot;,0,null],[&amp;quot;+2^76&amp;quot;,0,null],[&amp;quot;+2^77&amp;quot;,0,null],[&amp;quot;+2^78&amp;quot;,0,null],[&amp;quot;+2^79&amp;quot;,0,null],[&amp;quot;+2^80&amp;quot;,0,null],[&amp;quot;+2^81&amp;quot;,0,null],[&amp;quot;+2^82&amp;quot;,0,null],[&amp;quot;+2^83&amp;quot;,0,null],[&amp;quot;+2^84&amp;quot;,0,null],[&amp;quot;+2^85&amp;quot;,0,null],[&amp;quot;+2^86&amp;quot;,0,null],[&amp;quot;+2^87&amp;quot;,0,null],[&amp;quot;+2^88&amp;quot;,0,null],[&amp;quot;+2^89&amp;quot;,0,null],[&amp;quot;+2^90&amp;quot;,0,null],[&amp;quot;+2^91&amp;quot;,0,null],[&amp;quot;+2^92&amp;quot;,0,null],[&amp;quot;+2^93&amp;quot;,0,null],[&amp;quot;+2^94&amp;quot;,0,null],[&amp;quot;+2^95&amp;quot;,0,null],[&amp;quot;+2^96&amp;quot;,0,null],[&amp;quot;+2^97&amp;quot;,0,null],[&amp;quot;+2^98&amp;quot;,0,null],[&amp;quot;+2^99&amp;quot;,0,null],[&amp;quot;+2^100&amp;quot;,0,null],[&amp;quot;+2^101&amp;quot;,0,null],[&amp;quot;+2^102&amp;quot;,0,null],[&amp;quot;+2^103&amp;quot;,0,null],[&amp;quot;+2^104&amp;quot;,0,null],[&amp;quot;+2^105&amp;quot;,0,null],[&amp;quot;+2^106&amp;quot;,0,null],[&amp;quot;+2^107&amp;quot;,0,null],[&amp;quot;+2^108&amp;quot;,0,null],[&amp;quot;+2^109&amp;quot;,0,null],[&amp;quot;+2^110&amp;quot;,0,null],[&amp;quot;+2^111&amp;quot;,0,null],[&amp;quot;+2^112&amp;quot;,0,null],[&amp;quot;+2^113&amp;quot;,0,null],[&amp;quot;+2^114&amp;quot;,0,null],[&amp;quot;+2^115&amp;quot;,0,null],[&amp;quot;+2^116&amp;quot;,0,null],[&amp;quot;+2^117&amp;quot;,0,null],[&amp;quot;+2^118&amp;quot;,0,null],[&amp;quot;+2^119&amp;quot;,0,null],[&amp;quot;+2^120&amp;quot;,0,null],[&amp;quot;+2^121&amp;quot;,0,null],[&amp;quot;+2^122&amp;quot;,0,null],[&amp;quot;+2^123&amp;quot;,0,null],[&amp;quot;+2^124&amp;quot;,0,null],[&amp;quot;+2^125&amp;quot;,0,null],[&amp;quot;+2^126&amp;quot;,0,null],[&amp;quot;+2^127&amp;quot;,0,null] ]&lt;br /&gt;
 }}&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Key observation:&#039;&#039;&#039;  &lt;br /&gt;
For &amp;lt;math&amp;gt;|x| \ll 1&amp;lt;/math&amp;gt;, the ULP error grows rapidly and reaches extremely large values due to loss of significance. &lt;br /&gt;
For sufficiently large &amp;lt;math&amp;gt;x&amp;lt;/math&amp;gt;, both numerator and denominator overflow, leading to NaN results.&lt;br /&gt;
&lt;br /&gt;
=== expm1 ===&lt;br /&gt;
&lt;br /&gt;
This graph visualizes the ULP error of the &amp;lt;code&amp;gt;expm1&amp;lt;/code&amp;gt;-based formulation.&lt;br /&gt;
Using &amp;lt;code&amp;gt;expm1&amp;lt;/code&amp;gt; significantly improves numerical accuracy for small input values by avoiding catastrophic cancellation in the expression &amp;lt;math&amp;gt;e^{2x} - 1&amp;lt;/math&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
{{#widget:FloatErrorGraph|id=tanhf_expm1&lt;br /&gt;
  |data=[ [&amp;quot;-2^127&amp;quot;,0,1.17549e-38],[&amp;quot;-2^126&amp;quot;,0,1.17549e-38],[&amp;quot;-2^125&amp;quot;,0,1.17549e-38],[&amp;quot;-2^124&amp;quot;,0,1.17549e-38],[&amp;quot;-2^123&amp;quot;,0,1.17549e-38],[&amp;quot;-2^122&amp;quot;,0,1.17549e-38],[&amp;quot;-2^121&amp;quot;,0,1.17549e-38],[&amp;quot;-2^120&amp;quot;,0,1.17549e-38],[&amp;quot;-2^119&amp;quot;,0,1.17549e-38],[&amp;quot;-2^118&amp;quot;,0,1.17549e-38],[&amp;quot;-2^117&amp;quot;,0,1.17549e-38],[&amp;quot;-2^116&amp;quot;,0,1.17549e-38],[&amp;quot;-2^115&amp;quot;,0,1.17549e-38],[&amp;quot;-2^114&amp;quot;,0,1.17549e-38],[&amp;quot;-2^113&amp;quot;,0,1.17549e-38],[&amp;quot;-2^112&amp;quot;,0,1.17549e-38],[&amp;quot;-2^111&amp;quot;,0,1.17549e-38],[&amp;quot;-2^110&amp;quot;,0,1.17549e-38],[&amp;quot;-2^109&amp;quot;,0,1.17549e-38],[&amp;quot;-2^108&amp;quot;,0,1.17549e-38],[&amp;quot;-2^107&amp;quot;,0,1.17549e-38],[&amp;quot;-2^106&amp;quot;,0,1.17549e-38],[&amp;quot;-2^105&amp;quot;,0,1.17549e-38],[&amp;quot;-2^104&amp;quot;,0,1.17549e-38],[&amp;quot;-2^103&amp;quot;,0,1.17549e-38],[&amp;quot;-2^102&amp;quot;,0,1.17549e-38],[&amp;quot;-2^101&amp;quot;,0,1.17549e-38],[&amp;quot;-2^100&amp;quot;,0,1.17549e-38],[&amp;quot;-2^99&amp;quot;,0,1.17549e-38],[&amp;quot;-2^98&amp;quot;,0,1.17549e-38],[&amp;quot;-2^97&amp;quot;,0,1.17549e-38],[&amp;quot;-2^96&amp;quot;,0,1.17549e-38],[&amp;quot;-2^95&amp;quot;,0,1.17549e-38],[&amp;quot;-2^94&amp;quot;,0,1.17549e-38],[&amp;quot;-2^93&amp;quot;,0,1.17549e-38],[&amp;quot;-2^92&amp;quot;,0,1.17549e-38],[&amp;quot;-2^91&amp;quot;,0,1.17549e-38],[&amp;quot;-2^90&amp;quot;,0,1.17549e-38],[&amp;quot;-2^89&amp;quot;,0,1.17549e-38],[&amp;quot;-2^88&amp;quot;,0,1.17549e-38],[&amp;quot;-2^87&amp;quot;,0,1.17549e-38],[&amp;quot;-2^86&amp;quot;,0,1.17549e-38],[&amp;quot;-2^85&amp;quot;,0,1.17549e-38],[&amp;quot;-2^84&amp;quot;,0,1.17549e-38],[&amp;quot;-2^83&amp;quot;,0,1.17549e-38],[&amp;quot;-2^82&amp;quot;,0,1.17549e-38],[&amp;quot;-2^81&amp;quot;,0,1.17549e-38],[&amp;quot;-2^80&amp;quot;,0,1.17549e-38],[&amp;quot;-2^79&amp;quot;,0,1.17549e-38],[&amp;quot;-2^78&amp;quot;,0,1.17549e-38],[&amp;quot;-2^77&amp;quot;,0,1.17549e-38],[&amp;quot;-2^76&amp;quot;,0,1.17549e-38],[&amp;quot;-2^75&amp;quot;,0,1.17549e-38],[&amp;quot;-2^74&amp;quot;,0,1.17549e-38],[&amp;quot;-2^73&amp;quot;,0,1.17549e-38],[&amp;quot;-2^72&amp;quot;,0,1.17549e-38],[&amp;quot;-2^71&amp;quot;,0,1.17549e-38],[&amp;quot;-2^70&amp;quot;,0,1.17549e-38],[&amp;quot;-2^69&amp;quot;,0,1.17549e-38],[&amp;quot;-2^68&amp;quot;,0,1.17549e-38],[&amp;quot;-2^67&amp;quot;,0,1.17549e-38],[&amp;quot;-2^66&amp;quot;,0,1.17549e-38],[&amp;quot;-2^65&amp;quot;,0,1.17549e-38],[&amp;quot;-2^64&amp;quot;,0,1.17549e-38],[&amp;quot;-2^63&amp;quot;,0,1.17549e-38],[&amp;quot;-2^62&amp;quot;,0,1.17549e-38],[&amp;quot;-2^61&amp;quot;,0,1.17549e-38],[&amp;quot;-2^60&amp;quot;,0,1.17549e-38],[&amp;quot;-2^59&amp;quot;,0,1.17549e-38],[&amp;quot;-2^58&amp;quot;,0,1.17549e-38],[&amp;quot;-2^57&amp;quot;,0,1.17549e-38],[&amp;quot;-2^56&amp;quot;,0,1.17549e-38],[&amp;quot;-2^55&amp;quot;,0,1.17549e-38],[&amp;quot;-2^54&amp;quot;,0,1.17549e-38],[&amp;quot;-2^53&amp;quot;,0,1.17549e-38],[&amp;quot;-2^52&amp;quot;,0,1.17549e-38],[&amp;quot;-2^51&amp;quot;,0,1.17549e-38],[&amp;quot;-2^50&amp;quot;,0,1.17549e-38],[&amp;quot;-2^49&amp;quot;,0,1.17549e-38],[&amp;quot;-2^48&amp;quot;,0,1.17549e-38],[&amp;quot;-2^47&amp;quot;,0,1.17549e-38],[&amp;quot;-2^46&amp;quot;,0,1.17549e-38],[&amp;quot;-2^45&amp;quot;,0,1.17549e-38],[&amp;quot;-2^44&amp;quot;,0,1.17549e-38],[&amp;quot;-2^43&amp;quot;,0,1.17549e-38],[&amp;quot;-2^42&amp;quot;,0,1.17549e-38],[&amp;quot;-2^41&amp;quot;,0,1.17549e-38],[&amp;quot;-2^40&amp;quot;,0,1.17549e-38],[&amp;quot;-2^39&amp;quot;,0,1.17549e-38],[&amp;quot;-2^38&amp;quot;,0,1.17549e-38],[&amp;quot;-2^37&amp;quot;,0,1.17549e-38],[&amp;quot;-2^36&amp;quot;,0,1.17549e-38],[&amp;quot;-2^35&amp;quot;,0,1.17549e-38],[&amp;quot;-2^34&amp;quot;,0,1.17549e-38],[&amp;quot;-2^33&amp;quot;,0,1.17549e-38],[&amp;quot;-2^32&amp;quot;,0,1.17549e-38],[&amp;quot;-2^31&amp;quot;,0,1.17549e-38],[&amp;quot;-2^30&amp;quot;,0,1.17549e-38],[&amp;quot;-2^29&amp;quot;,0,1.17549e-38],[&amp;quot;-2^28&amp;quot;,0,1.17549e-38],[&amp;quot;-2^27&amp;quot;,0,1.17549e-38],[&amp;quot;-2^26&amp;quot;,0,1.17549e-38],[&amp;quot;-2^25&amp;quot;,0,1.17549e-38],[&amp;quot;-2^24&amp;quot;,0,1.17549e-38],[&amp;quot;-2^23&amp;quot;,0,1.17549e-38],[&amp;quot;-2^22&amp;quot;,0,1.17549e-38],[&amp;quot;-2^21&amp;quot;,0,1.17549e-38],[&amp;quot;-2^20&amp;quot;,0,1.17549e-38],[&amp;quot;-2^19&amp;quot;,0,1.17549e-38],[&amp;quot;-2^18&amp;quot;,0,1.17549e-38],[&amp;quot;-2^17&amp;quot;,0,1.17549e-38],[&amp;quot;-2^16&amp;quot;,0,1.17549e-38],[&amp;quot;-2^15&amp;quot;,0,1.17549e-38],[&amp;quot;-2^14&amp;quot;,0,1.17549e-38],[&amp;quot;-2^13&amp;quot;,0,1.17549e-38],[&amp;quot;-2^12&amp;quot;,0,1.17549e-38],[&amp;quot;-2^11&amp;quot;,0,1.17549e-38],[&amp;quot;-2^10&amp;quot;,0,1.17549e-38],[&amp;quot;-2^9&amp;quot;,0,1.17549e-38],[&amp;quot;-2^8&amp;quot;,0,1.17549e-38],[&amp;quot;-2^7&amp;quot;,0,1.17549e-38],[&amp;quot;-2^6&amp;quot;,0,1.17549e-38],[&amp;quot;-2^5&amp;quot;,0,1.17549e-38],[&amp;quot;-2^4&amp;quot;,2.53131e-14,4.24683e-07],[&amp;quot;-2^3&amp;quot;,1.19138e-07,1.9988],[&amp;quot;-2^2&amp;quot;,1.46791e-07,2.46274],[&amp;quot;-2^1&amp;quot;,1.47723e-07,2.47839],[&amp;quot;-2^0&amp;quot;,1.40758e-07,2.36153],[&amp;quot;-2^-1&amp;quot;,1.13422e-07,2.23445],[&amp;quot;-2^-2&amp;quot;,6.38012e-08,2.14081],[&amp;quot;-2^-3&amp;quot;,2.69538e-08,1.80884],[&amp;quot;-2^-4&amp;quot;,1.21308e-08,1.62817],[&amp;quot;-2^-5&amp;quot;,5.59535e-09,1.50199],[&amp;quot;-2^-6&amp;quot;,2.69177e-09,1.44513],[&amp;quot;-2^-7&amp;quot;,1.31289e-09,1.4097],[&amp;quot;-2^-8&amp;quot;,6.49688e-10,1.3952],[&amp;quot;-2^-9&amp;quot;,3.32043e-10,1.42611],[&amp;quot;-2^-10&amp;quot;,1.66275e-10,1.4283],[&amp;quot;-2^-11&amp;quot;,8.08595e-11,1.38916],[&amp;quot;-2^-12&amp;quot;,3.8939e-11,1.33794],[&amp;quot;-2^-13&amp;quot;,1.9401e-11,1.33323],[&amp;quot;-2^-14&amp;quot;,7.88212e-12,1.08331],[&amp;quot;-2^-15&amp;quot;,3.71372e-12,1.02082],[&amp;quot;-2^-16&amp;quot;,1.82845e-12,1.0052],[&amp;quot;-2^-17&amp;quot;,9.10676e-13,1.0013],[&amp;quot;-2^-18&amp;quot;,4.54895e-13,1.00032],[&amp;quot;-2^-19&amp;quot;,2.27392e-13,1.00008],[&amp;quot;-2^-20&amp;quot;,1.13689e-13,1.00002],[&amp;quot;-2^-21&amp;quot;,5.68437e-14,1],[&amp;quot;-2^-22&amp;quot;,2.84217e-14,1],[&amp;quot;-2^-23&amp;quot;,1.42109e-14,1],[&amp;quot;-2^-24&amp;quot;,7.10543e-15,1],[&amp;quot;-2^-25&amp;quot;,3.55271e-15,1],[&amp;quot;-2^-26&amp;quot;,9.92617e-24,5.58794e-09],[&amp;quot;-2^-27&amp;quot;,1.65436e-24,1.86265e-09],[&amp;quot;-2^-28&amp;quot;,8.27181e-25,1.86265e-09],[&amp;quot;-2^-29&amp;quot;,4.1359e-25,1.86265e-09],[&amp;quot;-2^-30&amp;quot;,2.06795e-25,1.86265e-09],[&amp;quot;-2^-31&amp;quot;,1.03398e-25,1.86265e-09],[&amp;quot;-2^-32&amp;quot;,5.16988e-26,1.86265e-09],[&amp;quot;-2^-33&amp;quot;,2.58494e-26,1.86265e-09],[&amp;quot;-2^-34&amp;quot;,1.29247e-26,1.86265e-09],[&amp;quot;-2^-35&amp;quot;,6.46235e-27,1.86265e-09],[&amp;quot;-2^-36&amp;quot;,3.23117e-27,1.86265e-09],[&amp;quot;-2^-37&amp;quot;,1.61559e-27,1.86265e-09],[&amp;quot;-2^-38&amp;quot;,8.07794e-28,1.86265e-09],[&amp;quot;-2^-39&amp;quot;,4.03897e-28,1.86265e-09],[&amp;quot;-2^-40&amp;quot;,2.01948e-28,1.86265e-09],[&amp;quot;-2^-41&amp;quot;,1.00974e-28,1.86265e-09],[&amp;quot;-2^-42&amp;quot;,5.04871e-29,1.86265e-09],[&amp;quot;-2^-43&amp;quot;,2.52435e-29,1.86265e-09],[&amp;quot;-2^-44&amp;quot;,1.26218e-29,1.86265e-09],[&amp;quot;-2^-45&amp;quot;,6.31089e-30,1.86265e-09],[&amp;quot;-2^-46&amp;quot;,3.15544e-30,1.86265e-09],[&amp;quot;-2^-47&amp;quot;,1.57772e-30,1.86265e-09],[&amp;quot;-2^-48&amp;quot;,7.88861e-31,1.86265e-09],[&amp;quot;-2^-49&amp;quot;,3.9443e-31,1.86265e-09],[&amp;quot;-2^-50&amp;quot;,1.97215e-31,1.86265e-09],[&amp;quot;-2^-51&amp;quot;,9.86076e-32,1.86265e-09],[&amp;quot;-2^-52&amp;quot;,4.93038e-32,1.86265e-09],[&amp;quot;-2^-53&amp;quot;,2.46519e-32,1.86265e-09],[&amp;quot;-2^-54&amp;quot;,1.2326e-32,1.86265e-09],[&amp;quot;-2^-55&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-56&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-57&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-58&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-59&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-60&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-61&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-62&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-63&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-64&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-65&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-66&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-67&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-68&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-69&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-70&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-71&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-72&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-73&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-74&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-75&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-76&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-77&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-78&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-79&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-80&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-81&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-82&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-83&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-84&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-85&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-86&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-87&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-88&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-89&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-90&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-91&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-92&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-93&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-94&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-95&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-96&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-97&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-98&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-99&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-100&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-101&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-102&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-103&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-104&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-105&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-106&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-107&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-108&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-109&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-110&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-111&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-112&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-113&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-114&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-115&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-116&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-117&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-118&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-119&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-120&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-121&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-122&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-123&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-124&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-125&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-126&amp;quot;,0,1.17549e-38],[&amp;quot;-SN&amp;quot;,0,1.17549e-38],[&amp;quot;0&amp;quot;,0,1.17549e-38],[&amp;quot;+SN&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-126&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-125&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-124&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-123&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-122&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-121&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-120&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-119&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-118&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-117&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-116&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-115&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-114&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-113&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-112&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-111&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-110&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-109&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-108&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-107&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-106&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-105&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-104&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-103&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-102&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-101&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-100&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-99&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-98&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-97&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-96&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-95&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-94&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-93&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-92&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-91&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-90&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-89&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-88&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-87&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-86&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-85&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-84&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-83&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-82&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-81&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-80&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-79&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-78&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-77&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-76&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-75&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-74&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-73&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-72&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-71&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-70&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-69&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-68&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-67&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-66&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-65&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-64&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-63&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-62&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-61&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-60&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-59&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-58&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-57&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-56&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-55&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-54&amp;quot;,1.2326e-32,1.86265e-09],[&amp;quot;+2^-53&amp;quot;,2.46519e-32,1.86265e-09],[&amp;quot;+2^-52&amp;quot;,4.93038e-32,1.86265e-09],[&amp;quot;+2^-51&amp;quot;,9.86076e-32,1.86265e-09],[&amp;quot;+2^-50&amp;quot;,1.97215e-31,1.86265e-09],[&amp;quot;+2^-49&amp;quot;,3.9443e-31,1.86265e-09],[&amp;quot;+2^-48&amp;quot;,7.88861e-31,1.86265e-09],[&amp;quot;+2^-47&amp;quot;,1.57772e-30,1.86265e-09],[&amp;quot;+2^-46&amp;quot;,3.15544e-30,1.86265e-09],[&amp;quot;+2^-45&amp;quot;,6.31089e-30,1.86265e-09],[&amp;quot;+2^-44&amp;quot;,1.26218e-29,1.86265e-09],[&amp;quot;+2^-43&amp;quot;,2.52435e-29,1.86265e-09],[&amp;quot;+2^-42&amp;quot;,5.04871e-29,1.86265e-09],[&amp;quot;+2^-41&amp;quot;,1.00974e-28,1.86265e-09],[&amp;quot;+2^-40&amp;quot;,2.01948e-28,1.86265e-09],[&amp;quot;+2^-39&amp;quot;,4.03897e-28,1.86265e-09],[&amp;quot;+2^-38&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-37&amp;quot;,1.61559e-27,1.86265e-09],[&amp;quot;+2^-36&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-35&amp;quot;,6.46235e-27,1.86265e-09],[&amp;quot;+2^-34&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-33&amp;quot;,2.58494e-26,1.86265e-09],[&amp;quot;+2^-32&amp;quot;,5.16988e-26,1.86265e-09],[&amp;quot;+2^-31&amp;quot;,1.03398e-25,1.86265e-09],[&amp;quot;+2^-30&amp;quot;,2.06795e-25,1.86265e-09],[&amp;quot;+2^-29&amp;quot;,4.1359e-25,1.86265e-09],[&amp;quot;+2^-28&amp;quot;,8.27181e-25,1.86265e-09],[&amp;quot;+2^-27&amp;quot;,1.65436e-24,1.86265e-09],[&amp;quot;+2^-26&amp;quot;,9.92617e-24,5.58794e-09],[&amp;quot;+2^-25&amp;quot;,3.55271e-15,1],[&amp;quot;+2^-24&amp;quot;,7.10543e-15,1],[&amp;quot;+2^-23&amp;quot;,1.42109e-14,1],[&amp;quot;+2^-22&amp;quot;,2.84217e-14,1],[&amp;quot;+2^-21&amp;quot;,5.68437e-14,1],[&amp;quot;+2^-20&amp;quot;,1.13689e-13,1.00002],[&amp;quot;+2^-19&amp;quot;,2.27391e-13,1.00008],[&amp;quot;+2^-18&amp;quot;,4.54892e-13,1.00032],[&amp;quot;+2^-17&amp;quot;,9.10665e-13,1.00129],[&amp;quot;+2^-16&amp;quot;,1.82841e-12,1.00518],[&amp;quot;+2^-15&amp;quot;,3.71353e-12,1.02077],[&amp;quot;+2^-14&amp;quot;,1.39798e-11,1.92136],[&amp;quot;+2^-13&amp;quot;,2.63239e-11,1.80896],[&amp;quot;+2^-12&amp;quot;,5.37712e-11,1.84757],[&amp;quot;+2^-11&amp;quot;,1.09932e-10,1.88861],[&amp;quot;+2^-10&amp;quot;,2.24787e-10,1.93091],[&amp;quot;+2^-9&amp;quot;,4.3452e-10,1.86625],[&amp;quot;+2^-8&amp;quot;,1.01783e-09,2.18576],[&amp;quot;+2^-7&amp;quot;,2.10562e-09,2.26089],[&amp;quot;+2^-6&amp;quot;,4.03954e-09,2.16871],[&amp;quot;+2^-5&amp;quot;,8.15746e-09,2.18975],[&amp;quot;+2^-4&amp;quot;,1.5545e-08,2.08641],[&amp;quot;+2^-3&amp;quot;,2.62153e-08,2.05864],[&amp;quot;+2^-2&amp;quot;,4.09295e-08,1.73804],[&amp;quot;+2^-1&amp;quot;,8.02409e-08,1.34622],[&amp;quot;+2^0&amp;quot;,8.66201e-08,1.45324],[&amp;quot;+2^1&amp;quot;,8.8654e-08,1.48737],[&amp;quot;+2^2&amp;quot;,6.7976e-08,1.14045],[&amp;quot;+2^3&amp;quot;,7.44117e-08,1.24842],[&amp;quot;+2^4&amp;quot;,2.53131e-14,4.24683e-07],[&amp;quot;+2^5&amp;quot;,0,null],[&amp;quot;+2^6&amp;quot;,0,null],[&amp;quot;+2^7&amp;quot;,0,null],[&amp;quot;+2^8&amp;quot;,0,null],[&amp;quot;+2^9&amp;quot;,0,null],[&amp;quot;+2^10&amp;quot;,0,null],[&amp;quot;+2^11&amp;quot;,0,null],[&amp;quot;+2^12&amp;quot;,0,null],[&amp;quot;+2^13&amp;quot;,0,null],[&amp;quot;+2^14&amp;quot;,0,null],[&amp;quot;+2^15&amp;quot;,0,null],[&amp;quot;+2^16&amp;quot;,0,null],[&amp;quot;+2^17&amp;quot;,0,null],[&amp;quot;+2^18&amp;quot;,0,null],[&amp;quot;+2^19&amp;quot;,0,null],[&amp;quot;+2^20&amp;quot;,0,null],[&amp;quot;+2^21&amp;quot;,0,null],[&amp;quot;+2^22&amp;quot;,0,null],[&amp;quot;+2^23&amp;quot;,0,null],[&amp;quot;+2^24&amp;quot;,0,null],[&amp;quot;+2^25&amp;quot;,0,null],[&amp;quot;+2^26&amp;quot;,0,null],[&amp;quot;+2^27&amp;quot;,0,null],[&amp;quot;+2^28&amp;quot;,0,null],[&amp;quot;+2^29&amp;quot;,0,null],[&amp;quot;+2^30&amp;quot;,0,null],[&amp;quot;+2^31&amp;quot;,0,null],[&amp;quot;+2^32&amp;quot;,0,null],[&amp;quot;+2^33&amp;quot;,0,null],[&amp;quot;+2^34&amp;quot;,0,null],[&amp;quot;+2^35&amp;quot;,0,null],[&amp;quot;+2^36&amp;quot;,0,null],[&amp;quot;+2^37&amp;quot;,0,null],[&amp;quot;+2^38&amp;quot;,0,null],[&amp;quot;+2^39&amp;quot;,0,null],[&amp;quot;+2^40&amp;quot;,0,null],[&amp;quot;+2^41&amp;quot;,0,null],[&amp;quot;+2^42&amp;quot;,0,null],[&amp;quot;+2^43&amp;quot;,0,null],[&amp;quot;+2^44&amp;quot;,0,null],[&amp;quot;+2^45&amp;quot;,0,null],[&amp;quot;+2^46&amp;quot;,0,null],[&amp;quot;+2^47&amp;quot;,0,null],[&amp;quot;+2^48&amp;quot;,0,null],[&amp;quot;+2^49&amp;quot;,0,null],[&amp;quot;+2^50&amp;quot;,0,null],[&amp;quot;+2^51&amp;quot;,0,null],[&amp;quot;+2^52&amp;quot;,0,null],[&amp;quot;+2^53&amp;quot;,0,null],[&amp;quot;+2^54&amp;quot;,0,null],[&amp;quot;+2^55&amp;quot;,0,null],[&amp;quot;+2^56&amp;quot;,0,null],[&amp;quot;+2^57&amp;quot;,0,null],[&amp;quot;+2^58&amp;quot;,0,null],[&amp;quot;+2^59&amp;quot;,0,null],[&amp;quot;+2^60&amp;quot;,0,null],[&amp;quot;+2^61&amp;quot;,0,null],[&amp;quot;+2^62&amp;quot;,0,null],[&amp;quot;+2^63&amp;quot;,0,null],[&amp;quot;+2^64&amp;quot;,0,null],[&amp;quot;+2^65&amp;quot;,0,null],[&amp;quot;+2^66&amp;quot;,0,null],[&amp;quot;+2^67&amp;quot;,0,null],[&amp;quot;+2^68&amp;quot;,0,null],[&amp;quot;+2^69&amp;quot;,0,null],[&amp;quot;+2^70&amp;quot;,0,null],[&amp;quot;+2^71&amp;quot;,0,null],[&amp;quot;+2^72&amp;quot;,0,null],[&amp;quot;+2^73&amp;quot;,0,null],[&amp;quot;+2^74&amp;quot;,0,null],[&amp;quot;+2^75&amp;quot;,0,null],[&amp;quot;+2^76&amp;quot;,0,null],[&amp;quot;+2^77&amp;quot;,0,null],[&amp;quot;+2^78&amp;quot;,0,null],[&amp;quot;+2^79&amp;quot;,0,null],[&amp;quot;+2^80&amp;quot;,0,null],[&amp;quot;+2^81&amp;quot;,0,null],[&amp;quot;+2^82&amp;quot;,0,null],[&amp;quot;+2^83&amp;quot;,0,null],[&amp;quot;+2^84&amp;quot;,0,null],[&amp;quot;+2^85&amp;quot;,0,null],[&amp;quot;+2^86&amp;quot;,0,null],[&amp;quot;+2^87&amp;quot;,0,null],[&amp;quot;+2^88&amp;quot;,0,null],[&amp;quot;+2^89&amp;quot;,0,null],[&amp;quot;+2^90&amp;quot;,0,null],[&amp;quot;+2^91&amp;quot;,0,null],[&amp;quot;+2^92&amp;quot;,0,null],[&amp;quot;+2^93&amp;quot;,0,null],[&amp;quot;+2^94&amp;quot;,0,null],[&amp;quot;+2^95&amp;quot;,0,null],[&amp;quot;+2^96&amp;quot;,0,null],[&amp;quot;+2^97&amp;quot;,0,null],[&amp;quot;+2^98&amp;quot;,0,null],[&amp;quot;+2^99&amp;quot;,0,null],[&amp;quot;+2^100&amp;quot;,0,null],[&amp;quot;+2^101&amp;quot;,0,null],[&amp;quot;+2^102&amp;quot;,0,null],[&amp;quot;+2^103&amp;quot;,0,null],[&amp;quot;+2^104&amp;quot;,0,null],[&amp;quot;+2^105&amp;quot;,0,null],[&amp;quot;+2^106&amp;quot;,0,null],[&amp;quot;+2^107&amp;quot;,0,null],[&amp;quot;+2^108&amp;quot;,0,null],[&amp;quot;+2^109&amp;quot;,0,null],[&amp;quot;+2^110&amp;quot;,0,null],[&amp;quot;+2^111&amp;quot;,0,null],[&amp;quot;+2^112&amp;quot;,0,null],[&amp;quot;+2^113&amp;quot;,0,null],[&amp;quot;+2^114&amp;quot;,0,null],[&amp;quot;+2^115&amp;quot;,0,null],[&amp;quot;+2^116&amp;quot;,0,null],[&amp;quot;+2^117&amp;quot;,0,null],[&amp;quot;+2^118&amp;quot;,0,null],[&amp;quot;+2^119&amp;quot;,0,null],[&amp;quot;+2^120&amp;quot;,0,null],[&amp;quot;+2^121&amp;quot;,0,null],[&amp;quot;+2^122&amp;quot;,0,null],[&amp;quot;+2^123&amp;quot;,0,null],[&amp;quot;+2^124&amp;quot;,0,null],[&amp;quot;+2^125&amp;quot;,0,null],[&amp;quot;+2^126&amp;quot;,0,null],[&amp;quot;+2^127&amp;quot;,0,null] ]&lt;br /&gt;
 }}&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Key observation:&#039;&#039;&#039;  &lt;br /&gt;
Compared to direct exponential formulations, the &amp;lt;code&amp;gt;expm1&amp;lt;/code&amp;gt; approach maintains low ULP error for small and medium values of &amp;lt;math&amp;gt;x&amp;lt;/math&amp;gt;. &lt;br /&gt;
Overflow is still present for very large inputs, but the overall error behavior is substantially more stable.&lt;br /&gt;
&lt;br /&gt;
== Numerical Issues and Error Metrics ==&lt;br /&gt;
&lt;br /&gt;
To evaluate and compare different software implementations of &amp;lt;code&amp;gt;tanh&amp;lt;/code&amp;gt;, several numerical error metrics are used throughout this article. These metrics make it possible to reason about accuracy, robustness, and failure modes in floating-point arithmetic.&lt;br /&gt;
&lt;br /&gt;
=== ULP Error ===&lt;br /&gt;
ULP (Unit in the Last Place) measures the distance between two adjacent representable floating-point numbers. The ULP error of a computed result is defined as the difference, measured in ULPs, between the computed value and the correctly rounded reference value.&lt;br /&gt;
&lt;br /&gt;
An error of &amp;lt;code&amp;gt;ULP ≤ 2&amp;lt;/code&amp;gt; is commonly considered acceptable for single-precision transcendental functions, as it indicates that the result is very close to the correctly rounded value.&lt;br /&gt;
&lt;br /&gt;
Large ULP errors typically indicate catastrophic cancellation, loss of significant bits, or severe rounding effects.&lt;br /&gt;
&lt;br /&gt;
=== NaN and Overflow ===&lt;br /&gt;
NaN (Not a Number) results occur when undefined floating-point operations are performed, such as &amp;lt;code&amp;gt;inf / inf&amp;lt;/code&amp;gt;. In &amp;lt;code&amp;gt;tanh&amp;lt;/code&amp;gt; implementations, this often happens when both the numerator and denominator of a formulation overflow to infinity.&lt;br /&gt;
&lt;br /&gt;
Tracking NaN generation is critical, as NaNs can silently propagate through numerical pipelines and invalidate downstream results.&lt;br /&gt;
&lt;br /&gt;
=== Error Graphs ===&lt;br /&gt;
The error graphs shown in the following sections visualize ULP error across the full &amp;lt;code&amp;gt;float32&amp;lt;/code&amp;gt; input domain. They make it possible to identify:&lt;br /&gt;
* regions with catastrophic cancellation,&lt;br /&gt;
* input ranges that trigger overflow or NaNs,&lt;br /&gt;
* and numerical stability differences between formulations.&lt;br /&gt;
&lt;br /&gt;
These graphs are a key tool for understanding not only the maximum error, but also how errors are distributed across different input magnitudes.&lt;br /&gt;
&lt;br /&gt;
== Taylor Expansion (Local Approximation) ==&lt;br /&gt;
&lt;br /&gt;
Taylor series approximations of &amp;lt;code&amp;gt;tanh(x)&amp;lt;/code&amp;gt; expand the function around &amp;lt;math&amp;gt;x = 0&amp;lt;/math&amp;gt; and therefore provide a very accurate approximation only for sufficiently small input magnitudes.&lt;br /&gt;
&lt;br /&gt;
For small &amp;lt;math&amp;gt;|x|&amp;lt;/math&amp;gt;, the hyperbolic tangent behaves approximately linear (&amp;lt;math&amp;gt;\tanh(x) \approx x&amp;lt;/math&amp;gt;), and low-order Taylor polynomials can achieve excellent accuracy with minimal computational cost. This makes Taylor-based approximations attractive as a fast path in range-reduced implementations.&lt;br /&gt;
&lt;br /&gt;
However, Taylor expansions have a strictly limited radius of convergence. Outside a narrow neighborhood around zero, the approximation error grows extremely rapidly and quickly becomes unusable. As a result, Taylor polynomials are not suitable as standalone implementations of &amp;lt;code&amp;gt;tanh&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
{{#widget:FloatErrorGraph|id=taylor&lt;br /&gt;
  |data=[ [&amp;quot;-2^127&amp;quot;,0,0],[&amp;quot;-2^126&amp;quot;,0,0],[&amp;quot;-2^125&amp;quot;,0,0],[&amp;quot;-2^124&amp;quot;,0,0],[&amp;quot;-2^123&amp;quot;,0,0],[&amp;quot;-2^122&amp;quot;,0,0],[&amp;quot;-2^121&amp;quot;,0,0],[&amp;quot;-2^120&amp;quot;,0,0],[&amp;quot;-2^119&amp;quot;,0,0],[&amp;quot;-2^118&amp;quot;,0,0],[&amp;quot;-2^117&amp;quot;,0,0],[&amp;quot;-2^116&amp;quot;,0,0],[&amp;quot;-2^115&amp;quot;,0,0],[&amp;quot;-2^114&amp;quot;,0,0],[&amp;quot;-2^113&amp;quot;,0,0],[&amp;quot;-2^112&amp;quot;,0,0],[&amp;quot;-2^111&amp;quot;,0,0],[&amp;quot;-2^110&amp;quot;,0,0],[&amp;quot;-2^109&amp;quot;,0,0],[&amp;quot;-2^108&amp;quot;,0,0],[&amp;quot;-2^107&amp;quot;,0,0],[&amp;quot;-2^106&amp;quot;,0,0],[&amp;quot;-2^105&amp;quot;,0,0],[&amp;quot;-2^104&amp;quot;,0,0],[&amp;quot;-2^103&amp;quot;,0,0],[&amp;quot;-2^102&amp;quot;,0,0],[&amp;quot;-2^101&amp;quot;,0,0],[&amp;quot;-2^100&amp;quot;,0,0],[&amp;quot;-2^99&amp;quot;,0,0],[&amp;quot;-2^98&amp;quot;,0,0],[&amp;quot;-2^97&amp;quot;,0,0],[&amp;quot;-2^96&amp;quot;,0,0],[&amp;quot;-2^95&amp;quot;,0,0],[&amp;quot;-2^94&amp;quot;,0,0],[&amp;quot;-2^93&amp;quot;,0,0],[&amp;quot;-2^92&amp;quot;,0,0],[&amp;quot;-2^91&amp;quot;,0,0],[&amp;quot;-2^90&amp;quot;,0,0],[&amp;quot;-2^89&amp;quot;,0,0],[&amp;quot;-2^88&amp;quot;,0,0],[&amp;quot;-2^87&amp;quot;,0,0],[&amp;quot;-2^86&amp;quot;,0,0],[&amp;quot;-2^85&amp;quot;,0,0],[&amp;quot;-2^84&amp;quot;,0,0],[&amp;quot;-2^83&amp;quot;,0,0],[&amp;quot;-2^82&amp;quot;,0,0],[&amp;quot;-2^81&amp;quot;,0,0],[&amp;quot;-2^80&amp;quot;,0,0],[&amp;quot;-2^79&amp;quot;,0,0],[&amp;quot;-2^78&amp;quot;,0,0],[&amp;quot;-2^77&amp;quot;,0,0],[&amp;quot;-2^76&amp;quot;,0,0],[&amp;quot;-2^75&amp;quot;,0,0],[&amp;quot;-2^74&amp;quot;,0,0],[&amp;quot;-2^73&amp;quot;,0,0],[&amp;quot;-2^72&amp;quot;,0,0],[&amp;quot;-2^71&amp;quot;,0,0],[&amp;quot;-2^70&amp;quot;,0,0],[&amp;quot;-2^69&amp;quot;,0,0],[&amp;quot;-2^68&amp;quot;,0,0],[&amp;quot;-2^67&amp;quot;,0,0],[&amp;quot;-2^66&amp;quot;,0,0],[&amp;quot;-2^65&amp;quot;,0,0],[&amp;quot;-2^64&amp;quot;,0,0],[&amp;quot;-2^63&amp;quot;,1.84467e+19,3.09485e+26],[&amp;quot;-2^62&amp;quot;,9.22336e+18,1.54742e+26],[&amp;quot;-2^61&amp;quot;,4.61168e+18,7.73712e+25],[&amp;quot;-2^60&amp;quot;,2.30584e+18,3.86856e+25],[&amp;quot;-2^59&amp;quot;,1.15292e+18,1.93428e+25],[&amp;quot;-2^58&amp;quot;,5.7646e+17,9.6714e+24],[&amp;quot;-2^57&amp;quot;,2.8823e+17,4.8357e+24],[&amp;quot;-2^56&amp;quot;,1.44115e+17,2.41785e+24],[&amp;quot;-2^55&amp;quot;,7.20575e+16,1.20892e+24],[&amp;quot;-2^54&amp;quot;,3.60288e+16,6.04462e+23],[&amp;quot;-2^53&amp;quot;,1.80144e+16,3.02231e+23],[&amp;quot;-2^52&amp;quot;,9.00719e+15,1.51116e+23],[&amp;quot;-2^51&amp;quot;,4.5036e+15,7.55578e+22],[&amp;quot;-2^50&amp;quot;,2.2518e+15,3.77789e+22],[&amp;quot;-2^49&amp;quot;,1.1259e+15,1.88894e+22],[&amp;quot;-2^48&amp;quot;,5.62949e+14,9.44472e+21],[&amp;quot;-2^47&amp;quot;,2.81475e+14,4.72236e+21],[&amp;quot;-2^46&amp;quot;,1.40737e+14,2.36118e+21],[&amp;quot;-2^45&amp;quot;,7.03687e+13,1.18059e+21],[&amp;quot;-2^44&amp;quot;,3.51843e+13,5.90295e+20],[&amp;quot;-2^43&amp;quot;,1.75922e+13,2.95148e+20],[&amp;quot;-2^42&amp;quot;,8.79608e+12,1.47574e+20],[&amp;quot;-2^41&amp;quot;,4.39804e+12,7.37869e+19],[&amp;quot;-2^40&amp;quot;,2.19902e+12,3.68935e+19],[&amp;quot;-2^39&amp;quot;,1.09951e+12,1.84467e+19],[&amp;quot;-2^38&amp;quot;,5.49755e+11,9.22336e+18],[&amp;quot;-2^37&amp;quot;,2.74878e+11,4.61168e+18],[&amp;quot;-2^36&amp;quot;,1.37439e+11,2.30584e+18],[&amp;quot;-2^35&amp;quot;,6.87194e+10,1.15292e+18],[&amp;quot;-2^34&amp;quot;,3.43597e+10,5.7646e+17],[&amp;quot;-2^33&amp;quot;,1.71799e+10,2.8823e+17],[&amp;quot;-2^32&amp;quot;,8.58993e+09,1.44115e+17],[&amp;quot;-2^31&amp;quot;,4.29085e+09,7.19885e+16],[&amp;quot;-2^30&amp;quot;,2.14542e+09,3.59943e+16],[&amp;quot;-2^29&amp;quot;,1.07271e+09,1.79971e+16],[&amp;quot;-2^28&amp;quot;,5.36356e+08,8.99856e+15],[&amp;quot;-2^27&amp;quot;,2.68178e+08,4.49928e+15],[&amp;quot;-2^26&amp;quot;,1.34089e+08,2.24964e+15],[&amp;quot;-2^25&amp;quot;,6.70445e+07,1.12482e+15],[&amp;quot;-2^24&amp;quot;,3.35223e+07,5.6241e+14],[&amp;quot;-2^23&amp;quot;,1.67611e+07,2.81205e+14],[&amp;quot;-2^22&amp;quot;,8.38056e+06,1.40603e+14],[&amp;quot;-2^21&amp;quot;,4.19028e+06,7.03013e+13],[&amp;quot;-2^20&amp;quot;,2.09514e+06,3.51506e+13],[&amp;quot;-2^19&amp;quot;,1.04757e+06,1.75753e+13],[&amp;quot;-2^18&amp;quot;,523784,8.78764e+12],[&amp;quot;-2^17&amp;quot;,261892,4.39381e+12],[&amp;quot;-2^16&amp;quot;,130945,2.1969e+12],[&amp;quot;-2^15&amp;quot;,65472.2,1.09844e+12],[&amp;quot;-2^14&amp;quot;,32735.6,5.49212e+11],[&amp;quot;-2^13&amp;quot;,16367.3,2.74598e+11],[&amp;quot;-2^12&amp;quot;,8183.15,1.3729e+11],[&amp;quot;-2^11&amp;quot;,4091.07,6.86368e+10],[&amp;quot;-2^10&amp;quot;,2045.04,3.431e+10],[&amp;quot;-2^9&amp;quot;,1022.02,1.71466e+10],[&amp;quot;-2^8&amp;quot;,510.509,8.56492e+09],[&amp;quot;-2^7&amp;quot;,254.754,4.27406e+09],[&amp;quot;-2^6&amp;quot;,126.876,2.12862e+09],[&amp;quot;-2^5&amp;quot;,62.9354,1.05588e+09],[&amp;quot;-2^4&amp;quot;,30.9629,5.19471e+08],[&amp;quot;-2^3&amp;quot;,14.9718,2.51185e+08],[&amp;quot;-2^2&amp;quot;,6.96672,1.16882e+08],[&amp;quot;-2^1&amp;quot;,2.94633,4.94312e+07],[&amp;quot;-2^0&amp;quot;,0.940715,1.57826e+07],[&amp;quot;-2^-1&amp;quot;,0.127635,2.14135e+06],[&amp;quot;-2^-2&amp;quot;,0.00318735,106950],[&amp;quot;-2^-3&amp;quot;,3.11475e-05,2090.28],[&amp;quot;-2^-4&amp;quot;,2.57737e-07,34.5929],[&amp;quot;-2^-5&amp;quot;,3.89213e-09,1.04478],[&amp;quot;-2^-6&amp;quot;,9.51472e-10,0.510818],[&amp;quot;-2^-7&amp;quot;,4.66402e-10,0.500795],[&amp;quot;-2^-8&amp;quot;,2.32917e-10,0.500185],[&amp;quot;-2^-9&amp;quot;,1.16423e-10,0.500033],[&amp;quot;-2^-10&amp;quot;,5.82082e-11,0.500004],[&amp;quot;-2^-11&amp;quot;,2.91037e-11,0.499998],[&amp;quot;-2^-12&amp;quot;,1.45519e-11,0.5],[&amp;quot;-2^-13&amp;quot;,4.85062e-12,0.333332],[&amp;quot;-2^-14&amp;quot;,6.06328e-13,0.0833331],[&amp;quot;-2^-15&amp;quot;,7.5791e-14,0.0208333],[&amp;quot;-2^-16&amp;quot;,9.47387e-15,0.00520832],[&amp;quot;-2^-17&amp;quot;,1.18424e-15,0.00130208],[&amp;quot;-2^-18&amp;quot;,1.48029e-16,0.00032552],[&amp;quot;-2^-19&amp;quot;,1.85039e-17,8.13808e-05],[&amp;quot;-2^-20&amp;quot;,2.31304e-18,2.03457e-05],[&amp;quot;-2^-21&amp;quot;,2.89156e-19,5.08688e-06],[&amp;quot;-2^-22&amp;quot;,3.61577e-20,1.27219e-06],[&amp;quot;-2^-23&amp;quot;,4.52633e-21,3.18512e-07],[&amp;quot;-2^-24&amp;quot;,5.691e-22,8.00937e-08],[&amp;quot;-2^-25&amp;quot;,7.27919e-23,2.04891e-08],[&amp;quot;-2^-26&amp;quot;,9.92617e-24,5.58794e-09],[&amp;quot;-2^-27&amp;quot;,1.65436e-24,1.86265e-09],[&amp;quot;-2^-28&amp;quot;,8.27181e-25,1.86265e-09],[&amp;quot;-2^-29&amp;quot;,4.1359e-25,1.86265e-09],[&amp;quot;-2^-30&amp;quot;,2.06795e-25,1.86265e-09],[&amp;quot;-2^-31&amp;quot;,1.03398e-25,1.86265e-09],[&amp;quot;-2^-32&amp;quot;,5.16988e-26,1.86265e-09],[&amp;quot;-2^-33&amp;quot;,2.58494e-26,1.86265e-09],[&amp;quot;-2^-34&amp;quot;,1.29247e-26,1.86265e-09],[&amp;quot;-2^-35&amp;quot;,6.46235e-27,1.86265e-09],[&amp;quot;-2^-36&amp;quot;,3.23117e-27,1.86265e-09],[&amp;quot;-2^-37&amp;quot;,1.61559e-27,1.86265e-09],[&amp;quot;-2^-38&amp;quot;,8.07794e-28,1.86265e-09],[&amp;quot;-2^-39&amp;quot;,4.03897e-28,1.86265e-09],[&amp;quot;-2^-40&amp;quot;,2.01948e-28,1.86265e-09],[&amp;quot;-2^-41&amp;quot;,1.00974e-28,1.86265e-09],[&amp;quot;-2^-42&amp;quot;,5.04871e-29,1.86265e-09],[&amp;quot;-2^-43&amp;quot;,2.52435e-29,1.86265e-09],[&amp;quot;-2^-44&amp;quot;,1.26218e-29,1.86265e-09],[&amp;quot;-2^-45&amp;quot;,6.31089e-30,1.86265e-09],[&amp;quot;-2^-46&amp;quot;,3.15544e-30,1.86265e-09],[&amp;quot;-2^-47&amp;quot;,1.57772e-30,1.86265e-09],[&amp;quot;-2^-48&amp;quot;,7.88861e-31,1.86265e-09],[&amp;quot;-2^-49&amp;quot;,3.9443e-31,1.86265e-09],[&amp;quot;-2^-50&amp;quot;,1.97215e-31,1.86265e-09],[&amp;quot;-2^-51&amp;quot;,9.86076e-32,1.86265e-09],[&amp;quot;-2^-52&amp;quot;,4.93038e-32,1.86265e-09],[&amp;quot;-2^-53&amp;quot;,2.46519e-32,1.86265e-09],[&amp;quot;-2^-54&amp;quot;,1.2326e-32,1.86265e-09],[&amp;quot;-2^-55&amp;quot;,0,0],[&amp;quot;-2^-56&amp;quot;,0,0],[&amp;quot;-2^-57&amp;quot;,0,0],[&amp;quot;-2^-58&amp;quot;,0,0],[&amp;quot;-2^-59&amp;quot;,0,0],[&amp;quot;-2^-60&amp;quot;,0,0],[&amp;quot;-2^-61&amp;quot;,0,0],[&amp;quot;-2^-62&amp;quot;,0,0],[&amp;quot;-2^-63&amp;quot;,0,0],[&amp;quot;-2^-64&amp;quot;,0,0],[&amp;quot;-2^-65&amp;quot;,0,0],[&amp;quot;-2^-66&amp;quot;,0,0],[&amp;quot;-2^-67&amp;quot;,0,0],[&amp;quot;-2^-68&amp;quot;,0,0],[&amp;quot;-2^-69&amp;quot;,0,0],[&amp;quot;-2^-70&amp;quot;,0,0],[&amp;quot;-2^-71&amp;quot;,0,0],[&amp;quot;-2^-72&amp;quot;,0,0],[&amp;quot;-2^-73&amp;quot;,0,0],[&amp;quot;-2^-74&amp;quot;,0,0],[&amp;quot;-2^-75&amp;quot;,0,0],[&amp;quot;-2^-76&amp;quot;,0,0],[&amp;quot;-2^-77&amp;quot;,0,0],[&amp;quot;-2^-78&amp;quot;,0,0],[&amp;quot;-2^-79&amp;quot;,0,0],[&amp;quot;-2^-80&amp;quot;,0,0],[&amp;quot;-2^-81&amp;quot;,0,0],[&amp;quot;-2^-82&amp;quot;,0,0],[&amp;quot;-2^-83&amp;quot;,0,0],[&amp;quot;-2^-84&amp;quot;,0,0],[&amp;quot;-2^-85&amp;quot;,0,0],[&amp;quot;-2^-86&amp;quot;,0,0],[&amp;quot;-2^-87&amp;quot;,0,0],[&amp;quot;-2^-88&amp;quot;,0,0],[&amp;quot;-2^-89&amp;quot;,0,0],[&amp;quot;-2^-90&amp;quot;,0,0],[&amp;quot;-2^-91&amp;quot;,0,0],[&amp;quot;-2^-92&amp;quot;,0,0],[&amp;quot;-2^-93&amp;quot;,0,0],[&amp;quot;-2^-94&amp;quot;,0,0],[&amp;quot;-2^-95&amp;quot;,0,0],[&amp;quot;-2^-96&amp;quot;,0,0],[&amp;quot;-2^-97&amp;quot;,0,0],[&amp;quot;-2^-98&amp;quot;,0,0],[&amp;quot;-2^-99&amp;quot;,0,0],[&amp;quot;-2^-100&amp;quot;,0,0],[&amp;quot;-2^-101&amp;quot;,0,0],[&amp;quot;-2^-102&amp;quot;,0,0],[&amp;quot;-2^-103&amp;quot;,0,0],[&amp;quot;-2^-104&amp;quot;,0,0],[&amp;quot;-2^-105&amp;quot;,0,0],[&amp;quot;-2^-106&amp;quot;,0,0],[&amp;quot;-2^-107&amp;quot;,0,0],[&amp;quot;-2^-108&amp;quot;,0,0],[&amp;quot;-2^-109&amp;quot;,0,0],[&amp;quot;-2^-110&amp;quot;,0,0],[&amp;quot;-2^-111&amp;quot;,0,0],[&amp;quot;-2^-112&amp;quot;,0,0],[&amp;quot;-2^-113&amp;quot;,0,0],[&amp;quot;-2^-114&amp;quot;,0,0],[&amp;quot;-2^-115&amp;quot;,0,0],[&amp;quot;-2^-116&amp;quot;,0,0],[&amp;quot;-2^-117&amp;quot;,0,0],[&amp;quot;-2^-118&amp;quot;,0,0],[&amp;quot;-2^-119&amp;quot;,0,0],[&amp;quot;-2^-120&amp;quot;,0,0],[&amp;quot;-2^-121&amp;quot;,0,0],[&amp;quot;-2^-122&amp;quot;,0,0],[&amp;quot;-2^-123&amp;quot;,0,0],[&amp;quot;-2^-124&amp;quot;,0,0],[&amp;quot;-2^-125&amp;quot;,0,0],[&amp;quot;-2^-126&amp;quot;,0,0],[&amp;quot;-SN&amp;quot;,0,0],[&amp;quot;0&amp;quot;,0,0],[&amp;quot;+SN&amp;quot;,0,0],[&amp;quot;+2^-126&amp;quot;,0,0],[&amp;quot;+2^-125&amp;quot;,0,0],[&amp;quot;+2^-124&amp;quot;,0,0],[&amp;quot;+2^-123&amp;quot;,0,0],[&amp;quot;+2^-122&amp;quot;,0,0],[&amp;quot;+2^-121&amp;quot;,0,0],[&amp;quot;+2^-120&amp;quot;,0,0],[&amp;quot;+2^-119&amp;quot;,0,0],[&amp;quot;+2^-118&amp;quot;,0,0],[&amp;quot;+2^-117&amp;quot;,0,0],[&amp;quot;+2^-116&amp;quot;,0,0],[&amp;quot;+2^-115&amp;quot;,0,0],[&amp;quot;+2^-114&amp;quot;,0,0],[&amp;quot;+2^-113&amp;quot;,0,0],[&amp;quot;+2^-112&amp;quot;,0,0],[&amp;quot;+2^-111&amp;quot;,0,0],[&amp;quot;+2^-110&amp;quot;,0,0],[&amp;quot;+2^-109&amp;quot;,0,0],[&amp;quot;+2^-108&amp;quot;,0,0],[&amp;quot;+2^-107&amp;quot;,0,0],[&amp;quot;+2^-106&amp;quot;,0,0],[&amp;quot;+2^-105&amp;quot;,0,0],[&amp;quot;+2^-104&amp;quot;,0,0],[&amp;quot;+2^-103&amp;quot;,0,0],[&amp;quot;+2^-102&amp;quot;,0,0],[&amp;quot;+2^-101&amp;quot;,0,0],[&amp;quot;+2^-100&amp;quot;,0,0],[&amp;quot;+2^-99&amp;quot;,0,0],[&amp;quot;+2^-98&amp;quot;,0,0],[&amp;quot;+2^-97&amp;quot;,0,0],[&amp;quot;+2^-96&amp;quot;,0,0],[&amp;quot;+2^-95&amp;quot;,0,0],[&amp;quot;+2^-94&amp;quot;,0,0],[&amp;quot;+2^-93&amp;quot;,0,0],[&amp;quot;+2^-92&amp;quot;,0,0],[&amp;quot;+2^-91&amp;quot;,0,0],[&amp;quot;+2^-90&amp;quot;,0,0],[&amp;quot;+2^-89&amp;quot;,0,0],[&amp;quot;+2^-88&amp;quot;,0,0],[&amp;quot;+2^-87&amp;quot;,0,0],[&amp;quot;+2^-86&amp;quot;,0,0],[&amp;quot;+2^-85&amp;quot;,0,0],[&amp;quot;+2^-84&amp;quot;,0,0],[&amp;quot;+2^-83&amp;quot;,0,0],[&amp;quot;+2^-82&amp;quot;,0,0],[&amp;quot;+2^-81&amp;quot;,0,0],[&amp;quot;+2^-80&amp;quot;,0,0],[&amp;quot;+2^-79&amp;quot;,0,0],[&amp;quot;+2^-78&amp;quot;,0,0],[&amp;quot;+2^-77&amp;quot;,0,0],[&amp;quot;+2^-76&amp;quot;,0,0],[&amp;quot;+2^-75&amp;quot;,0,0],[&amp;quot;+2^-74&amp;quot;,0,0],[&amp;quot;+2^-73&amp;quot;,0,0],[&amp;quot;+2^-72&amp;quot;,0,0],[&amp;quot;+2^-71&amp;quot;,0,0],[&amp;quot;+2^-70&amp;quot;,0,0],[&amp;quot;+2^-69&amp;quot;,0,0],[&amp;quot;+2^-68&amp;quot;,0,0],[&amp;quot;+2^-67&amp;quot;,0,0],[&amp;quot;+2^-66&amp;quot;,0,0],[&amp;quot;+2^-65&amp;quot;,0,0],[&amp;quot;+2^-64&amp;quot;,0,0],[&amp;quot;+2^-63&amp;quot;,0,0],[&amp;quot;+2^-62&amp;quot;,0,0],[&amp;quot;+2^-61&amp;quot;,0,0],[&amp;quot;+2^-60&amp;quot;,0,0],[&amp;quot;+2^-59&amp;quot;,0,0],[&amp;quot;+2^-58&amp;quot;,0,0],[&amp;quot;+2^-57&amp;quot;,0,0],[&amp;quot;+2^-56&amp;quot;,0,0],[&amp;quot;+2^-55&amp;quot;,0,0],[&amp;quot;+2^-54&amp;quot;,1.2326e-32,1.86265e-09],[&amp;quot;+2^-53&amp;quot;,2.46519e-32,1.86265e-09],[&amp;quot;+2^-52&amp;quot;,4.93038e-32,1.86265e-09],[&amp;quot;+2^-51&amp;quot;,9.86076e-32,1.86265e-09],[&amp;quot;+2^-50&amp;quot;,1.97215e-31,1.86265e-09],[&amp;quot;+2^-49&amp;quot;,3.9443e-31,1.86265e-09],[&amp;quot;+2^-48&amp;quot;,7.88861e-31,1.86265e-09],[&amp;quot;+2^-47&amp;quot;,1.57772e-30,1.86265e-09],[&amp;quot;+2^-46&amp;quot;,3.15544e-30,1.86265e-09],[&amp;quot;+2^-45&amp;quot;,6.31089e-30,1.86265e-09],[&amp;quot;+2^-44&amp;quot;,1.26218e-29,1.86265e-09],[&amp;quot;+2^-43&amp;quot;,2.52435e-29,1.86265e-09],[&amp;quot;+2^-42&amp;quot;,5.04871e-29,1.86265e-09],[&amp;quot;+2^-41&amp;quot;,1.00974e-28,1.86265e-09],[&amp;quot;+2^-40&amp;quot;,2.01948e-28,1.86265e-09],[&amp;quot;+2^-39&amp;quot;,4.03897e-28,1.86265e-09],[&amp;quot;+2^-38&amp;quot;,8.07794e-28,1.86265e-09],[&amp;quot;+2^-37&amp;quot;,1.61559e-27,1.86265e-09],[&amp;quot;+2^-36&amp;quot;,3.23117e-27,1.86265e-09],[&amp;quot;+2^-35&amp;quot;,6.46235e-27,1.86265e-09],[&amp;quot;+2^-34&amp;quot;,1.29247e-26,1.86265e-09],[&amp;quot;+2^-33&amp;quot;,2.58494e-26,1.86265e-09],[&amp;quot;+2^-32&amp;quot;,5.16988e-26,1.86265e-09],[&amp;quot;+2^-31&amp;quot;,1.03398e-25,1.86265e-09],[&amp;quot;+2^-30&amp;quot;,2.06795e-25,1.86265e-09],[&amp;quot;+2^-29&amp;quot;,4.1359e-25,1.86265e-09],[&amp;quot;+2^-28&amp;quot;,8.27181e-25,1.86265e-09],[&amp;quot;+2^-27&amp;quot;,1.65436e-24,1.86265e-09],[&amp;quot;+2^-26&amp;quot;,9.92617e-24,5.58794e-09],[&amp;quot;+2^-25&amp;quot;,7.27919e-23,2.04891e-08],[&amp;quot;+2^-24&amp;quot;,5.691e-22,8.00937e-08],[&amp;quot;+2^-23&amp;quot;,4.52633e-21,3.18512e-07],[&amp;quot;+2^-22&amp;quot;,3.61577e-20,1.27219e-06],[&amp;quot;+2^-21&amp;quot;,2.89156e-19,5.08688e-06],[&amp;quot;+2^-20&amp;quot;,2.31304e-18,2.03457e-05],[&amp;quot;+2^-19&amp;quot;,1.85039e-17,8.13808e-05],[&amp;quot;+2^-18&amp;quot;,1.48029e-16,0.00032552],[&amp;quot;+2^-17&amp;quot;,1.18424e-15,0.00130208],[&amp;quot;+2^-16&amp;quot;,9.47387e-15,0.00520832],[&amp;quot;+2^-15&amp;quot;,7.5791e-14,0.0208333],[&amp;quot;+2^-14&amp;quot;,6.06328e-13,0.0833331],[&amp;quot;+2^-13&amp;quot;,4.85062e-12,0.333332],[&amp;quot;+2^-12&amp;quot;,1.45519e-11,0.5],[&amp;quot;+2^-11&amp;quot;,2.91037e-11,0.499998],[&amp;quot;+2^-10&amp;quot;,5.82082e-11,0.500004],[&amp;quot;+2^-9&amp;quot;,1.16423e-10,0.500033],[&amp;quot;+2^-8&amp;quot;,2.32917e-10,0.500185],[&amp;quot;+2^-7&amp;quot;,4.66402e-10,0.500795],[&amp;quot;+2^-6&amp;quot;,9.51472e-10,0.510818],[&amp;quot;+2^-5&amp;quot;,3.89213e-09,1.04478],[&amp;quot;+2^-4&amp;quot;,2.57737e-07,34.5929],[&amp;quot;+2^-3&amp;quot;,3.11475e-05,2090.28],[&amp;quot;+2^-2&amp;quot;,0.00318735,106950],[&amp;quot;+2^-1&amp;quot;,0.127635,2.14135e+06],[&amp;quot;+2^0&amp;quot;,0.940715,1.57826e+07],[&amp;quot;+2^1&amp;quot;,2.94633,4.94312e+07],[&amp;quot;+2^2&amp;quot;,6.96672,1.16882e+08],[&amp;quot;+2^3&amp;quot;,14.9718,1.33874e+08],[&amp;quot;+2^4&amp;quot;,30.9629,2.59736e+08],[&amp;quot;+2^5&amp;quot;,62.9354,5.27941e+08],[&amp;quot;+2^6&amp;quot;,126.876,1.06431e+09],[&amp;quot;+2^7&amp;quot;,254.754,2.13703e+09],[&amp;quot;+2^8&amp;quot;,510.509,4.28246e+09],[&amp;quot;+2^9&amp;quot;,1022.02,8.57331e+09],[&amp;quot;+2^10&amp;quot;,2045.04,1.7155e+10],[&amp;quot;+2^11&amp;quot;,4091.07,3.43184e+10],[&amp;quot;+2^12&amp;quot;,8183.15,6.86452e+10],[&amp;quot;+2^13&amp;quot;,16367.3,1.37299e+11],[&amp;quot;+2^14&amp;quot;,32735.6,2.74606e+11],[&amp;quot;+2^15&amp;quot;,65472.2,5.4922e+11],[&amp;quot;+2^16&amp;quot;,130945,1.09845e+12],[&amp;quot;+2^17&amp;quot;,261892,2.19691e+12],[&amp;quot;+2^18&amp;quot;,523784,4.39382e+12],[&amp;quot;+2^19&amp;quot;,1.04757e+06,8.78765e+12],[&amp;quot;+2^20&amp;quot;,2.09514e+06,1.75753e+13],[&amp;quot;+2^21&amp;quot;,4.19028e+06,3.51506e+13],[&amp;quot;+2^22&amp;quot;,8.38056e+06,7.03013e+13],[&amp;quot;+2^23&amp;quot;,1.67611e+07,1.40603e+14],[&amp;quot;+2^24&amp;quot;,3.35223e+07,2.81205e+14],[&amp;quot;+2^25&amp;quot;,6.70445e+07,5.6241e+14],[&amp;quot;+2^26&amp;quot;,1.34089e+08,1.12482e+15],[&amp;quot;+2^27&amp;quot;,2.68178e+08,2.24964e+15],[&amp;quot;+2^28&amp;quot;,5.36356e+08,4.49928e+15],[&amp;quot;+2^29&amp;quot;,1.07271e+09,8.99856e+15],[&amp;quot;+2^30&amp;quot;,2.14542e+09,1.79971e+16],[&amp;quot;+2^31&amp;quot;,4.29085e+09,3.59943e+16],[&amp;quot;+2^32&amp;quot;,8.58993e+09,7.20575e+16],[&amp;quot;+2^33&amp;quot;,1.71799e+10,1.44115e+17],[&amp;quot;+2^34&amp;quot;,3.43597e+10,2.8823e+17],[&amp;quot;+2^35&amp;quot;,6.87194e+10,5.7646e+17],[&amp;quot;+2^36&amp;quot;,1.37439e+11,1.15292e+18],[&amp;quot;+2^37&amp;quot;,2.74878e+11,2.30584e+18],[&amp;quot;+2^38&amp;quot;,5.49755e+11,4.61168e+18],[&amp;quot;+2^39&amp;quot;,1.09951e+12,9.22336e+18],[&amp;quot;+2^40&amp;quot;,2.19902e+12,1.84467e+19],[&amp;quot;+2^41&amp;quot;,4.39804e+12,3.68935e+19],[&amp;quot;+2^42&amp;quot;,8.79608e+12,7.37869e+19],[&amp;quot;+2^43&amp;quot;,1.75922e+13,1.47574e+20],[&amp;quot;+2^44&amp;quot;,3.51843e+13,2.95148e+20],[&amp;quot;+2^45&amp;quot;,7.03687e+13,5.90295e+20],[&amp;quot;+2^46&amp;quot;,1.40737e+14,1.18059e+21],[&amp;quot;+2^47&amp;quot;,2.81475e+14,2.36118e+21],[&amp;quot;+2^48&amp;quot;,5.62949e+14,4.72236e+21],[&amp;quot;+2^49&amp;quot;,1.1259e+15,9.44472e+21],[&amp;quot;+2^50&amp;quot;,2.2518e+15,1.88894e+22],[&amp;quot;+2^51&amp;quot;,4.5036e+15,3.77789e+22],[&amp;quot;+2^52&amp;quot;,9.00719e+15,7.55578e+22],[&amp;quot;+2^53&amp;quot;,1.80144e+16,1.51116e+23],[&amp;quot;+2^54&amp;quot;,3.60288e+16,3.02231e+23],[&amp;quot;+2^55&amp;quot;,7.20575e+16,6.04462e+23],[&amp;quot;+2^56&amp;quot;,1.44115e+17,1.20892e+24],[&amp;quot;+2^57&amp;quot;,2.8823e+17,2.41785e+24],[&amp;quot;+2^58&amp;quot;,5.7646e+17,4.8357e+24],[&amp;quot;+2^59&amp;quot;,1.15292e+18,9.6714e+24],[&amp;quot;+2^60&amp;quot;,2.30584e+18,1.93428e+25],[&amp;quot;+2^61&amp;quot;,4.61168e+18,3.86856e+25],[&amp;quot;+2^62&amp;quot;,9.22336e+18,7.73712e+25],[&amp;quot;+2^63&amp;quot;,1.84467e+19,1.54742e+26],[&amp;quot;+2^64&amp;quot;,0,0],[&amp;quot;+2^65&amp;quot;,0,0],[&amp;quot;+2^66&amp;quot;,0,0],[&amp;quot;+2^67&amp;quot;,0,0],[&amp;quot;+2^68&amp;quot;,0,0],[&amp;quot;+2^69&amp;quot;,0,0],[&amp;quot;+2^70&amp;quot;,0,0],[&amp;quot;+2^71&amp;quot;,0,0],[&amp;quot;+2^72&amp;quot;,0,0],[&amp;quot;+2^73&amp;quot;,0,0],[&amp;quot;+2^74&amp;quot;,0,0],[&amp;quot;+2^75&amp;quot;,0,0],[&amp;quot;+2^76&amp;quot;,0,0],[&amp;quot;+2^77&amp;quot;,0,0],[&amp;quot;+2^78&amp;quot;,0,0],[&amp;quot;+2^79&amp;quot;,0,0],[&amp;quot;+2^80&amp;quot;,0,0],[&amp;quot;+2^81&amp;quot;,0,0],[&amp;quot;+2^82&amp;quot;,0,0],[&amp;quot;+2^83&amp;quot;,0,0],[&amp;quot;+2^84&amp;quot;,0,0],[&amp;quot;+2^85&amp;quot;,0,0],[&amp;quot;+2^86&amp;quot;,0,0],[&amp;quot;+2^87&amp;quot;,0,0],[&amp;quot;+2^88&amp;quot;,0,0],[&amp;quot;+2^89&amp;quot;,0,0],[&amp;quot;+2^90&amp;quot;,0,0],[&amp;quot;+2^91&amp;quot;,0,0],[&amp;quot;+2^92&amp;quot;,0,0],[&amp;quot;+2^93&amp;quot;,0,0],[&amp;quot;+2^94&amp;quot;,0,0],[&amp;quot;+2^95&amp;quot;,0,0],[&amp;quot;+2^96&amp;quot;,0,0],[&amp;quot;+2^97&amp;quot;,0,0],[&amp;quot;+2^98&amp;quot;,0,0],[&amp;quot;+2^99&amp;quot;,0,0],[&amp;quot;+2^100&amp;quot;,0,0],[&amp;quot;+2^101&amp;quot;,0,0],[&amp;quot;+2^102&amp;quot;,0,0],[&amp;quot;+2^103&amp;quot;,0,0],[&amp;quot;+2^104&amp;quot;,0,0],[&amp;quot;+2^105&amp;quot;,0,0],[&amp;quot;+2^106&amp;quot;,0,0],[&amp;quot;+2^107&amp;quot;,0,0],[&amp;quot;+2^108&amp;quot;,0,0],[&amp;quot;+2^109&amp;quot;,0,0],[&amp;quot;+2^110&amp;quot;,0,0],[&amp;quot;+2^111&amp;quot;,0,0],[&amp;quot;+2^112&amp;quot;,0,0],[&amp;quot;+2^113&amp;quot;,0,0],[&amp;quot;+2^114&amp;quot;,0,0],[&amp;quot;+2^115&amp;quot;,0,0],[&amp;quot;+2^116&amp;quot;,0,0],[&amp;quot;+2^117&amp;quot;,0,0],[&amp;quot;+2^118&amp;quot;,0,0],[&amp;quot;+2^119&amp;quot;,0,0],[&amp;quot;+2^120&amp;quot;,0,0],[&amp;quot;+2^121&amp;quot;,0,0],[&amp;quot;+2^122&amp;quot;,0,0],[&amp;quot;+2^123&amp;quot;,0,0],[&amp;quot;+2^124&amp;quot;,0,0],[&amp;quot;+2^125&amp;quot;,0,0],[&amp;quot;+2^126&amp;quot;,0,0],[&amp;quot;+2^127&amp;quot;,0,0] ]&lt;br /&gt;
 }}&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Key observation:&#039;&#039;&#039;  &lt;br /&gt;
The Taylor approximation is highly accurate near zero but diverges extremely fast outside its convergence region.  &lt;br /&gt;
This makes it unsuitable as a standalone &amp;lt;code&amp;gt;tanh&amp;lt;/code&amp;gt; implementation, but very effective as a local approximation when combined with range-based dispatching.&lt;br /&gt;
&lt;br /&gt;
== Localized Approximation Functions ==&lt;br /&gt;
&lt;br /&gt;
Localized approximation functions are designed to provide highly accurate and computationally efficient evaluations of &amp;lt;code&amp;gt;tanh(x)&amp;lt;/code&amp;gt; within a restricted input range. &lt;br /&gt;
They are typically used as building blocks in range-based dispatch strategies, where different approximations are selected depending on the magnitude of the input value.&lt;br /&gt;
&lt;br /&gt;
By limiting the valid input range, these approximations can achieve very low ULP error with simple polynomial or rational expressions, avoiding expensive transcendental functions.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
!Id&lt;br /&gt;
!Formula&lt;br /&gt;
!ULP &amp;lt;= 2.0&lt;br /&gt;
!NaN&lt;br /&gt;
|-&lt;br /&gt;
|signum&lt;br /&gt;
|&amp;lt;math&amp;gt;\sgn{x}&amp;lt;/math&amp;gt;&lt;br /&gt;
| -inf &amp;lt; x &amp;lt; -8.31763&lt;br /&gt;
8.31763 &amp;lt; x &amp;lt; inf&lt;br /&gt;
| -&lt;br /&gt;
|-&lt;br /&gt;
|poly1&lt;br /&gt;
|&amp;lt;math&amp;gt;x&amp;lt;/math&amp;gt;&lt;br /&gt;
| -0.000704229 &amp;lt; x &amp;lt; 0.000704229&lt;br /&gt;
| -&lt;br /&gt;
|-&lt;br /&gt;
|poly3&lt;br /&gt;
|&amp;lt;math&amp;gt;x - \frac{1}{3} x^3&amp;lt;/math&amp;gt;&lt;br /&gt;
| -0.0291781 &amp;lt; x &amp;lt; 0.0291781 &lt;br /&gt;
| -&lt;br /&gt;
|-&lt;br /&gt;
|lambert7&lt;br /&gt;
|&amp;lt;math&amp;gt;\frac{x \cdot (135135 + x^2 \cdot (17325 + x^2 \cdot (378 + x^2)))}{135135 + x^2 \cdot (62370 + x^2 \cdot (3150 + 28 \cdot x^2))}&lt;br /&gt;
&amp;lt;/math&amp;gt;&lt;br /&gt;
| -0.000947416 &amp;lt; x &amp;lt; 0.000947207&lt;br /&gt;
| -inf &amp;lt; x &amp;lt; -1.51629e+06&lt;br /&gt;
1.51629e+06 &amp;lt; x &amp;lt; inf&lt;br /&gt;
|-&lt;br /&gt;
|pade&lt;br /&gt;
|&amp;lt;math&amp;gt;x + x \cdot \frac{x^2 \cdot \left( p_1 \cdot x^2 + p_0 \right)}{\left( x^2 + q_1 \right) \cdot x^2 + q_0} &lt;br /&gt;
&lt;br /&gt;
&amp;lt;/math&amp;gt; &amp;lt;math&amp;gt;\begin{align}&lt;br /&gt;
p_0 &amp;amp;= -0.2059432032 \\&lt;br /&gt;
p_1 &amp;amp;= -0.0009577527 \\&lt;br /&gt;
q_0 &amp;amp;= 0.6178299136 \\ &lt;br /&gt;
q_1 &amp;amp;= 0.25&lt;br /&gt;
\end{align}&lt;br /&gt;
&amp;lt;/math&amp;gt;&lt;br /&gt;
| -0.0016973 &amp;lt; x &amp;lt; 0.0016973 &lt;br /&gt;
| -inf &amp;lt; x &amp;lt; -6.14896e+18&lt;br /&gt;
6.14896e+18 &amp;lt; x &amp;lt; inf&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
The functions listed above are not intended to be used independently over the full input domain. &lt;br /&gt;
Instead, they are commonly combined with explicit range checks and sign handling to form a complete &amp;lt;code&amp;gt;tanh&amp;lt;/code&amp;gt; implementation.&lt;br /&gt;
&lt;br /&gt;
For example, low-order polynomials are well suited for very small &amp;lt;math&amp;gt;|x|&amp;lt;/math&amp;gt;, while rational or higher-order approximations extend the usable range before switching to exponential-based formulations or saturation.&lt;br /&gt;
&lt;br /&gt;
==Recommended Implementation Strategy==&lt;br /&gt;
A robust and high-performance &amp;lt;code&amp;gt;tanh(x)&amp;lt;/code&amp;gt; implementation typically combines multiple techniques, each optimized for a specific input range. No single formulation provides optimal accuracy and performance across the entire floating-point domain.&lt;br /&gt;
&lt;br /&gt;
A common strategy is based on range-dependent dispatching:&lt;br /&gt;
*&#039;&#039;&#039;Sign handling&#039;&#039;&#039;&lt;br /&gt;
  Use the odd symmetry of &amp;lt;code&amp;gt;tanh&amp;lt;/code&amp;gt; (&amp;lt;math&amp;gt;\tanh(-x) = -\tanh(x)&amp;lt;/math&amp;gt;) to reduce the implementation to non-negative inputs.*&#039;&#039;&#039;Very small inputs (&amp;lt;math&amp;gt;|x| \ll 1&amp;lt;/math&amp;gt;)&#039;&#039;&#039;&lt;br /&gt;
  Use a low-order polynomial or Taylor approximation, exploiting &amp;lt;math&amp;gt;\tanh(x) \approx x&amp;lt;/math&amp;gt; to avoid cancellation and expensive transcendental functions.&lt;br /&gt;
*&#039;&#039;&#039;Small to medium inputs&#039;&#039;&#039;&lt;br /&gt;
  Apply a localized polynomial or rational approximation with bounded ULP error.&lt;br /&gt;
*&#039;&#039;&#039;Medium to large inputs&#039;&#039;&#039;&lt;br /&gt;
  Use an &amp;lt;code&amp;gt;expm1&amp;lt;/code&amp;gt;-based formulation to maintain numerical stability while avoiding catastrophic cancellation.&lt;br /&gt;
*&#039;&#039;&#039;Very large inputs&#039;&#039;&#039;&lt;br /&gt;
  Directly return &amp;lt;code&amp;gt;±1&amp;lt;/code&amp;gt;, as &amp;lt;code&amp;gt;tanh(x)&amp;lt;/code&amp;gt; saturates rapidly and additional computation provides no meaningful benefit.&lt;br /&gt;
This hybrid approach minimizes ULP error, avoids NaN generation, and allows fine-grained control over the accuracy–performance trade-off. It is widely used in optimized math libraries and hardware-accelerated implementations.&lt;br /&gt;
== External Links ==&lt;br /&gt;
&lt;br /&gt;
* https://yaikhom.com/2020-04-28-localised-approximation-of-hyperbolic-tangents.html#mjx-eqn-eqn7-th%20degree%20lambert%20approximant&lt;br /&gt;
*https://github.com/llvm/llvm-project/blob/main/libc/src/math/generic/tanhf.cpp&lt;br /&gt;
*https://forums.developer.nvidia.com/t/hardware-accelerated-tanh-on-turing/173291&lt;br /&gt;
[[Category:Numerical Precision]]&lt;/div&gt;</summary>
		<author><name>Timo.stripf</name></author>
	</entry>
	<entry>
		<id>https://www.emmtrix.com/w139/index.php?title=tanh_Software_Implementation&amp;diff=2935</id>
		<title>tanh Software Implementation</title>
		<link rel="alternate" type="text/html" href="https://www.emmtrix.com/w139/index.php?title=tanh_Software_Implementation&amp;diff=2935"/>
		<updated>2026-02-04T12:09:20Z</updated>

		<summary type="html">&lt;p&gt;Timo.stripf: /* Localized Approximation Functions */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;[[File:Hyperbolic Tangent.svg|thumb]]&lt;br /&gt;
The hyperbolic tangent function, commonly referred to as &amp;lt;code&amp;gt;tanh&amp;lt;/code&amp;gt;, is a mathematical function that maps real numbers to the range &amp;lt;math&amp;gt;(-1, 1)&amp;lt;/math&amp;gt;. It is defined as:&lt;br /&gt;
&lt;br /&gt;
:&amp;lt;math&amp;gt;\tanh(x) = \frac{e^x - e^{-x}}{e^x + e^{-x}}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
From a software implementation perspective, &amp;lt;code&amp;gt;tanh&amp;lt;/code&amp;gt; is a numerically challenging function due to the use of exponential functions, which can easily overflow for large inputs and suffer from catastrophic cancellation for very small values of &amp;lt;math&amp;gt;x&amp;lt;/math&amp;gt;. These issues are particularly relevant in single-precision floating-point (&amp;lt;code&amp;gt;float32&amp;lt;/code&amp;gt;) implementations, where both accuracy and performance are critical.&lt;br /&gt;
&lt;br /&gt;
In machine learning and deep learning, &amp;lt;code&amp;gt;tanh&amp;lt;/code&amp;gt; is widely used as an activation function because it introduces non-linearity and produces outputs centered around zero, which can improve gradient-based optimization. As a result, &amp;lt;code&amp;gt;tanh&amp;lt;/code&amp;gt; is often evaluated billions of times during training and inference, making efficient and numerically stable implementations essential.&lt;br /&gt;
&lt;br /&gt;
This article focuses on practical software implementations of &amp;lt;code&amp;gt;tanh&amp;lt;/code&amp;gt;, analyzing different computational formulations with respect to floating-point accuracy, ULP error, overflow behavior, and NaN generation. The goal is to provide a foundation for designing robust &amp;lt;code&amp;gt;tanh&amp;lt;/code&amp;gt; implementations suitable for performance-critical environments such as numerical libraries, embedded systems, and AI workloads.&lt;br /&gt;
&lt;br /&gt;
== Problem Overview ==&lt;br /&gt;
The hyperbolic tangent function poses several challenges when implemented in finite-precision floating-point arithmetic. While the mathematical definition is simple, direct translations into software often suffer from numerical instability and performance issues.&lt;br /&gt;
&lt;br /&gt;
A primary challenge is the use of exponential functions. For large input values, &amp;lt;code&amp;gt;exp(x)&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;exp(-x)&amp;lt;/code&amp;gt; can overflow in single-precision floating-point arithmetic, leading to infinities and, in some formulations, NaN results. Avoiding overflow therefore requires careful reformulation or explicit range handling.&lt;br /&gt;
&lt;br /&gt;
For very small input values, catastrophic cancellation becomes the dominant issue. Expressions such as &amp;lt;math&amp;gt;e^x - e^{-x}&amp;lt;/math&amp;gt; involve the subtraction of nearly equal numbers, causing a severe loss of significant bits and large ULP errors. In these regions, naive implementations may return zero or highly inaccurate results even though &amp;lt;math&amp;gt;\tanh(x) \approx x&amp;lt;/math&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
In addition to accuracy concerns, performance is a critical factor. In many applications, especially machine learning and numerical simulations, &amp;lt;code&amp;gt;tanh&amp;lt;/code&amp;gt; is evaluated extremely frequently. This makes it necessary to balance numerical robustness with the cost of transcendental function calls, branch complexity, and instruction-level efficiency.&lt;br /&gt;
&lt;br /&gt;
==Direct Exponential Formulations==&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+&lt;br /&gt;
!Id&lt;br /&gt;
!Formula&lt;br /&gt;
!Highest ULP Error&lt;br /&gt;
!ULP &amp;lt;= 2.0&lt;br /&gt;
!NaN&lt;br /&gt;
|-&lt;br /&gt;
|exp_v1&lt;br /&gt;
|&amp;lt;math&amp;gt;\frac{e^x - e^{-x}}{e^x + e^{-x}}&amp;lt;/math&amp;gt;&lt;br /&gt;
|x &amp;lt; 0: 8.55638e+08 at -2.98023e-08 with 0x0p+0 instead of -0x1p-25&lt;br /&gt;
&lt;br /&gt;
x &amp;gt; 0: 8.55638e+08 at 2.98023e-08 with 0x0p+0 instead of 0x1p-25&lt;br /&gt;
| -88.7246 &amp;lt; x &amp;lt; -7.14197&lt;br /&gt;
7.14197 &amp;lt; x &amp;lt; 88.7246&lt;br /&gt;
| -inf &amp;lt; x &amp;lt; -88.7246&lt;br /&gt;
88.7246 &amp;lt; x &amp;lt; inf&lt;br /&gt;
|-&lt;br /&gt;
|exp_v2&lt;br /&gt;
|&amp;lt;math&amp;gt;\frac{e^{2x} - 1}{e^{2x} + 1}&amp;lt;/math&amp;gt;&lt;br /&gt;
|x &amp;lt; 0: 8.47249e+08 at -1.49012e-08 with 0x0p+0 instead of -0x1p-26&lt;br /&gt;
x &amp;gt; 0: 8.55638e+08 at 2.98023e-08 with 0x0p+0 instead of 0x1.fffffep-26&lt;br /&gt;
| -inf &amp;lt; x &amp;lt; -0.547821&lt;br /&gt;
0.254822 &amp;lt; x &amp;lt; 8.31787&lt;br /&gt;
&lt;br /&gt;
8.38379 &amp;lt; x &amp;lt; 44.3623&lt;br /&gt;
|44.3623 &amp;lt; x &amp;lt; inf&lt;br /&gt;
|-&lt;br /&gt;
|exp_v3&lt;br /&gt;
|&amp;lt;math&amp;gt;1 - \frac{2}{e^{2x} + 1}&amp;lt;/math&amp;gt;&lt;br /&gt;
|x &amp;lt; 0: 8.59832e+08 at -4.47035e-08 with 0x0p+0 instead of -0x1.8p-25&lt;br /&gt;
x &amp;gt; 0: 8.68221e+08 at 8.9407e-08 with 0x0p+0 instead of 0x1.7ffffcp-24&lt;br /&gt;
| -inf &amp;lt; x &amp;lt; -6.26233&lt;br /&gt;
0.346382 &amp;lt; x &amp;lt; inf&lt;br /&gt;
| -&lt;br /&gt;
|-&lt;br /&gt;
|expm1&lt;br /&gt;
|&amp;lt;math&amp;gt;\frac{expm1(2x)}{expm1(2x) + 2}&amp;lt;/math&amp;gt; &lt;br /&gt;
|x &amp;lt; 0: 2.49585 at -3.95627 with -0x1.ffa00ap-1 instead of -0x1.ffa00ep-1&lt;br /&gt;
x &amp;gt; 0: 2.41657 at 0.0155837 with 0x1.fe9b66p-7 instead of 0x1.fe9b62p-7&lt;br /&gt;
| -inf &amp;lt; x &amp;lt; -5.57227&lt;br /&gt;
-0.42981 &amp;lt; x &amp;lt; 0.00777948&lt;br /&gt;
&lt;br /&gt;
0.125351 &amp;lt; x &amp;lt; 44.3623&lt;br /&gt;
|44.3623 &amp;lt; x &amp;lt; inf&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
The first equation is the definition of the hyperbolic tangent function. This equation requires two exponentials with different values. Since exponentials are expensive to compute, we can use the second equation to reduce the number of exponentials to one.&lt;br /&gt;
&lt;br /&gt;
The second equation has the drawback that both the numerator and the denominator can become infinity for large values of x which results in NaN. That happens for &amp;lt;math&amp;gt;|x| &amp;gt; \log(FLT\_MAX)/2 \approx 44&amp;lt;/math&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
The third equation solves this problem but has a slightly higher error for &amp;lt;math&amp;gt;x \approx 2^{-25}&amp;lt;/math&amp;gt;. In the following graph you can see the error chart for equation 3. The error charts for the other two equations are very similar. All three equations have an error of up to 16 mio ULPs for &amp;lt;math&amp;gt;x &amp;lt; 2^{-5}&amp;lt;/math&amp;gt; which means that up to 24 bits could be wrong.&lt;br /&gt;
&lt;br /&gt;
=== exp_v2 ===&lt;br /&gt;
&lt;br /&gt;
The following error graph shows the ULP error of the &amp;lt;code&amp;gt;exp_v2&amp;lt;/code&amp;gt; formulation across the full &amp;lt;code&amp;gt;float32&amp;lt;/code&amp;gt; input range. &lt;br /&gt;
This formulation reduces the number of exponential evaluations to one, but is particularly sensitive to catastrophic cancellation for very small input values and to overflow for large &amp;lt;math&amp;gt;|x|&amp;lt;/math&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
{{#widget:FloatErrorGraph|id=tanhf_exp_v2&lt;br /&gt;
  |data=[ [&amp;quot;-2^127&amp;quot;,0,1.17549e-38],[&amp;quot;-2^126&amp;quot;,0,1.17549e-38],[&amp;quot;-2^125&amp;quot;,0,1.17549e-38],[&amp;quot;-2^124&amp;quot;,0,1.17549e-38],[&amp;quot;-2^123&amp;quot;,0,1.17549e-38],[&amp;quot;-2^122&amp;quot;,0,1.17549e-38],[&amp;quot;-2^121&amp;quot;,0,1.17549e-38],[&amp;quot;-2^120&amp;quot;,0,1.17549e-38],[&amp;quot;-2^119&amp;quot;,0,1.17549e-38],[&amp;quot;-2^118&amp;quot;,0,1.17549e-38],[&amp;quot;-2^117&amp;quot;,0,1.17549e-38],[&amp;quot;-2^116&amp;quot;,0,1.17549e-38],[&amp;quot;-2^115&amp;quot;,0,1.17549e-38],[&amp;quot;-2^114&amp;quot;,0,1.17549e-38],[&amp;quot;-2^113&amp;quot;,0,1.17549e-38],[&amp;quot;-2^112&amp;quot;,0,1.17549e-38],[&amp;quot;-2^111&amp;quot;,0,1.17549e-38],[&amp;quot;-2^110&amp;quot;,0,1.17549e-38],[&amp;quot;-2^109&amp;quot;,0,1.17549e-38],[&amp;quot;-2^108&amp;quot;,0,1.17549e-38],[&amp;quot;-2^107&amp;quot;,0,1.17549e-38],[&amp;quot;-2^106&amp;quot;,0,1.17549e-38],[&amp;quot;-2^105&amp;quot;,0,1.17549e-38],[&amp;quot;-2^104&amp;quot;,0,1.17549e-38],[&amp;quot;-2^103&amp;quot;,0,1.17549e-38],[&amp;quot;-2^102&amp;quot;,0,1.17549e-38],[&amp;quot;-2^101&amp;quot;,0,1.17549e-38],[&amp;quot;-2^100&amp;quot;,0,1.17549e-38],[&amp;quot;-2^99&amp;quot;,0,1.17549e-38],[&amp;quot;-2^98&amp;quot;,0,1.17549e-38],[&amp;quot;-2^97&amp;quot;,0,1.17549e-38],[&amp;quot;-2^96&amp;quot;,0,1.17549e-38],[&amp;quot;-2^95&amp;quot;,0,1.17549e-38],[&amp;quot;-2^94&amp;quot;,0,1.17549e-38],[&amp;quot;-2^93&amp;quot;,0,1.17549e-38],[&amp;quot;-2^92&amp;quot;,0,1.17549e-38],[&amp;quot;-2^91&amp;quot;,0,1.17549e-38],[&amp;quot;-2^90&amp;quot;,0,1.17549e-38],[&amp;quot;-2^89&amp;quot;,0,1.17549e-38],[&amp;quot;-2^88&amp;quot;,0,1.17549e-38],[&amp;quot;-2^87&amp;quot;,0,1.17549e-38],[&amp;quot;-2^86&amp;quot;,0,1.17549e-38],[&amp;quot;-2^85&amp;quot;,0,1.17549e-38],[&amp;quot;-2^84&amp;quot;,0,1.17549e-38],[&amp;quot;-2^83&amp;quot;,0,1.17549e-38],[&amp;quot;-2^82&amp;quot;,0,1.17549e-38],[&amp;quot;-2^81&amp;quot;,0,1.17549e-38],[&amp;quot;-2^80&amp;quot;,0,1.17549e-38],[&amp;quot;-2^79&amp;quot;,0,1.17549e-38],[&amp;quot;-2^78&amp;quot;,0,1.17549e-38],[&amp;quot;-2^77&amp;quot;,0,1.17549e-38],[&amp;quot;-2^76&amp;quot;,0,1.17549e-38],[&amp;quot;-2^75&amp;quot;,0,1.17549e-38],[&amp;quot;-2^74&amp;quot;,0,1.17549e-38],[&amp;quot;-2^73&amp;quot;,0,1.17549e-38],[&amp;quot;-2^72&amp;quot;,0,1.17549e-38],[&amp;quot;-2^71&amp;quot;,0,1.17549e-38],[&amp;quot;-2^70&amp;quot;,0,1.17549e-38],[&amp;quot;-2^69&amp;quot;,0,1.17549e-38],[&amp;quot;-2^68&amp;quot;,0,1.17549e-38],[&amp;quot;-2^67&amp;quot;,0,1.17549e-38],[&amp;quot;-2^66&amp;quot;,0,1.17549e-38],[&amp;quot;-2^65&amp;quot;,0,1.17549e-38],[&amp;quot;-2^64&amp;quot;,0,1.17549e-38],[&amp;quot;-2^63&amp;quot;,0,1.17549e-38],[&amp;quot;-2^62&amp;quot;,0,1.17549e-38],[&amp;quot;-2^61&amp;quot;,0,1.17549e-38],[&amp;quot;-2^60&amp;quot;,0,1.17549e-38],[&amp;quot;-2^59&amp;quot;,0,1.17549e-38],[&amp;quot;-2^58&amp;quot;,0,1.17549e-38],[&amp;quot;-2^57&amp;quot;,0,1.17549e-38],[&amp;quot;-2^56&amp;quot;,0,1.17549e-38],[&amp;quot;-2^55&amp;quot;,0,1.17549e-38],[&amp;quot;-2^54&amp;quot;,0,1.17549e-38],[&amp;quot;-2^53&amp;quot;,0,1.17549e-38],[&amp;quot;-2^52&amp;quot;,0,1.17549e-38],[&amp;quot;-2^51&amp;quot;,0,1.17549e-38],[&amp;quot;-2^50&amp;quot;,0,1.17549e-38],[&amp;quot;-2^49&amp;quot;,0,1.17549e-38],[&amp;quot;-2^48&amp;quot;,0,1.17549e-38],[&amp;quot;-2^47&amp;quot;,0,1.17549e-38],[&amp;quot;-2^46&amp;quot;,0,1.17549e-38],[&amp;quot;-2^45&amp;quot;,0,1.17549e-38],[&amp;quot;-2^44&amp;quot;,0,1.17549e-38],[&amp;quot;-2^43&amp;quot;,0,1.17549e-38],[&amp;quot;-2^42&amp;quot;,0,1.17549e-38],[&amp;quot;-2^41&amp;quot;,0,1.17549e-38],[&amp;quot;-2^40&amp;quot;,0,1.17549e-38],[&amp;quot;-2^39&amp;quot;,0,1.17549e-38],[&amp;quot;-2^38&amp;quot;,0,1.17549e-38],[&amp;quot;-2^37&amp;quot;,0,1.17549e-38],[&amp;quot;-2^36&amp;quot;,0,1.17549e-38],[&amp;quot;-2^35&amp;quot;,0,1.17549e-38],[&amp;quot;-2^34&amp;quot;,0,1.17549e-38],[&amp;quot;-2^33&amp;quot;,0,1.17549e-38],[&amp;quot;-2^32&amp;quot;,0,1.17549e-38],[&amp;quot;-2^31&amp;quot;,0,1.17549e-38],[&amp;quot;-2^30&amp;quot;,0,1.17549e-38],[&amp;quot;-2^29&amp;quot;,0,1.17549e-38],[&amp;quot;-2^28&amp;quot;,0,1.17549e-38],[&amp;quot;-2^27&amp;quot;,0,1.17549e-38],[&amp;quot;-2^26&amp;quot;,0,1.17549e-38],[&amp;quot;-2^25&amp;quot;,0,1.17549e-38],[&amp;quot;-2^24&amp;quot;,0,1.17549e-38],[&amp;quot;-2^23&amp;quot;,0,1.17549e-38],[&amp;quot;-2^22&amp;quot;,0,1.17549e-38],[&amp;quot;-2^21&amp;quot;,0,1.17549e-38],[&amp;quot;-2^20&amp;quot;,0,1.17549e-38],[&amp;quot;-2^19&amp;quot;,0,1.17549e-38],[&amp;quot;-2^18&amp;quot;,0,1.17549e-38],[&amp;quot;-2^17&amp;quot;,0,1.17549e-38],[&amp;quot;-2^16&amp;quot;,0,1.17549e-38],[&amp;quot;-2^15&amp;quot;,0,1.17549e-38],[&amp;quot;-2^14&amp;quot;,0,1.17549e-38],[&amp;quot;-2^13&amp;quot;,0,1.17549e-38],[&amp;quot;-2^12&amp;quot;,0,1.17549e-38],[&amp;quot;-2^11&amp;quot;,0,1.17549e-38],[&amp;quot;-2^10&amp;quot;,0,1.17549e-38],[&amp;quot;-2^9&amp;quot;,0,1.17549e-38],[&amp;quot;-2^8&amp;quot;,0,1.17549e-38],[&amp;quot;-2^7&amp;quot;,0,1.17549e-38],[&amp;quot;-2^6&amp;quot;,0,1.17549e-38],[&amp;quot;-2^5&amp;quot;,0,1.17549e-38],[&amp;quot;-2^4&amp;quot;,2.53131e-14,4.24683e-07],[&amp;quot;-2^3&amp;quot;,5.95888e-08,0.999734],[&amp;quot;-2^2&amp;quot;,8.86696e-08,1.48763],[&amp;quot;-2^1&amp;quot;,8.91609e-08,1.49587],[&amp;quot;-2^0&amp;quot;,8.73743e-08,1.4659],[&amp;quot;-2^-1&amp;quot;,8.65382e-08,2.13222],[&amp;quot;-2^-2&amp;quot;,6.18481e-08,2.61428],[&amp;quot;-2^-3&amp;quot;,3.88071e-08,3.08594],[&amp;quot;-2^-4&amp;quot;,2.6234e-08,4.23594],[&amp;quot;-2^-5&amp;quot;,2.02799e-08,8.19726],[&amp;quot;-2^-6&amp;quot;,1.75183e-08,12.4495],[&amp;quot;-2^-7&amp;quot;,1.61937e-08,17.3878],[&amp;quot;-2^-8&amp;quot;,1.5566e-08,33.4278],[&amp;quot;-2^-9&amp;quot;,1.51688e-08,65.1495],[&amp;quot;-2^-10&amp;quot;,1.50281e-08,129.09],[&amp;quot;-2^-11&amp;quot;,1.49613e-08,257.033],[&amp;quot;-2^-12&amp;quot;,1.49296e-08,512.977],[&amp;quot;-2^-13&amp;quot;,1.49174e-08,1025.12],[&amp;quot;-2^-14&amp;quot;,1.4909e-08,2049.08],[&amp;quot;-2^-15&amp;quot;,1.49049e-08,4097.02],[&amp;quot;-2^-16&amp;quot;,1.4903e-08,8193],[&amp;quot;-2^-17&amp;quot;,1.49021e-08,16385],[&amp;quot;-2^-18&amp;quot;,1.48862e-08,32735],[&amp;quot;-2^-19&amp;quot;,1.48973e-08,65519],[&amp;quot;-2^-20&amp;quot;,1.49001e-08,131063],[&amp;quot;-2^-21&amp;quot;,1.49009e-08,262139],[&amp;quot;-2^-22&amp;quot;,1.49011e-08,524285],[&amp;quot;-2^-23&amp;quot;,1.49011e-08,1.04857e+06],[&amp;quot;-2^-24&amp;quot;,1.49011e-08,2.09715e+06],[&amp;quot;-2^-25&amp;quot;,1.49012e-08,4.1943e+06],[&amp;quot;-2^-26&amp;quot;,1.49007e-08,8.38835e+06],[&amp;quot;-2^-27&amp;quot;,1.49012e-08,8.47249e+08],[&amp;quot;-2^-28&amp;quot;,7.45058e-09,8.38861e+08],[&amp;quot;-2^-29&amp;quot;,3.72529e-09,8.30472e+08],[&amp;quot;-2^-30&amp;quot;,1.86265e-09,8.22084e+08],[&amp;quot;-2^-31&amp;quot;,9.31323e-10,8.13695e+08],[&amp;quot;-2^-32&amp;quot;,4.65661e-10,8.05306e+08],[&amp;quot;-2^-33&amp;quot;,2.32831e-10,7.96918e+08],[&amp;quot;-2^-34&amp;quot;,1.16415e-10,7.88529e+08],[&amp;quot;-2^-35&amp;quot;,5.82077e-11,7.80141e+08],[&amp;quot;-2^-36&amp;quot;,2.91038e-11,7.71752e+08],[&amp;quot;-2^-37&amp;quot;,1.45519e-11,7.63363e+08],[&amp;quot;-2^-38&amp;quot;,7.27596e-12,7.54975e+08],[&amp;quot;-2^-39&amp;quot;,3.63798e-12,7.46586e+08],[&amp;quot;-2^-40&amp;quot;,1.81899e-12,7.38198e+08],[&amp;quot;-2^-41&amp;quot;,9.09495e-13,7.29809e+08],[&amp;quot;-2^-42&amp;quot;,4.54747e-13,7.2142e+08],[&amp;quot;-2^-43&amp;quot;,2.27374e-13,7.13032e+08],[&amp;quot;-2^-44&amp;quot;,1.13687e-13,7.04643e+08],[&amp;quot;-2^-45&amp;quot;,5.68434e-14,6.96254e+08],[&amp;quot;-2^-46&amp;quot;,2.84217e-14,6.87866e+08],[&amp;quot;-2^-47&amp;quot;,1.42109e-14,6.79477e+08],[&amp;quot;-2^-48&amp;quot;,7.10543e-15,6.71089e+08],[&amp;quot;-2^-49&amp;quot;,3.55271e-15,6.627e+08],[&amp;quot;-2^-50&amp;quot;,1.77636e-15,6.54311e+08],[&amp;quot;-2^-51&amp;quot;,8.88178e-16,6.45923e+08],[&amp;quot;-2^-52&amp;quot;,4.44089e-16,6.37534e+08],[&amp;quot;-2^-53&amp;quot;,2.22045e-16,6.29146e+08],[&amp;quot;-2^-54&amp;quot;,1.11022e-16,6.20757e+08],[&amp;quot;-2^-55&amp;quot;,5.55111e-17,6.12368e+08],[&amp;quot;-2^-56&amp;quot;,2.77556e-17,6.0398e+08],[&amp;quot;-2^-57&amp;quot;,1.38778e-17,5.95591e+08],[&amp;quot;-2^-58&amp;quot;,6.93889e-18,5.87203e+08],[&amp;quot;-2^-59&amp;quot;,3.46945e-18,5.78814e+08],[&amp;quot;-2^-60&amp;quot;,1.73472e-18,5.70425e+08],[&amp;quot;-2^-61&amp;quot;,8.67362e-19,5.62037e+08],[&amp;quot;-2^-62&amp;quot;,4.33681e-19,5.53648e+08],[&amp;quot;-2^-63&amp;quot;,2.1684e-19,5.4526e+08],[&amp;quot;-2^-64&amp;quot;,1.0842e-19,5.36871e+08],[&amp;quot;-2^-65&amp;quot;,5.42101e-20,5.28482e+08],[&amp;quot;-2^-66&amp;quot;,2.71051e-20,5.20094e+08],[&amp;quot;-2^-67&amp;quot;,1.35525e-20,5.11705e+08],[&amp;quot;-2^-68&amp;quot;,6.77626e-21,5.03316e+08],[&amp;quot;-2^-69&amp;quot;,3.38813e-21,4.94928e+08],[&amp;quot;-2^-70&amp;quot;,1.69407e-21,4.86539e+08],[&amp;quot;-2^-71&amp;quot;,8.47033e-22,4.78151e+08],[&amp;quot;-2^-72&amp;quot;,4.23516e-22,4.69762e+08],[&amp;quot;-2^-73&amp;quot;,2.11758e-22,4.61373e+08],[&amp;quot;-2^-74&amp;quot;,1.05879e-22,4.52985e+08],[&amp;quot;-2^-75&amp;quot;,5.29396e-23,4.44596e+08],[&amp;quot;-2^-76&amp;quot;,2.64698e-23,4.36208e+08],[&amp;quot;-2^-77&amp;quot;,1.32349e-23,4.27819e+08],[&amp;quot;-2^-78&amp;quot;,6.61744e-24,4.1943e+08],[&amp;quot;-2^-79&amp;quot;,3.30872e-24,4.11042e+08],[&amp;quot;-2^-80&amp;quot;,1.65436e-24,4.02653e+08],[&amp;quot;-2^-81&amp;quot;,8.27181e-25,3.94265e+08],[&amp;quot;-2^-82&amp;quot;,4.1359e-25,3.85876e+08],[&amp;quot;-2^-83&amp;quot;,2.06795e-25,3.77487e+08],[&amp;quot;-2^-84&amp;quot;,1.03398e-25,3.69099e+08],[&amp;quot;-2^-85&amp;quot;,5.16988e-26,3.6071e+08],[&amp;quot;-2^-86&amp;quot;,2.58494e-26,3.52322e+08],[&amp;quot;-2^-87&amp;quot;,1.29247e-26,3.43933e+08],[&amp;quot;-2^-88&amp;quot;,6.46235e-27,3.35544e+08],[&amp;quot;-2^-89&amp;quot;,3.23117e-27,3.27156e+08],[&amp;quot;-2^-90&amp;quot;,1.61559e-27,3.18767e+08],[&amp;quot;-2^-91&amp;quot;,8.07794e-28,3.10378e+08],[&amp;quot;-2^-92&amp;quot;,4.03897e-28,3.0199e+08],[&amp;quot;-2^-93&amp;quot;,2.01948e-28,2.93601e+08],[&amp;quot;-2^-94&amp;quot;,1.00974e-28,2.85213e+08],[&amp;quot;-2^-95&amp;quot;,5.04871e-29,2.76824e+08],[&amp;quot;-2^-96&amp;quot;,2.52435e-29,2.68435e+08],[&amp;quot;-2^-97&amp;quot;,1.26218e-29,2.60047e+08],[&amp;quot;-2^-98&amp;quot;,6.31089e-30,2.51658e+08],[&amp;quot;-2^-99&amp;quot;,3.15544e-30,2.4327e+08],[&amp;quot;-2^-100&amp;quot;,1.57772e-30,2.34881e+08],[&amp;quot;-2^-101&amp;quot;,7.88861e-31,2.26492e+08],[&amp;quot;-2^-102&amp;quot;,3.9443e-31,2.18104e+08],[&amp;quot;-2^-103&amp;quot;,1.97215e-31,2.09715e+08],[&amp;quot;-2^-104&amp;quot;,9.86076e-32,2.01327e+08],[&amp;quot;-2^-105&amp;quot;,4.93038e-32,1.92938e+08],[&amp;quot;-2^-106&amp;quot;,2.46519e-32,1.84549e+08],[&amp;quot;-2^-107&amp;quot;,1.2326e-32,1.76161e+08],[&amp;quot;-2^-108&amp;quot;,6.16298e-33,1.67772e+08],[&amp;quot;-2^-109&amp;quot;,3.08149e-33,1.59384e+08],[&amp;quot;-2^-110&amp;quot;,1.54074e-33,1.50995e+08],[&amp;quot;-2^-111&amp;quot;,7.70372e-34,1.42606e+08],[&amp;quot;-2^-112&amp;quot;,3.85186e-34,1.34218e+08],[&amp;quot;-2^-113&amp;quot;,1.92593e-34,1.25829e+08],[&amp;quot;-2^-114&amp;quot;,9.62965e-35,1.17441e+08],[&amp;quot;-2^-115&amp;quot;,4.81482e-35,1.09052e+08],[&amp;quot;-2^-116&amp;quot;,2.40741e-35,1.00663e+08],[&amp;quot;-2^-117&amp;quot;,1.20371e-35,9.22747e+07],[&amp;quot;-2^-118&amp;quot;,6.01853e-36,8.38861e+07],[&amp;quot;-2^-119&amp;quot;,3.00927e-36,7.54975e+07],[&amp;quot;-2^-120&amp;quot;,1.50463e-36,6.71089e+07],[&amp;quot;-2^-121&amp;quot;,7.52316e-37,5.87203e+07],[&amp;quot;-2^-122&amp;quot;,3.76158e-37,5.03316e+07],[&amp;quot;-2^-123&amp;quot;,1.88079e-37,4.1943e+07],[&amp;quot;-2^-124&amp;quot;,9.40395e-38,3.35544e+07],[&amp;quot;-2^-125&amp;quot;,4.70198e-38,2.51658e+07],[&amp;quot;-2^-126&amp;quot;,2.35099e-38,1.67772e+07],[&amp;quot;-SN&amp;quot;,1.17546e-38,8.38835e+06],[&amp;quot;0&amp;quot;,0,1.17549e-38],[&amp;quot;+SN&amp;quot;,1.17546e-38,8.38835e+06],[&amp;quot;+2^-126&amp;quot;,2.35095e-38,1.6777e+07],[&amp;quot;+2^-125&amp;quot;,4.70191e-38,2.51656e+07],[&amp;quot;+2^-124&amp;quot;,9.40381e-38,3.35542e+07],[&amp;quot;+2^-123&amp;quot;,1.88076e-37,4.19428e+07],[&amp;quot;+2^-122&amp;quot;,3.76152e-37,5.03314e+07],[&amp;quot;+2^-121&amp;quot;,7.52305e-37,5.872e+07],[&amp;quot;+2^-120&amp;quot;,1.50461e-36,6.71086e+07],[&amp;quot;+2^-119&amp;quot;,3.00922e-36,7.54972e+07],[&amp;quot;+2^-118&amp;quot;,6.01844e-36,8.38858e+07],[&amp;quot;+2^-117&amp;quot;,1.20369e-35,9.22744e+07],[&amp;quot;+2^-116&amp;quot;,2.40738e-35,1.00663e+08],[&amp;quot;+2^-115&amp;quot;,4.81475e-35,1.09052e+08],[&amp;quot;+2^-114&amp;quot;,9.6295e-35,1.1744e+08],[&amp;quot;+2^-113&amp;quot;,1.9259e-34,1.25829e+08],[&amp;quot;+2^-112&amp;quot;,3.8518e-34,1.34217e+08],[&amp;quot;+2^-111&amp;quot;,7.7036e-34,1.42606e+08],[&amp;quot;+2^-110&amp;quot;,1.54072e-33,1.50995e+08],[&amp;quot;+2^-109&amp;quot;,3.08144e-33,1.59383e+08],[&amp;quot;+2^-108&amp;quot;,6.16288e-33,1.67772e+08],[&amp;quot;+2^-107&amp;quot;,1.23258e-32,1.76161e+08],[&amp;quot;+2^-106&amp;quot;,2.46515e-32,1.84549e+08],[&amp;quot;+2^-105&amp;quot;,4.93031e-32,1.92938e+08],[&amp;quot;+2^-104&amp;quot;,9.86061e-32,2.01326e+08],[&amp;quot;+2^-103&amp;quot;,1.97212e-31,2.09715e+08],[&amp;quot;+2^-102&amp;quot;,3.94424e-31,2.18104e+08],[&amp;quot;+2^-101&amp;quot;,7.88849e-31,2.26492e+08],[&amp;quot;+2^-100&amp;quot;,1.5777e-30,2.34881e+08],[&amp;quot;+2^-99&amp;quot;,3.1554e-30,2.43269e+08],[&amp;quot;+2^-98&amp;quot;,6.31079e-30,2.51658e+08],[&amp;quot;+2^-97&amp;quot;,1.26216e-29,2.60047e+08],[&amp;quot;+2^-96&amp;quot;,2.52432e-29,2.68435e+08],[&amp;quot;+2^-95&amp;quot;,5.04863e-29,2.76824e+08],[&amp;quot;+2^-94&amp;quot;,1.00973e-28,2.85212e+08],[&amp;quot;+2^-93&amp;quot;,2.01945e-28,2.93601e+08],[&amp;quot;+2^-92&amp;quot;,4.03891e-28,3.0199e+08],[&amp;quot;+2^-91&amp;quot;,8.07781e-28,3.10378e+08],[&amp;quot;+2^-90&amp;quot;,1.61556e-27,3.18767e+08],[&amp;quot;+2^-89&amp;quot;,3.23112e-27,3.27155e+08],[&amp;quot;+2^-88&amp;quot;,6.46225e-27,3.35544e+08],[&amp;quot;+2^-87&amp;quot;,1.29245e-26,3.43933e+08],[&amp;quot;+2^-86&amp;quot;,2.5849e-26,3.52321e+08],[&amp;quot;+2^-85&amp;quot;,5.1698e-26,3.6071e+08],[&amp;quot;+2^-84&amp;quot;,1.03396e-25,3.69098e+08],[&amp;quot;+2^-83&amp;quot;,2.06792e-25,3.77487e+08],[&amp;quot;+2^-82&amp;quot;,4.13584e-25,3.85876e+08],[&amp;quot;+2^-81&amp;quot;,8.27168e-25,3.94264e+08],[&amp;quot;+2^-80&amp;quot;,1.65434e-24,4.02653e+08],[&amp;quot;+2^-79&amp;quot;,3.30867e-24,4.11042e+08],[&amp;quot;+2^-78&amp;quot;,6.61734e-24,4.1943e+08],[&amp;quot;+2^-77&amp;quot;,1.32347e-23,4.27819e+08],[&amp;quot;+2^-76&amp;quot;,2.64694e-23,4.36207e+08],[&amp;quot;+2^-75&amp;quot;,5.29388e-23,4.44596e+08],[&amp;quot;+2^-74&amp;quot;,1.05878e-22,4.52985e+08],[&amp;quot;+2^-73&amp;quot;,2.11755e-22,4.61373e+08],[&amp;quot;+2^-72&amp;quot;,4.2351e-22,4.69762e+08],[&amp;quot;+2^-71&amp;quot;,8.4702e-22,4.7815e+08],[&amp;quot;+2^-70&amp;quot;,1.69404e-21,4.86539e+08],[&amp;quot;+2^-69&amp;quot;,3.38808e-21,4.94928e+08],[&amp;quot;+2^-68&amp;quot;,6.77616e-21,5.03316e+08],[&amp;quot;+2^-67&amp;quot;,1.35523e-20,5.11705e+08],[&amp;quot;+2^-66&amp;quot;,2.71046e-20,5.20093e+08],[&amp;quot;+2^-65&amp;quot;,5.42093e-20,5.28482e+08],[&amp;quot;+2^-64&amp;quot;,1.08419e-19,5.36871e+08],[&amp;quot;+2^-63&amp;quot;,2.16837e-19,5.45259e+08],[&amp;quot;+2^-62&amp;quot;,4.33674e-19,5.53648e+08],[&amp;quot;+2^-61&amp;quot;,8.67349e-19,5.62036e+08],[&amp;quot;+2^-60&amp;quot;,1.7347e-18,5.70425e+08],[&amp;quot;+2^-59&amp;quot;,3.46939e-18,5.78814e+08],[&amp;quot;+2^-58&amp;quot;,6.93879e-18,5.87202e+08],[&amp;quot;+2^-57&amp;quot;,1.38776e-17,5.95591e+08],[&amp;quot;+2^-56&amp;quot;,2.77552e-17,6.0398e+08],[&amp;quot;+2^-55&amp;quot;,5.55103e-17,6.12368e+08],[&amp;quot;+2^-54&amp;quot;,1.11021e-16,6.20757e+08],[&amp;quot;+2^-53&amp;quot;,2.22041e-16,6.29145e+08],[&amp;quot;+2^-52&amp;quot;,4.44082e-16,6.37534e+08],[&amp;quot;+2^-51&amp;quot;,8.88165e-16,6.45923e+08],[&amp;quot;+2^-50&amp;quot;,1.77633e-15,6.54311e+08],[&amp;quot;+2^-49&amp;quot;,3.55266e-15,6.627e+08],[&amp;quot;+2^-48&amp;quot;,7.10532e-15,6.71088e+08],[&amp;quot;+2^-47&amp;quot;,1.42106e-14,6.79477e+08],[&amp;quot;+2^-46&amp;quot;,2.84213e-14,6.87866e+08],[&amp;quot;+2^-45&amp;quot;,5.68426e-14,6.96254e+08],[&amp;quot;+2^-44&amp;quot;,1.13685e-13,7.04643e+08],[&amp;quot;+2^-43&amp;quot;,2.2737e-13,7.13031e+08],[&amp;quot;+2^-42&amp;quot;,4.5474e-13,7.2142e+08],[&amp;quot;+2^-41&amp;quot;,9.09481e-13,7.29809e+08],[&amp;quot;+2^-40&amp;quot;,1.81896e-12,7.38197e+08],[&amp;quot;+2^-39&amp;quot;,3.63792e-12,7.46586e+08],[&amp;quot;+2^-38&amp;quot;,7.27585e-12,7.54974e+08],[&amp;quot;+2^-37&amp;quot;,1.45517e-11,7.63363e+08],[&amp;quot;+2^-36&amp;quot;,2.91034e-11,7.71752e+08],[&amp;quot;+2^-35&amp;quot;,5.82068e-11,7.8014e+08],[&amp;quot;+2^-34&amp;quot;,1.16414e-10,7.88529e+08],[&amp;quot;+2^-33&amp;quot;,2.32827e-10,7.96918e+08],[&amp;quot;+2^-32&amp;quot;,4.65654e-10,8.05306e+08],[&amp;quot;+2^-31&amp;quot;,9.31308e-10,8.13695e+08],[&amp;quot;+2^-30&amp;quot;,1.86262e-09,8.22083e+08],[&amp;quot;+2^-29&amp;quot;,3.72523e-09,8.30472e+08],[&amp;quot;+2^-28&amp;quot;,7.45047e-09,8.38861e+08],[&amp;quot;+2^-27&amp;quot;,1.49009e-08,8.47249e+08],[&amp;quot;+2^-26&amp;quot;,2.98019e-08,8.55638e+08],[&amp;quot;+2^-25&amp;quot;,2.98023e-08,8.38861e+06],[&amp;quot;+2^-24&amp;quot;,2.98023e-08,4.1943e+06],[&amp;quot;+2^-23&amp;quot;,2.98023e-08,2.09715e+06],[&amp;quot;+2^-22&amp;quot;,2.98023e-08,1.04857e+06],[&amp;quot;+2^-21&amp;quot;,2.98021e-08,524284],[&amp;quot;+2^-20&amp;quot;,2.98014e-08,262136],[&amp;quot;+2^-19&amp;quot;,2.97987e-08,131056],[&amp;quot;+2^-18&amp;quot;,2.97878e-08,65504],[&amp;quot;+2^-17&amp;quot;,2.98014e-08,32767],[&amp;quot;+2^-16&amp;quot;,2.98005e-08,16384],[&amp;quot;+2^-15&amp;quot;,2.98023e-08,8192],[&amp;quot;+2^-14&amp;quot;,2.98025e-08,4351.99],[&amp;quot;+2^-13&amp;quot;,2.98044e-08,2815.96],[&amp;quot;+2^-12&amp;quot;,2.98091e-08,1024.23],[&amp;quot;+2^-11&amp;quot;,2.9812e-08,512.167],[&amp;quot;+2^-10&amp;quot;,2.98154e-08,256.113],[&amp;quot;+2^-9&amp;quot;,2.98591e-08,128.244],[&amp;quot;+2^-8&amp;quot;,3.00204e-08,85.3328],[&amp;quot;+2^-7&amp;quot;,3.01947e-08,43.675],[&amp;quot;+2^-6&amp;quot;,3.05713e-08,30.45],[&amp;quot;+2^-5&amp;quot;,3.14319e-08,8.43743],[&amp;quot;+2^-4&amp;quot;,3.31291e-08,7.77058],[&amp;quot;+2^-3&amp;quot;,3.58597e-08,3.99659],[&amp;quot;+2^-2&amp;quot;,4.14639e-08,2.29246],[&amp;quot;+2^-1&amp;quot;,8.02409e-08,1.34622],[&amp;quot;+2^0&amp;quot;,8.66201e-08,1.45324],[&amp;quot;+2^1&amp;quot;,8.8654e-08,1.48737],[&amp;quot;+2^2&amp;quot;,6.7976e-08,1.14045],[&amp;quot;+2^3&amp;quot;,1.33955e-07,2.2474],[&amp;quot;+2^4&amp;quot;,2.53131e-14,4.24683e-07],[&amp;quot;+2^5&amp;quot;,0,null],[&amp;quot;+2^6&amp;quot;,0,null],[&amp;quot;+2^7&amp;quot;,0,null],[&amp;quot;+2^8&amp;quot;,0,null],[&amp;quot;+2^9&amp;quot;,0,null],[&amp;quot;+2^10&amp;quot;,0,null],[&amp;quot;+2^11&amp;quot;,0,null],[&amp;quot;+2^12&amp;quot;,0,null],[&amp;quot;+2^13&amp;quot;,0,null],[&amp;quot;+2^14&amp;quot;,0,null],[&amp;quot;+2^15&amp;quot;,0,null],[&amp;quot;+2^16&amp;quot;,0,null],[&amp;quot;+2^17&amp;quot;,0,null],[&amp;quot;+2^18&amp;quot;,0,null],[&amp;quot;+2^19&amp;quot;,0,null],[&amp;quot;+2^20&amp;quot;,0,null],[&amp;quot;+2^21&amp;quot;,0,null],[&amp;quot;+2^22&amp;quot;,0,null],[&amp;quot;+2^23&amp;quot;,0,null],[&amp;quot;+2^24&amp;quot;,0,null],[&amp;quot;+2^25&amp;quot;,0,null],[&amp;quot;+2^26&amp;quot;,0,null],[&amp;quot;+2^27&amp;quot;,0,null],[&amp;quot;+2^28&amp;quot;,0,null],[&amp;quot;+2^29&amp;quot;,0,null],[&amp;quot;+2^30&amp;quot;,0,null],[&amp;quot;+2^31&amp;quot;,0,null],[&amp;quot;+2^32&amp;quot;,0,null],[&amp;quot;+2^33&amp;quot;,0,null],[&amp;quot;+2^34&amp;quot;,0,null],[&amp;quot;+2^35&amp;quot;,0,null],[&amp;quot;+2^36&amp;quot;,0,null],[&amp;quot;+2^37&amp;quot;,0,null],[&amp;quot;+2^38&amp;quot;,0,null],[&amp;quot;+2^39&amp;quot;,0,null],[&amp;quot;+2^40&amp;quot;,0,null],[&amp;quot;+2^41&amp;quot;,0,null],[&amp;quot;+2^42&amp;quot;,0,null],[&amp;quot;+2^43&amp;quot;,0,null],[&amp;quot;+2^44&amp;quot;,0,null],[&amp;quot;+2^45&amp;quot;,0,null],[&amp;quot;+2^46&amp;quot;,0,null],[&amp;quot;+2^47&amp;quot;,0,null],[&amp;quot;+2^48&amp;quot;,0,null],[&amp;quot;+2^49&amp;quot;,0,null],[&amp;quot;+2^50&amp;quot;,0,null],[&amp;quot;+2^51&amp;quot;,0,null],[&amp;quot;+2^52&amp;quot;,0,null],[&amp;quot;+2^53&amp;quot;,0,null],[&amp;quot;+2^54&amp;quot;,0,null],[&amp;quot;+2^55&amp;quot;,0,null],[&amp;quot;+2^56&amp;quot;,0,null],[&amp;quot;+2^57&amp;quot;,0,null],[&amp;quot;+2^58&amp;quot;,0,null],[&amp;quot;+2^59&amp;quot;,0,null],[&amp;quot;+2^60&amp;quot;,0,null],[&amp;quot;+2^61&amp;quot;,0,null],[&amp;quot;+2^62&amp;quot;,0,null],[&amp;quot;+2^63&amp;quot;,0,null],[&amp;quot;+2^64&amp;quot;,0,null],[&amp;quot;+2^65&amp;quot;,0,null],[&amp;quot;+2^66&amp;quot;,0,null],[&amp;quot;+2^67&amp;quot;,0,null],[&amp;quot;+2^68&amp;quot;,0,null],[&amp;quot;+2^69&amp;quot;,0,null],[&amp;quot;+2^70&amp;quot;,0,null],[&amp;quot;+2^71&amp;quot;,0,null],[&amp;quot;+2^72&amp;quot;,0,null],[&amp;quot;+2^73&amp;quot;,0,null],[&amp;quot;+2^74&amp;quot;,0,null],[&amp;quot;+2^75&amp;quot;,0,null],[&amp;quot;+2^76&amp;quot;,0,null],[&amp;quot;+2^77&amp;quot;,0,null],[&amp;quot;+2^78&amp;quot;,0,null],[&amp;quot;+2^79&amp;quot;,0,null],[&amp;quot;+2^80&amp;quot;,0,null],[&amp;quot;+2^81&amp;quot;,0,null],[&amp;quot;+2^82&amp;quot;,0,null],[&amp;quot;+2^83&amp;quot;,0,null],[&amp;quot;+2^84&amp;quot;,0,null],[&amp;quot;+2^85&amp;quot;,0,null],[&amp;quot;+2^86&amp;quot;,0,null],[&amp;quot;+2^87&amp;quot;,0,null],[&amp;quot;+2^88&amp;quot;,0,null],[&amp;quot;+2^89&amp;quot;,0,null],[&amp;quot;+2^90&amp;quot;,0,null],[&amp;quot;+2^91&amp;quot;,0,null],[&amp;quot;+2^92&amp;quot;,0,null],[&amp;quot;+2^93&amp;quot;,0,null],[&amp;quot;+2^94&amp;quot;,0,null],[&amp;quot;+2^95&amp;quot;,0,null],[&amp;quot;+2^96&amp;quot;,0,null],[&amp;quot;+2^97&amp;quot;,0,null],[&amp;quot;+2^98&amp;quot;,0,null],[&amp;quot;+2^99&amp;quot;,0,null],[&amp;quot;+2^100&amp;quot;,0,null],[&amp;quot;+2^101&amp;quot;,0,null],[&amp;quot;+2^102&amp;quot;,0,null],[&amp;quot;+2^103&amp;quot;,0,null],[&amp;quot;+2^104&amp;quot;,0,null],[&amp;quot;+2^105&amp;quot;,0,null],[&amp;quot;+2^106&amp;quot;,0,null],[&amp;quot;+2^107&amp;quot;,0,null],[&amp;quot;+2^108&amp;quot;,0,null],[&amp;quot;+2^109&amp;quot;,0,null],[&amp;quot;+2^110&amp;quot;,0,null],[&amp;quot;+2^111&amp;quot;,0,null],[&amp;quot;+2^112&amp;quot;,0,null],[&amp;quot;+2^113&amp;quot;,0,null],[&amp;quot;+2^114&amp;quot;,0,null],[&amp;quot;+2^115&amp;quot;,0,null],[&amp;quot;+2^116&amp;quot;,0,null],[&amp;quot;+2^117&amp;quot;,0,null],[&amp;quot;+2^118&amp;quot;,0,null],[&amp;quot;+2^119&amp;quot;,0,null],[&amp;quot;+2^120&amp;quot;,0,null],[&amp;quot;+2^121&amp;quot;,0,null],[&amp;quot;+2^122&amp;quot;,0,null],[&amp;quot;+2^123&amp;quot;,0,null],[&amp;quot;+2^124&amp;quot;,0,null],[&amp;quot;+2^125&amp;quot;,0,null],[&amp;quot;+2^126&amp;quot;,0,null],[&amp;quot;+2^127&amp;quot;,0,null] ]&lt;br /&gt;
 }}&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Key observation:&#039;&#039;&#039;  &lt;br /&gt;
For &amp;lt;math&amp;gt;|x| \ll 1&amp;lt;/math&amp;gt;, the ULP error grows rapidly and reaches extremely large values due to loss of significance. &lt;br /&gt;
For sufficiently large &amp;lt;math&amp;gt;x&amp;lt;/math&amp;gt;, both numerator and denominator overflow, leading to NaN results.&lt;br /&gt;
&lt;br /&gt;
=== expm1 ===&lt;br /&gt;
&lt;br /&gt;
This graph visualizes the ULP error of the &amp;lt;code&amp;gt;expm1&amp;lt;/code&amp;gt;-based formulation.&lt;br /&gt;
Using &amp;lt;code&amp;gt;expm1&amp;lt;/code&amp;gt; significantly improves numerical accuracy for small input values by avoiding catastrophic cancellation in the expression &amp;lt;math&amp;gt;e^{2x} - 1&amp;lt;/math&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
{{#widget:FloatErrorGraph|id=tanhf_expm1&lt;br /&gt;
  |data=[ [&amp;quot;-2^127&amp;quot;,0,1.17549e-38],[&amp;quot;-2^126&amp;quot;,0,1.17549e-38],[&amp;quot;-2^125&amp;quot;,0,1.17549e-38],[&amp;quot;-2^124&amp;quot;,0,1.17549e-38],[&amp;quot;-2^123&amp;quot;,0,1.17549e-38],[&amp;quot;-2^122&amp;quot;,0,1.17549e-38],[&amp;quot;-2^121&amp;quot;,0,1.17549e-38],[&amp;quot;-2^120&amp;quot;,0,1.17549e-38],[&amp;quot;-2^119&amp;quot;,0,1.17549e-38],[&amp;quot;-2^118&amp;quot;,0,1.17549e-38],[&amp;quot;-2^117&amp;quot;,0,1.17549e-38],[&amp;quot;-2^116&amp;quot;,0,1.17549e-38],[&amp;quot;-2^115&amp;quot;,0,1.17549e-38],[&amp;quot;-2^114&amp;quot;,0,1.17549e-38],[&amp;quot;-2^113&amp;quot;,0,1.17549e-38],[&amp;quot;-2^112&amp;quot;,0,1.17549e-38],[&amp;quot;-2^111&amp;quot;,0,1.17549e-38],[&amp;quot;-2^110&amp;quot;,0,1.17549e-38],[&amp;quot;-2^109&amp;quot;,0,1.17549e-38],[&amp;quot;-2^108&amp;quot;,0,1.17549e-38],[&amp;quot;-2^107&amp;quot;,0,1.17549e-38],[&amp;quot;-2^106&amp;quot;,0,1.17549e-38],[&amp;quot;-2^105&amp;quot;,0,1.17549e-38],[&amp;quot;-2^104&amp;quot;,0,1.17549e-38],[&amp;quot;-2^103&amp;quot;,0,1.17549e-38],[&amp;quot;-2^102&amp;quot;,0,1.17549e-38],[&amp;quot;-2^101&amp;quot;,0,1.17549e-38],[&amp;quot;-2^100&amp;quot;,0,1.17549e-38],[&amp;quot;-2^99&amp;quot;,0,1.17549e-38],[&amp;quot;-2^98&amp;quot;,0,1.17549e-38],[&amp;quot;-2^97&amp;quot;,0,1.17549e-38],[&amp;quot;-2^96&amp;quot;,0,1.17549e-38],[&amp;quot;-2^95&amp;quot;,0,1.17549e-38],[&amp;quot;-2^94&amp;quot;,0,1.17549e-38],[&amp;quot;-2^93&amp;quot;,0,1.17549e-38],[&amp;quot;-2^92&amp;quot;,0,1.17549e-38],[&amp;quot;-2^91&amp;quot;,0,1.17549e-38],[&amp;quot;-2^90&amp;quot;,0,1.17549e-38],[&amp;quot;-2^89&amp;quot;,0,1.17549e-38],[&amp;quot;-2^88&amp;quot;,0,1.17549e-38],[&amp;quot;-2^87&amp;quot;,0,1.17549e-38],[&amp;quot;-2^86&amp;quot;,0,1.17549e-38],[&amp;quot;-2^85&amp;quot;,0,1.17549e-38],[&amp;quot;-2^84&amp;quot;,0,1.17549e-38],[&amp;quot;-2^83&amp;quot;,0,1.17549e-38],[&amp;quot;-2^82&amp;quot;,0,1.17549e-38],[&amp;quot;-2^81&amp;quot;,0,1.17549e-38],[&amp;quot;-2^80&amp;quot;,0,1.17549e-38],[&amp;quot;-2^79&amp;quot;,0,1.17549e-38],[&amp;quot;-2^78&amp;quot;,0,1.17549e-38],[&amp;quot;-2^77&amp;quot;,0,1.17549e-38],[&amp;quot;-2^76&amp;quot;,0,1.17549e-38],[&amp;quot;-2^75&amp;quot;,0,1.17549e-38],[&amp;quot;-2^74&amp;quot;,0,1.17549e-38],[&amp;quot;-2^73&amp;quot;,0,1.17549e-38],[&amp;quot;-2^72&amp;quot;,0,1.17549e-38],[&amp;quot;-2^71&amp;quot;,0,1.17549e-38],[&amp;quot;-2^70&amp;quot;,0,1.17549e-38],[&amp;quot;-2^69&amp;quot;,0,1.17549e-38],[&amp;quot;-2^68&amp;quot;,0,1.17549e-38],[&amp;quot;-2^67&amp;quot;,0,1.17549e-38],[&amp;quot;-2^66&amp;quot;,0,1.17549e-38],[&amp;quot;-2^65&amp;quot;,0,1.17549e-38],[&amp;quot;-2^64&amp;quot;,0,1.17549e-38],[&amp;quot;-2^63&amp;quot;,0,1.17549e-38],[&amp;quot;-2^62&amp;quot;,0,1.17549e-38],[&amp;quot;-2^61&amp;quot;,0,1.17549e-38],[&amp;quot;-2^60&amp;quot;,0,1.17549e-38],[&amp;quot;-2^59&amp;quot;,0,1.17549e-38],[&amp;quot;-2^58&amp;quot;,0,1.17549e-38],[&amp;quot;-2^57&amp;quot;,0,1.17549e-38],[&amp;quot;-2^56&amp;quot;,0,1.17549e-38],[&amp;quot;-2^55&amp;quot;,0,1.17549e-38],[&amp;quot;-2^54&amp;quot;,0,1.17549e-38],[&amp;quot;-2^53&amp;quot;,0,1.17549e-38],[&amp;quot;-2^52&amp;quot;,0,1.17549e-38],[&amp;quot;-2^51&amp;quot;,0,1.17549e-38],[&amp;quot;-2^50&amp;quot;,0,1.17549e-38],[&amp;quot;-2^49&amp;quot;,0,1.17549e-38],[&amp;quot;-2^48&amp;quot;,0,1.17549e-38],[&amp;quot;-2^47&amp;quot;,0,1.17549e-38],[&amp;quot;-2^46&amp;quot;,0,1.17549e-38],[&amp;quot;-2^45&amp;quot;,0,1.17549e-38],[&amp;quot;-2^44&amp;quot;,0,1.17549e-38],[&amp;quot;-2^43&amp;quot;,0,1.17549e-38],[&amp;quot;-2^42&amp;quot;,0,1.17549e-38],[&amp;quot;-2^41&amp;quot;,0,1.17549e-38],[&amp;quot;-2^40&amp;quot;,0,1.17549e-38],[&amp;quot;-2^39&amp;quot;,0,1.17549e-38],[&amp;quot;-2^38&amp;quot;,0,1.17549e-38],[&amp;quot;-2^37&amp;quot;,0,1.17549e-38],[&amp;quot;-2^36&amp;quot;,0,1.17549e-38],[&amp;quot;-2^35&amp;quot;,0,1.17549e-38],[&amp;quot;-2^34&amp;quot;,0,1.17549e-38],[&amp;quot;-2^33&amp;quot;,0,1.17549e-38],[&amp;quot;-2^32&amp;quot;,0,1.17549e-38],[&amp;quot;-2^31&amp;quot;,0,1.17549e-38],[&amp;quot;-2^30&amp;quot;,0,1.17549e-38],[&amp;quot;-2^29&amp;quot;,0,1.17549e-38],[&amp;quot;-2^28&amp;quot;,0,1.17549e-38],[&amp;quot;-2^27&amp;quot;,0,1.17549e-38],[&amp;quot;-2^26&amp;quot;,0,1.17549e-38],[&amp;quot;-2^25&amp;quot;,0,1.17549e-38],[&amp;quot;-2^24&amp;quot;,0,1.17549e-38],[&amp;quot;-2^23&amp;quot;,0,1.17549e-38],[&amp;quot;-2^22&amp;quot;,0,1.17549e-38],[&amp;quot;-2^21&amp;quot;,0,1.17549e-38],[&amp;quot;-2^20&amp;quot;,0,1.17549e-38],[&amp;quot;-2^19&amp;quot;,0,1.17549e-38],[&amp;quot;-2^18&amp;quot;,0,1.17549e-38],[&amp;quot;-2^17&amp;quot;,0,1.17549e-38],[&amp;quot;-2^16&amp;quot;,0,1.17549e-38],[&amp;quot;-2^15&amp;quot;,0,1.17549e-38],[&amp;quot;-2^14&amp;quot;,0,1.17549e-38],[&amp;quot;-2^13&amp;quot;,0,1.17549e-38],[&amp;quot;-2^12&amp;quot;,0,1.17549e-38],[&amp;quot;-2^11&amp;quot;,0,1.17549e-38],[&amp;quot;-2^10&amp;quot;,0,1.17549e-38],[&amp;quot;-2^9&amp;quot;,0,1.17549e-38],[&amp;quot;-2^8&amp;quot;,0,1.17549e-38],[&amp;quot;-2^7&amp;quot;,0,1.17549e-38],[&amp;quot;-2^6&amp;quot;,0,1.17549e-38],[&amp;quot;-2^5&amp;quot;,0,1.17549e-38],[&amp;quot;-2^4&amp;quot;,2.53131e-14,4.24683e-07],[&amp;quot;-2^3&amp;quot;,1.19138e-07,1.9988],[&amp;quot;-2^2&amp;quot;,1.46791e-07,2.46274],[&amp;quot;-2^1&amp;quot;,1.47723e-07,2.47839],[&amp;quot;-2^0&amp;quot;,1.40758e-07,2.36153],[&amp;quot;-2^-1&amp;quot;,1.13422e-07,2.23445],[&amp;quot;-2^-2&amp;quot;,6.38012e-08,2.14081],[&amp;quot;-2^-3&amp;quot;,2.69538e-08,1.80884],[&amp;quot;-2^-4&amp;quot;,1.21308e-08,1.62817],[&amp;quot;-2^-5&amp;quot;,5.59535e-09,1.50199],[&amp;quot;-2^-6&amp;quot;,2.69177e-09,1.44513],[&amp;quot;-2^-7&amp;quot;,1.31289e-09,1.4097],[&amp;quot;-2^-8&amp;quot;,6.49688e-10,1.3952],[&amp;quot;-2^-9&amp;quot;,3.32043e-10,1.42611],[&amp;quot;-2^-10&amp;quot;,1.66275e-10,1.4283],[&amp;quot;-2^-11&amp;quot;,8.08595e-11,1.38916],[&amp;quot;-2^-12&amp;quot;,3.8939e-11,1.33794],[&amp;quot;-2^-13&amp;quot;,1.9401e-11,1.33323],[&amp;quot;-2^-14&amp;quot;,7.88212e-12,1.08331],[&amp;quot;-2^-15&amp;quot;,3.71372e-12,1.02082],[&amp;quot;-2^-16&amp;quot;,1.82845e-12,1.0052],[&amp;quot;-2^-17&amp;quot;,9.10676e-13,1.0013],[&amp;quot;-2^-18&amp;quot;,4.54895e-13,1.00032],[&amp;quot;-2^-19&amp;quot;,2.27392e-13,1.00008],[&amp;quot;-2^-20&amp;quot;,1.13689e-13,1.00002],[&amp;quot;-2^-21&amp;quot;,5.68437e-14,1],[&amp;quot;-2^-22&amp;quot;,2.84217e-14,1],[&amp;quot;-2^-23&amp;quot;,1.42109e-14,1],[&amp;quot;-2^-24&amp;quot;,7.10543e-15,1],[&amp;quot;-2^-25&amp;quot;,3.55271e-15,1],[&amp;quot;-2^-26&amp;quot;,9.92617e-24,5.58794e-09],[&amp;quot;-2^-27&amp;quot;,1.65436e-24,1.86265e-09],[&amp;quot;-2^-28&amp;quot;,8.27181e-25,1.86265e-09],[&amp;quot;-2^-29&amp;quot;,4.1359e-25,1.86265e-09],[&amp;quot;-2^-30&amp;quot;,2.06795e-25,1.86265e-09],[&amp;quot;-2^-31&amp;quot;,1.03398e-25,1.86265e-09],[&amp;quot;-2^-32&amp;quot;,5.16988e-26,1.86265e-09],[&amp;quot;-2^-33&amp;quot;,2.58494e-26,1.86265e-09],[&amp;quot;-2^-34&amp;quot;,1.29247e-26,1.86265e-09],[&amp;quot;-2^-35&amp;quot;,6.46235e-27,1.86265e-09],[&amp;quot;-2^-36&amp;quot;,3.23117e-27,1.86265e-09],[&amp;quot;-2^-37&amp;quot;,1.61559e-27,1.86265e-09],[&amp;quot;-2^-38&amp;quot;,8.07794e-28,1.86265e-09],[&amp;quot;-2^-39&amp;quot;,4.03897e-28,1.86265e-09],[&amp;quot;-2^-40&amp;quot;,2.01948e-28,1.86265e-09],[&amp;quot;-2^-41&amp;quot;,1.00974e-28,1.86265e-09],[&amp;quot;-2^-42&amp;quot;,5.04871e-29,1.86265e-09],[&amp;quot;-2^-43&amp;quot;,2.52435e-29,1.86265e-09],[&amp;quot;-2^-44&amp;quot;,1.26218e-29,1.86265e-09],[&amp;quot;-2^-45&amp;quot;,6.31089e-30,1.86265e-09],[&amp;quot;-2^-46&amp;quot;,3.15544e-30,1.86265e-09],[&amp;quot;-2^-47&amp;quot;,1.57772e-30,1.86265e-09],[&amp;quot;-2^-48&amp;quot;,7.88861e-31,1.86265e-09],[&amp;quot;-2^-49&amp;quot;,3.9443e-31,1.86265e-09],[&amp;quot;-2^-50&amp;quot;,1.97215e-31,1.86265e-09],[&amp;quot;-2^-51&amp;quot;,9.86076e-32,1.86265e-09],[&amp;quot;-2^-52&amp;quot;,4.93038e-32,1.86265e-09],[&amp;quot;-2^-53&amp;quot;,2.46519e-32,1.86265e-09],[&amp;quot;-2^-54&amp;quot;,1.2326e-32,1.86265e-09],[&amp;quot;-2^-55&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-56&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-57&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-58&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-59&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-60&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-61&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-62&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-63&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-64&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-65&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-66&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-67&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-68&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-69&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-70&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-71&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-72&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-73&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-74&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-75&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-76&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-77&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-78&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-79&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-80&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-81&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-82&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-83&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-84&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-85&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-86&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-87&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-88&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-89&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-90&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-91&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-92&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-93&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-94&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-95&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-96&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-97&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-98&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-99&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-100&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-101&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-102&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-103&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-104&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-105&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-106&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-107&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-108&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-109&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-110&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-111&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-112&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-113&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-114&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-115&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-116&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-117&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-118&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-119&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-120&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-121&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-122&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-123&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-124&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-125&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-126&amp;quot;,0,1.17549e-38],[&amp;quot;-SN&amp;quot;,0,1.17549e-38],[&amp;quot;0&amp;quot;,0,1.17549e-38],[&amp;quot;+SN&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-126&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-125&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-124&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-123&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-122&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-121&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-120&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-119&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-118&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-117&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-116&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-115&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-114&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-113&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-112&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-111&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-110&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-109&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-108&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-107&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-106&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-105&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-104&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-103&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-102&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-101&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-100&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-99&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-98&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-97&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-96&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-95&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-94&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-93&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-92&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-91&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-90&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-89&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-88&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-87&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-86&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-85&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-84&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-83&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-82&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-81&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-80&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-79&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-78&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-77&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-76&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-75&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-74&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-73&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-72&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-71&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-70&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-69&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-68&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-67&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-66&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-65&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-64&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-63&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-62&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-61&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-60&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-59&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-58&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-57&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-56&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-55&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-54&amp;quot;,1.2326e-32,1.86265e-09],[&amp;quot;+2^-53&amp;quot;,2.46519e-32,1.86265e-09],[&amp;quot;+2^-52&amp;quot;,4.93038e-32,1.86265e-09],[&amp;quot;+2^-51&amp;quot;,9.86076e-32,1.86265e-09],[&amp;quot;+2^-50&amp;quot;,1.97215e-31,1.86265e-09],[&amp;quot;+2^-49&amp;quot;,3.9443e-31,1.86265e-09],[&amp;quot;+2^-48&amp;quot;,7.88861e-31,1.86265e-09],[&amp;quot;+2^-47&amp;quot;,1.57772e-30,1.86265e-09],[&amp;quot;+2^-46&amp;quot;,3.15544e-30,1.86265e-09],[&amp;quot;+2^-45&amp;quot;,6.31089e-30,1.86265e-09],[&amp;quot;+2^-44&amp;quot;,1.26218e-29,1.86265e-09],[&amp;quot;+2^-43&amp;quot;,2.52435e-29,1.86265e-09],[&amp;quot;+2^-42&amp;quot;,5.04871e-29,1.86265e-09],[&amp;quot;+2^-41&amp;quot;,1.00974e-28,1.86265e-09],[&amp;quot;+2^-40&amp;quot;,2.01948e-28,1.86265e-09],[&amp;quot;+2^-39&amp;quot;,4.03897e-28,1.86265e-09],[&amp;quot;+2^-38&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-37&amp;quot;,1.61559e-27,1.86265e-09],[&amp;quot;+2^-36&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-35&amp;quot;,6.46235e-27,1.86265e-09],[&amp;quot;+2^-34&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-33&amp;quot;,2.58494e-26,1.86265e-09],[&amp;quot;+2^-32&amp;quot;,5.16988e-26,1.86265e-09],[&amp;quot;+2^-31&amp;quot;,1.03398e-25,1.86265e-09],[&amp;quot;+2^-30&amp;quot;,2.06795e-25,1.86265e-09],[&amp;quot;+2^-29&amp;quot;,4.1359e-25,1.86265e-09],[&amp;quot;+2^-28&amp;quot;,8.27181e-25,1.86265e-09],[&amp;quot;+2^-27&amp;quot;,1.65436e-24,1.86265e-09],[&amp;quot;+2^-26&amp;quot;,9.92617e-24,5.58794e-09],[&amp;quot;+2^-25&amp;quot;,3.55271e-15,1],[&amp;quot;+2^-24&amp;quot;,7.10543e-15,1],[&amp;quot;+2^-23&amp;quot;,1.42109e-14,1],[&amp;quot;+2^-22&amp;quot;,2.84217e-14,1],[&amp;quot;+2^-21&amp;quot;,5.68437e-14,1],[&amp;quot;+2^-20&amp;quot;,1.13689e-13,1.00002],[&amp;quot;+2^-19&amp;quot;,2.27391e-13,1.00008],[&amp;quot;+2^-18&amp;quot;,4.54892e-13,1.00032],[&amp;quot;+2^-17&amp;quot;,9.10665e-13,1.00129],[&amp;quot;+2^-16&amp;quot;,1.82841e-12,1.00518],[&amp;quot;+2^-15&amp;quot;,3.71353e-12,1.02077],[&amp;quot;+2^-14&amp;quot;,1.39798e-11,1.92136],[&amp;quot;+2^-13&amp;quot;,2.63239e-11,1.80896],[&amp;quot;+2^-12&amp;quot;,5.37712e-11,1.84757],[&amp;quot;+2^-11&amp;quot;,1.09932e-10,1.88861],[&amp;quot;+2^-10&amp;quot;,2.24787e-10,1.93091],[&amp;quot;+2^-9&amp;quot;,4.3452e-10,1.86625],[&amp;quot;+2^-8&amp;quot;,1.01783e-09,2.18576],[&amp;quot;+2^-7&amp;quot;,2.10562e-09,2.26089],[&amp;quot;+2^-6&amp;quot;,4.03954e-09,2.16871],[&amp;quot;+2^-5&amp;quot;,8.15746e-09,2.18975],[&amp;quot;+2^-4&amp;quot;,1.5545e-08,2.08641],[&amp;quot;+2^-3&amp;quot;,2.62153e-08,2.05864],[&amp;quot;+2^-2&amp;quot;,4.09295e-08,1.73804],[&amp;quot;+2^-1&amp;quot;,8.02409e-08,1.34622],[&amp;quot;+2^0&amp;quot;,8.66201e-08,1.45324],[&amp;quot;+2^1&amp;quot;,8.8654e-08,1.48737],[&amp;quot;+2^2&amp;quot;,6.7976e-08,1.14045],[&amp;quot;+2^3&amp;quot;,7.44117e-08,1.24842],[&amp;quot;+2^4&amp;quot;,2.53131e-14,4.24683e-07],[&amp;quot;+2^5&amp;quot;,0,null],[&amp;quot;+2^6&amp;quot;,0,null],[&amp;quot;+2^7&amp;quot;,0,null],[&amp;quot;+2^8&amp;quot;,0,null],[&amp;quot;+2^9&amp;quot;,0,null],[&amp;quot;+2^10&amp;quot;,0,null],[&amp;quot;+2^11&amp;quot;,0,null],[&amp;quot;+2^12&amp;quot;,0,null],[&amp;quot;+2^13&amp;quot;,0,null],[&amp;quot;+2^14&amp;quot;,0,null],[&amp;quot;+2^15&amp;quot;,0,null],[&amp;quot;+2^16&amp;quot;,0,null],[&amp;quot;+2^17&amp;quot;,0,null],[&amp;quot;+2^18&amp;quot;,0,null],[&amp;quot;+2^19&amp;quot;,0,null],[&amp;quot;+2^20&amp;quot;,0,null],[&amp;quot;+2^21&amp;quot;,0,null],[&amp;quot;+2^22&amp;quot;,0,null],[&amp;quot;+2^23&amp;quot;,0,null],[&amp;quot;+2^24&amp;quot;,0,null],[&amp;quot;+2^25&amp;quot;,0,null],[&amp;quot;+2^26&amp;quot;,0,null],[&amp;quot;+2^27&amp;quot;,0,null],[&amp;quot;+2^28&amp;quot;,0,null],[&amp;quot;+2^29&amp;quot;,0,null],[&amp;quot;+2^30&amp;quot;,0,null],[&amp;quot;+2^31&amp;quot;,0,null],[&amp;quot;+2^32&amp;quot;,0,null],[&amp;quot;+2^33&amp;quot;,0,null],[&amp;quot;+2^34&amp;quot;,0,null],[&amp;quot;+2^35&amp;quot;,0,null],[&amp;quot;+2^36&amp;quot;,0,null],[&amp;quot;+2^37&amp;quot;,0,null],[&amp;quot;+2^38&amp;quot;,0,null],[&amp;quot;+2^39&amp;quot;,0,null],[&amp;quot;+2^40&amp;quot;,0,null],[&amp;quot;+2^41&amp;quot;,0,null],[&amp;quot;+2^42&amp;quot;,0,null],[&amp;quot;+2^43&amp;quot;,0,null],[&amp;quot;+2^44&amp;quot;,0,null],[&amp;quot;+2^45&amp;quot;,0,null],[&amp;quot;+2^46&amp;quot;,0,null],[&amp;quot;+2^47&amp;quot;,0,null],[&amp;quot;+2^48&amp;quot;,0,null],[&amp;quot;+2^49&amp;quot;,0,null],[&amp;quot;+2^50&amp;quot;,0,null],[&amp;quot;+2^51&amp;quot;,0,null],[&amp;quot;+2^52&amp;quot;,0,null],[&amp;quot;+2^53&amp;quot;,0,null],[&amp;quot;+2^54&amp;quot;,0,null],[&amp;quot;+2^55&amp;quot;,0,null],[&amp;quot;+2^56&amp;quot;,0,null],[&amp;quot;+2^57&amp;quot;,0,null],[&amp;quot;+2^58&amp;quot;,0,null],[&amp;quot;+2^59&amp;quot;,0,null],[&amp;quot;+2^60&amp;quot;,0,null],[&amp;quot;+2^61&amp;quot;,0,null],[&amp;quot;+2^62&amp;quot;,0,null],[&amp;quot;+2^63&amp;quot;,0,null],[&amp;quot;+2^64&amp;quot;,0,null],[&amp;quot;+2^65&amp;quot;,0,null],[&amp;quot;+2^66&amp;quot;,0,null],[&amp;quot;+2^67&amp;quot;,0,null],[&amp;quot;+2^68&amp;quot;,0,null],[&amp;quot;+2^69&amp;quot;,0,null],[&amp;quot;+2^70&amp;quot;,0,null],[&amp;quot;+2^71&amp;quot;,0,null],[&amp;quot;+2^72&amp;quot;,0,null],[&amp;quot;+2^73&amp;quot;,0,null],[&amp;quot;+2^74&amp;quot;,0,null],[&amp;quot;+2^75&amp;quot;,0,null],[&amp;quot;+2^76&amp;quot;,0,null],[&amp;quot;+2^77&amp;quot;,0,null],[&amp;quot;+2^78&amp;quot;,0,null],[&amp;quot;+2^79&amp;quot;,0,null],[&amp;quot;+2^80&amp;quot;,0,null],[&amp;quot;+2^81&amp;quot;,0,null],[&amp;quot;+2^82&amp;quot;,0,null],[&amp;quot;+2^83&amp;quot;,0,null],[&amp;quot;+2^84&amp;quot;,0,null],[&amp;quot;+2^85&amp;quot;,0,null],[&amp;quot;+2^86&amp;quot;,0,null],[&amp;quot;+2^87&amp;quot;,0,null],[&amp;quot;+2^88&amp;quot;,0,null],[&amp;quot;+2^89&amp;quot;,0,null],[&amp;quot;+2^90&amp;quot;,0,null],[&amp;quot;+2^91&amp;quot;,0,null],[&amp;quot;+2^92&amp;quot;,0,null],[&amp;quot;+2^93&amp;quot;,0,null],[&amp;quot;+2^94&amp;quot;,0,null],[&amp;quot;+2^95&amp;quot;,0,null],[&amp;quot;+2^96&amp;quot;,0,null],[&amp;quot;+2^97&amp;quot;,0,null],[&amp;quot;+2^98&amp;quot;,0,null],[&amp;quot;+2^99&amp;quot;,0,null],[&amp;quot;+2^100&amp;quot;,0,null],[&amp;quot;+2^101&amp;quot;,0,null],[&amp;quot;+2^102&amp;quot;,0,null],[&amp;quot;+2^103&amp;quot;,0,null],[&amp;quot;+2^104&amp;quot;,0,null],[&amp;quot;+2^105&amp;quot;,0,null],[&amp;quot;+2^106&amp;quot;,0,null],[&amp;quot;+2^107&amp;quot;,0,null],[&amp;quot;+2^108&amp;quot;,0,null],[&amp;quot;+2^109&amp;quot;,0,null],[&amp;quot;+2^110&amp;quot;,0,null],[&amp;quot;+2^111&amp;quot;,0,null],[&amp;quot;+2^112&amp;quot;,0,null],[&amp;quot;+2^113&amp;quot;,0,null],[&amp;quot;+2^114&amp;quot;,0,null],[&amp;quot;+2^115&amp;quot;,0,null],[&amp;quot;+2^116&amp;quot;,0,null],[&amp;quot;+2^117&amp;quot;,0,null],[&amp;quot;+2^118&amp;quot;,0,null],[&amp;quot;+2^119&amp;quot;,0,null],[&amp;quot;+2^120&amp;quot;,0,null],[&amp;quot;+2^121&amp;quot;,0,null],[&amp;quot;+2^122&amp;quot;,0,null],[&amp;quot;+2^123&amp;quot;,0,null],[&amp;quot;+2^124&amp;quot;,0,null],[&amp;quot;+2^125&amp;quot;,0,null],[&amp;quot;+2^126&amp;quot;,0,null],[&amp;quot;+2^127&amp;quot;,0,null] ]&lt;br /&gt;
 }}&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Key observation:&#039;&#039;&#039;  &lt;br /&gt;
Compared to direct exponential formulations, the &amp;lt;code&amp;gt;expm1&amp;lt;/code&amp;gt; approach maintains low ULP error for small and medium values of &amp;lt;math&amp;gt;x&amp;lt;/math&amp;gt;. &lt;br /&gt;
Overflow is still present for very large inputs, but the overall error behavior is substantially more stable.&lt;br /&gt;
&lt;br /&gt;
== Numerical Issues and Error Metrics ==&lt;br /&gt;
&lt;br /&gt;
To evaluate and compare different software implementations of &amp;lt;code&amp;gt;tanh&amp;lt;/code&amp;gt;, several numerical error metrics are used throughout this article. These metrics make it possible to reason about accuracy, robustness, and failure modes in floating-point arithmetic.&lt;br /&gt;
&lt;br /&gt;
=== ULP Error ===&lt;br /&gt;
ULP (Unit in the Last Place) measures the distance between two adjacent representable floating-point numbers. The ULP error of a computed result is defined as the difference, measured in ULPs, between the computed value and the correctly rounded reference value.&lt;br /&gt;
&lt;br /&gt;
An error of &amp;lt;code&amp;gt;ULP ≤ 2&amp;lt;/code&amp;gt; is commonly considered acceptable for single-precision transcendental functions, as it indicates that the result is very close to the correctly rounded value.&lt;br /&gt;
&lt;br /&gt;
Large ULP errors typically indicate catastrophic cancellation, loss of significant bits, or severe rounding effects.&lt;br /&gt;
&lt;br /&gt;
=== NaN and Overflow ===&lt;br /&gt;
NaN (Not a Number) results occur when undefined floating-point operations are performed, such as &amp;lt;code&amp;gt;inf / inf&amp;lt;/code&amp;gt;. In &amp;lt;code&amp;gt;tanh&amp;lt;/code&amp;gt; implementations, this often happens when both the numerator and denominator of a formulation overflow to infinity.&lt;br /&gt;
&lt;br /&gt;
Tracking NaN generation is critical, as NaNs can silently propagate through numerical pipelines and invalidate downstream results.&lt;br /&gt;
&lt;br /&gt;
=== Error Graphs ===&lt;br /&gt;
The error graphs shown in the following sections visualize ULP error across the full &amp;lt;code&amp;gt;float32&amp;lt;/code&amp;gt; input domain. They make it possible to identify:&lt;br /&gt;
* regions with catastrophic cancellation,&lt;br /&gt;
* input ranges that trigger overflow or NaNs,&lt;br /&gt;
* and numerical stability differences between formulations.&lt;br /&gt;
&lt;br /&gt;
These graphs are a key tool for understanding not only the maximum error, but also how errors are distributed across different input magnitudes.&lt;br /&gt;
&lt;br /&gt;
== Taylor Expansion (Local Approximation) ==&lt;br /&gt;
&lt;br /&gt;
Taylor series approximations of &amp;lt;code&amp;gt;tanh(x)&amp;lt;/code&amp;gt; expand the function around &amp;lt;math&amp;gt;x = 0&amp;lt;/math&amp;gt; and therefore provide a very accurate approximation only for sufficiently small input magnitudes.&lt;br /&gt;
&lt;br /&gt;
For small &amp;lt;math&amp;gt;|x|&amp;lt;/math&amp;gt;, the hyperbolic tangent behaves approximately linear (&amp;lt;math&amp;gt;\tanh(x) \approx x&amp;lt;/math&amp;gt;), and low-order Taylor polynomials can achieve excellent accuracy with minimal computational cost. This makes Taylor-based approximations attractive as a fast path in range-reduced implementations.&lt;br /&gt;
&lt;br /&gt;
However, Taylor expansions have a strictly limited radius of convergence. Outside a narrow neighborhood around zero, the approximation error grows extremely rapidly and quickly becomes unusable. As a result, Taylor polynomials are not suitable as standalone implementations of &amp;lt;code&amp;gt;tanh&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
{{#widget:FloatErrorGraph|id=taylor&lt;br /&gt;
  |data=[ [&amp;quot;-2^127&amp;quot;,0,0],[&amp;quot;-2^126&amp;quot;,0,0],[&amp;quot;-2^125&amp;quot;,0,0],[&amp;quot;-2^124&amp;quot;,0,0],[&amp;quot;-2^123&amp;quot;,0,0],[&amp;quot;-2^122&amp;quot;,0,0],[&amp;quot;-2^121&amp;quot;,0,0],[&amp;quot;-2^120&amp;quot;,0,0],[&amp;quot;-2^119&amp;quot;,0,0],[&amp;quot;-2^118&amp;quot;,0,0],[&amp;quot;-2^117&amp;quot;,0,0],[&amp;quot;-2^116&amp;quot;,0,0],[&amp;quot;-2^115&amp;quot;,0,0],[&amp;quot;-2^114&amp;quot;,0,0],[&amp;quot;-2^113&amp;quot;,0,0],[&amp;quot;-2^112&amp;quot;,0,0],[&amp;quot;-2^111&amp;quot;,0,0],[&amp;quot;-2^110&amp;quot;,0,0],[&amp;quot;-2^109&amp;quot;,0,0],[&amp;quot;-2^108&amp;quot;,0,0],[&amp;quot;-2^107&amp;quot;,0,0],[&amp;quot;-2^106&amp;quot;,0,0],[&amp;quot;-2^105&amp;quot;,0,0],[&amp;quot;-2^104&amp;quot;,0,0],[&amp;quot;-2^103&amp;quot;,0,0],[&amp;quot;-2^102&amp;quot;,0,0],[&amp;quot;-2^101&amp;quot;,0,0],[&amp;quot;-2^100&amp;quot;,0,0],[&amp;quot;-2^99&amp;quot;,0,0],[&amp;quot;-2^98&amp;quot;,0,0],[&amp;quot;-2^97&amp;quot;,0,0],[&amp;quot;-2^96&amp;quot;,0,0],[&amp;quot;-2^95&amp;quot;,0,0],[&amp;quot;-2^94&amp;quot;,0,0],[&amp;quot;-2^93&amp;quot;,0,0],[&amp;quot;-2^92&amp;quot;,0,0],[&amp;quot;-2^91&amp;quot;,0,0],[&amp;quot;-2^90&amp;quot;,0,0],[&amp;quot;-2^89&amp;quot;,0,0],[&amp;quot;-2^88&amp;quot;,0,0],[&amp;quot;-2^87&amp;quot;,0,0],[&amp;quot;-2^86&amp;quot;,0,0],[&amp;quot;-2^85&amp;quot;,0,0],[&amp;quot;-2^84&amp;quot;,0,0],[&amp;quot;-2^83&amp;quot;,0,0],[&amp;quot;-2^82&amp;quot;,0,0],[&amp;quot;-2^81&amp;quot;,0,0],[&amp;quot;-2^80&amp;quot;,0,0],[&amp;quot;-2^79&amp;quot;,0,0],[&amp;quot;-2^78&amp;quot;,0,0],[&amp;quot;-2^77&amp;quot;,0,0],[&amp;quot;-2^76&amp;quot;,0,0],[&amp;quot;-2^75&amp;quot;,0,0],[&amp;quot;-2^74&amp;quot;,0,0],[&amp;quot;-2^73&amp;quot;,0,0],[&amp;quot;-2^72&amp;quot;,0,0],[&amp;quot;-2^71&amp;quot;,0,0],[&amp;quot;-2^70&amp;quot;,0,0],[&amp;quot;-2^69&amp;quot;,0,0],[&amp;quot;-2^68&amp;quot;,0,0],[&amp;quot;-2^67&amp;quot;,0,0],[&amp;quot;-2^66&amp;quot;,0,0],[&amp;quot;-2^65&amp;quot;,0,0],[&amp;quot;-2^64&amp;quot;,0,0],[&amp;quot;-2^63&amp;quot;,1.84467e+19,3.09485e+26],[&amp;quot;-2^62&amp;quot;,9.22336e+18,1.54742e+26],[&amp;quot;-2^61&amp;quot;,4.61168e+18,7.73712e+25],[&amp;quot;-2^60&amp;quot;,2.30584e+18,3.86856e+25],[&amp;quot;-2^59&amp;quot;,1.15292e+18,1.93428e+25],[&amp;quot;-2^58&amp;quot;,5.7646e+17,9.6714e+24],[&amp;quot;-2^57&amp;quot;,2.8823e+17,4.8357e+24],[&amp;quot;-2^56&amp;quot;,1.44115e+17,2.41785e+24],[&amp;quot;-2^55&amp;quot;,7.20575e+16,1.20892e+24],[&amp;quot;-2^54&amp;quot;,3.60288e+16,6.04462e+23],[&amp;quot;-2^53&amp;quot;,1.80144e+16,3.02231e+23],[&amp;quot;-2^52&amp;quot;,9.00719e+15,1.51116e+23],[&amp;quot;-2^51&amp;quot;,4.5036e+15,7.55578e+22],[&amp;quot;-2^50&amp;quot;,2.2518e+15,3.77789e+22],[&amp;quot;-2^49&amp;quot;,1.1259e+15,1.88894e+22],[&amp;quot;-2^48&amp;quot;,5.62949e+14,9.44472e+21],[&amp;quot;-2^47&amp;quot;,2.81475e+14,4.72236e+21],[&amp;quot;-2^46&amp;quot;,1.40737e+14,2.36118e+21],[&amp;quot;-2^45&amp;quot;,7.03687e+13,1.18059e+21],[&amp;quot;-2^44&amp;quot;,3.51843e+13,5.90295e+20],[&amp;quot;-2^43&amp;quot;,1.75922e+13,2.95148e+20],[&amp;quot;-2^42&amp;quot;,8.79608e+12,1.47574e+20],[&amp;quot;-2^41&amp;quot;,4.39804e+12,7.37869e+19],[&amp;quot;-2^40&amp;quot;,2.19902e+12,3.68935e+19],[&amp;quot;-2^39&amp;quot;,1.09951e+12,1.84467e+19],[&amp;quot;-2^38&amp;quot;,5.49755e+11,9.22336e+18],[&amp;quot;-2^37&amp;quot;,2.74878e+11,4.61168e+18],[&amp;quot;-2^36&amp;quot;,1.37439e+11,2.30584e+18],[&amp;quot;-2^35&amp;quot;,6.87194e+10,1.15292e+18],[&amp;quot;-2^34&amp;quot;,3.43597e+10,5.7646e+17],[&amp;quot;-2^33&amp;quot;,1.71799e+10,2.8823e+17],[&amp;quot;-2^32&amp;quot;,8.58993e+09,1.44115e+17],[&amp;quot;-2^31&amp;quot;,4.29085e+09,7.19885e+16],[&amp;quot;-2^30&amp;quot;,2.14542e+09,3.59943e+16],[&amp;quot;-2^29&amp;quot;,1.07271e+09,1.79971e+16],[&amp;quot;-2^28&amp;quot;,5.36356e+08,8.99856e+15],[&amp;quot;-2^27&amp;quot;,2.68178e+08,4.49928e+15],[&amp;quot;-2^26&amp;quot;,1.34089e+08,2.24964e+15],[&amp;quot;-2^25&amp;quot;,6.70445e+07,1.12482e+15],[&amp;quot;-2^24&amp;quot;,3.35223e+07,5.6241e+14],[&amp;quot;-2^23&amp;quot;,1.67611e+07,2.81205e+14],[&amp;quot;-2^22&amp;quot;,8.38056e+06,1.40603e+14],[&amp;quot;-2^21&amp;quot;,4.19028e+06,7.03013e+13],[&amp;quot;-2^20&amp;quot;,2.09514e+06,3.51506e+13],[&amp;quot;-2^19&amp;quot;,1.04757e+06,1.75753e+13],[&amp;quot;-2^18&amp;quot;,523784,8.78764e+12],[&amp;quot;-2^17&amp;quot;,261892,4.39381e+12],[&amp;quot;-2^16&amp;quot;,130945,2.1969e+12],[&amp;quot;-2^15&amp;quot;,65472.2,1.09844e+12],[&amp;quot;-2^14&amp;quot;,32735.6,5.49212e+11],[&amp;quot;-2^13&amp;quot;,16367.3,2.74598e+11],[&amp;quot;-2^12&amp;quot;,8183.15,1.3729e+11],[&amp;quot;-2^11&amp;quot;,4091.07,6.86368e+10],[&amp;quot;-2^10&amp;quot;,2045.04,3.431e+10],[&amp;quot;-2^9&amp;quot;,1022.02,1.71466e+10],[&amp;quot;-2^8&amp;quot;,510.509,8.56492e+09],[&amp;quot;-2^7&amp;quot;,254.754,4.27406e+09],[&amp;quot;-2^6&amp;quot;,126.876,2.12862e+09],[&amp;quot;-2^5&amp;quot;,62.9354,1.05588e+09],[&amp;quot;-2^4&amp;quot;,30.9629,5.19471e+08],[&amp;quot;-2^3&amp;quot;,14.9718,2.51185e+08],[&amp;quot;-2^2&amp;quot;,6.96672,1.16882e+08],[&amp;quot;-2^1&amp;quot;,2.94633,4.94312e+07],[&amp;quot;-2^0&amp;quot;,0.940715,1.57826e+07],[&amp;quot;-2^-1&amp;quot;,0.127635,2.14135e+06],[&amp;quot;-2^-2&amp;quot;,0.00318735,106950],[&amp;quot;-2^-3&amp;quot;,3.11475e-05,2090.28],[&amp;quot;-2^-4&amp;quot;,2.57737e-07,34.5929],[&amp;quot;-2^-5&amp;quot;,3.89213e-09,1.04478],[&amp;quot;-2^-6&amp;quot;,9.51472e-10,0.510818],[&amp;quot;-2^-7&amp;quot;,4.66402e-10,0.500795],[&amp;quot;-2^-8&amp;quot;,2.32917e-10,0.500185],[&amp;quot;-2^-9&amp;quot;,1.16423e-10,0.500033],[&amp;quot;-2^-10&amp;quot;,5.82082e-11,0.500004],[&amp;quot;-2^-11&amp;quot;,2.91037e-11,0.499998],[&amp;quot;-2^-12&amp;quot;,1.45519e-11,0.5],[&amp;quot;-2^-13&amp;quot;,4.85062e-12,0.333332],[&amp;quot;-2^-14&amp;quot;,6.06328e-13,0.0833331],[&amp;quot;-2^-15&amp;quot;,7.5791e-14,0.0208333],[&amp;quot;-2^-16&amp;quot;,9.47387e-15,0.00520832],[&amp;quot;-2^-17&amp;quot;,1.18424e-15,0.00130208],[&amp;quot;-2^-18&amp;quot;,1.48029e-16,0.00032552],[&amp;quot;-2^-19&amp;quot;,1.85039e-17,8.13808e-05],[&amp;quot;-2^-20&amp;quot;,2.31304e-18,2.03457e-05],[&amp;quot;-2^-21&amp;quot;,2.89156e-19,5.08688e-06],[&amp;quot;-2^-22&amp;quot;,3.61577e-20,1.27219e-06],[&amp;quot;-2^-23&amp;quot;,4.52633e-21,3.18512e-07],[&amp;quot;-2^-24&amp;quot;,5.691e-22,8.00937e-08],[&amp;quot;-2^-25&amp;quot;,7.27919e-23,2.04891e-08],[&amp;quot;-2^-26&amp;quot;,9.92617e-24,5.58794e-09],[&amp;quot;-2^-27&amp;quot;,1.65436e-24,1.86265e-09],[&amp;quot;-2^-28&amp;quot;,8.27181e-25,1.86265e-09],[&amp;quot;-2^-29&amp;quot;,4.1359e-25,1.86265e-09],[&amp;quot;-2^-30&amp;quot;,2.06795e-25,1.86265e-09],[&amp;quot;-2^-31&amp;quot;,1.03398e-25,1.86265e-09],[&amp;quot;-2^-32&amp;quot;,5.16988e-26,1.86265e-09],[&amp;quot;-2^-33&amp;quot;,2.58494e-26,1.86265e-09],[&amp;quot;-2^-34&amp;quot;,1.29247e-26,1.86265e-09],[&amp;quot;-2^-35&amp;quot;,6.46235e-27,1.86265e-09],[&amp;quot;-2^-36&amp;quot;,3.23117e-27,1.86265e-09],[&amp;quot;-2^-37&amp;quot;,1.61559e-27,1.86265e-09],[&amp;quot;-2^-38&amp;quot;,8.07794e-28,1.86265e-09],[&amp;quot;-2^-39&amp;quot;,4.03897e-28,1.86265e-09],[&amp;quot;-2^-40&amp;quot;,2.01948e-28,1.86265e-09],[&amp;quot;-2^-41&amp;quot;,1.00974e-28,1.86265e-09],[&amp;quot;-2^-42&amp;quot;,5.04871e-29,1.86265e-09],[&amp;quot;-2^-43&amp;quot;,2.52435e-29,1.86265e-09],[&amp;quot;-2^-44&amp;quot;,1.26218e-29,1.86265e-09],[&amp;quot;-2^-45&amp;quot;,6.31089e-30,1.86265e-09],[&amp;quot;-2^-46&amp;quot;,3.15544e-30,1.86265e-09],[&amp;quot;-2^-47&amp;quot;,1.57772e-30,1.86265e-09],[&amp;quot;-2^-48&amp;quot;,7.88861e-31,1.86265e-09],[&amp;quot;-2^-49&amp;quot;,3.9443e-31,1.86265e-09],[&amp;quot;-2^-50&amp;quot;,1.97215e-31,1.86265e-09],[&amp;quot;-2^-51&amp;quot;,9.86076e-32,1.86265e-09],[&amp;quot;-2^-52&amp;quot;,4.93038e-32,1.86265e-09],[&amp;quot;-2^-53&amp;quot;,2.46519e-32,1.86265e-09],[&amp;quot;-2^-54&amp;quot;,1.2326e-32,1.86265e-09],[&amp;quot;-2^-55&amp;quot;,0,0],[&amp;quot;-2^-56&amp;quot;,0,0],[&amp;quot;-2^-57&amp;quot;,0,0],[&amp;quot;-2^-58&amp;quot;,0,0],[&amp;quot;-2^-59&amp;quot;,0,0],[&amp;quot;-2^-60&amp;quot;,0,0],[&amp;quot;-2^-61&amp;quot;,0,0],[&amp;quot;-2^-62&amp;quot;,0,0],[&amp;quot;-2^-63&amp;quot;,0,0],[&amp;quot;-2^-64&amp;quot;,0,0],[&amp;quot;-2^-65&amp;quot;,0,0],[&amp;quot;-2^-66&amp;quot;,0,0],[&amp;quot;-2^-67&amp;quot;,0,0],[&amp;quot;-2^-68&amp;quot;,0,0],[&amp;quot;-2^-69&amp;quot;,0,0],[&amp;quot;-2^-70&amp;quot;,0,0],[&amp;quot;-2^-71&amp;quot;,0,0],[&amp;quot;-2^-72&amp;quot;,0,0],[&amp;quot;-2^-73&amp;quot;,0,0],[&amp;quot;-2^-74&amp;quot;,0,0],[&amp;quot;-2^-75&amp;quot;,0,0],[&amp;quot;-2^-76&amp;quot;,0,0],[&amp;quot;-2^-77&amp;quot;,0,0],[&amp;quot;-2^-78&amp;quot;,0,0],[&amp;quot;-2^-79&amp;quot;,0,0],[&amp;quot;-2^-80&amp;quot;,0,0],[&amp;quot;-2^-81&amp;quot;,0,0],[&amp;quot;-2^-82&amp;quot;,0,0],[&amp;quot;-2^-83&amp;quot;,0,0],[&amp;quot;-2^-84&amp;quot;,0,0],[&amp;quot;-2^-85&amp;quot;,0,0],[&amp;quot;-2^-86&amp;quot;,0,0],[&amp;quot;-2^-87&amp;quot;,0,0],[&amp;quot;-2^-88&amp;quot;,0,0],[&amp;quot;-2^-89&amp;quot;,0,0],[&amp;quot;-2^-90&amp;quot;,0,0],[&amp;quot;-2^-91&amp;quot;,0,0],[&amp;quot;-2^-92&amp;quot;,0,0],[&amp;quot;-2^-93&amp;quot;,0,0],[&amp;quot;-2^-94&amp;quot;,0,0],[&amp;quot;-2^-95&amp;quot;,0,0],[&amp;quot;-2^-96&amp;quot;,0,0],[&amp;quot;-2^-97&amp;quot;,0,0],[&amp;quot;-2^-98&amp;quot;,0,0],[&amp;quot;-2^-99&amp;quot;,0,0],[&amp;quot;-2^-100&amp;quot;,0,0],[&amp;quot;-2^-101&amp;quot;,0,0],[&amp;quot;-2^-102&amp;quot;,0,0],[&amp;quot;-2^-103&amp;quot;,0,0],[&amp;quot;-2^-104&amp;quot;,0,0],[&amp;quot;-2^-105&amp;quot;,0,0],[&amp;quot;-2^-106&amp;quot;,0,0],[&amp;quot;-2^-107&amp;quot;,0,0],[&amp;quot;-2^-108&amp;quot;,0,0],[&amp;quot;-2^-109&amp;quot;,0,0],[&amp;quot;-2^-110&amp;quot;,0,0],[&amp;quot;-2^-111&amp;quot;,0,0],[&amp;quot;-2^-112&amp;quot;,0,0],[&amp;quot;-2^-113&amp;quot;,0,0],[&amp;quot;-2^-114&amp;quot;,0,0],[&amp;quot;-2^-115&amp;quot;,0,0],[&amp;quot;-2^-116&amp;quot;,0,0],[&amp;quot;-2^-117&amp;quot;,0,0],[&amp;quot;-2^-118&amp;quot;,0,0],[&amp;quot;-2^-119&amp;quot;,0,0],[&amp;quot;-2^-120&amp;quot;,0,0],[&amp;quot;-2^-121&amp;quot;,0,0],[&amp;quot;-2^-122&amp;quot;,0,0],[&amp;quot;-2^-123&amp;quot;,0,0],[&amp;quot;-2^-124&amp;quot;,0,0],[&amp;quot;-2^-125&amp;quot;,0,0],[&amp;quot;-2^-126&amp;quot;,0,0],[&amp;quot;-SN&amp;quot;,0,0],[&amp;quot;0&amp;quot;,0,0],[&amp;quot;+SN&amp;quot;,0,0],[&amp;quot;+2^-126&amp;quot;,0,0],[&amp;quot;+2^-125&amp;quot;,0,0],[&amp;quot;+2^-124&amp;quot;,0,0],[&amp;quot;+2^-123&amp;quot;,0,0],[&amp;quot;+2^-122&amp;quot;,0,0],[&amp;quot;+2^-121&amp;quot;,0,0],[&amp;quot;+2^-120&amp;quot;,0,0],[&amp;quot;+2^-119&amp;quot;,0,0],[&amp;quot;+2^-118&amp;quot;,0,0],[&amp;quot;+2^-117&amp;quot;,0,0],[&amp;quot;+2^-116&amp;quot;,0,0],[&amp;quot;+2^-115&amp;quot;,0,0],[&amp;quot;+2^-114&amp;quot;,0,0],[&amp;quot;+2^-113&amp;quot;,0,0],[&amp;quot;+2^-112&amp;quot;,0,0],[&amp;quot;+2^-111&amp;quot;,0,0],[&amp;quot;+2^-110&amp;quot;,0,0],[&amp;quot;+2^-109&amp;quot;,0,0],[&amp;quot;+2^-108&amp;quot;,0,0],[&amp;quot;+2^-107&amp;quot;,0,0],[&amp;quot;+2^-106&amp;quot;,0,0],[&amp;quot;+2^-105&amp;quot;,0,0],[&amp;quot;+2^-104&amp;quot;,0,0],[&amp;quot;+2^-103&amp;quot;,0,0],[&amp;quot;+2^-102&amp;quot;,0,0],[&amp;quot;+2^-101&amp;quot;,0,0],[&amp;quot;+2^-100&amp;quot;,0,0],[&amp;quot;+2^-99&amp;quot;,0,0],[&amp;quot;+2^-98&amp;quot;,0,0],[&amp;quot;+2^-97&amp;quot;,0,0],[&amp;quot;+2^-96&amp;quot;,0,0],[&amp;quot;+2^-95&amp;quot;,0,0],[&amp;quot;+2^-94&amp;quot;,0,0],[&amp;quot;+2^-93&amp;quot;,0,0],[&amp;quot;+2^-92&amp;quot;,0,0],[&amp;quot;+2^-91&amp;quot;,0,0],[&amp;quot;+2^-90&amp;quot;,0,0],[&amp;quot;+2^-89&amp;quot;,0,0],[&amp;quot;+2^-88&amp;quot;,0,0],[&amp;quot;+2^-87&amp;quot;,0,0],[&amp;quot;+2^-86&amp;quot;,0,0],[&amp;quot;+2^-85&amp;quot;,0,0],[&amp;quot;+2^-84&amp;quot;,0,0],[&amp;quot;+2^-83&amp;quot;,0,0],[&amp;quot;+2^-82&amp;quot;,0,0],[&amp;quot;+2^-81&amp;quot;,0,0],[&amp;quot;+2^-80&amp;quot;,0,0],[&amp;quot;+2^-79&amp;quot;,0,0],[&amp;quot;+2^-78&amp;quot;,0,0],[&amp;quot;+2^-77&amp;quot;,0,0],[&amp;quot;+2^-76&amp;quot;,0,0],[&amp;quot;+2^-75&amp;quot;,0,0],[&amp;quot;+2^-74&amp;quot;,0,0],[&amp;quot;+2^-73&amp;quot;,0,0],[&amp;quot;+2^-72&amp;quot;,0,0],[&amp;quot;+2^-71&amp;quot;,0,0],[&amp;quot;+2^-70&amp;quot;,0,0],[&amp;quot;+2^-69&amp;quot;,0,0],[&amp;quot;+2^-68&amp;quot;,0,0],[&amp;quot;+2^-67&amp;quot;,0,0],[&amp;quot;+2^-66&amp;quot;,0,0],[&amp;quot;+2^-65&amp;quot;,0,0],[&amp;quot;+2^-64&amp;quot;,0,0],[&amp;quot;+2^-63&amp;quot;,0,0],[&amp;quot;+2^-62&amp;quot;,0,0],[&amp;quot;+2^-61&amp;quot;,0,0],[&amp;quot;+2^-60&amp;quot;,0,0],[&amp;quot;+2^-59&amp;quot;,0,0],[&amp;quot;+2^-58&amp;quot;,0,0],[&amp;quot;+2^-57&amp;quot;,0,0],[&amp;quot;+2^-56&amp;quot;,0,0],[&amp;quot;+2^-55&amp;quot;,0,0],[&amp;quot;+2^-54&amp;quot;,1.2326e-32,1.86265e-09],[&amp;quot;+2^-53&amp;quot;,2.46519e-32,1.86265e-09],[&amp;quot;+2^-52&amp;quot;,4.93038e-32,1.86265e-09],[&amp;quot;+2^-51&amp;quot;,9.86076e-32,1.86265e-09],[&amp;quot;+2^-50&amp;quot;,1.97215e-31,1.86265e-09],[&amp;quot;+2^-49&amp;quot;,3.9443e-31,1.86265e-09],[&amp;quot;+2^-48&amp;quot;,7.88861e-31,1.86265e-09],[&amp;quot;+2^-47&amp;quot;,1.57772e-30,1.86265e-09],[&amp;quot;+2^-46&amp;quot;,3.15544e-30,1.86265e-09],[&amp;quot;+2^-45&amp;quot;,6.31089e-30,1.86265e-09],[&amp;quot;+2^-44&amp;quot;,1.26218e-29,1.86265e-09],[&amp;quot;+2^-43&amp;quot;,2.52435e-29,1.86265e-09],[&amp;quot;+2^-42&amp;quot;,5.04871e-29,1.86265e-09],[&amp;quot;+2^-41&amp;quot;,1.00974e-28,1.86265e-09],[&amp;quot;+2^-40&amp;quot;,2.01948e-28,1.86265e-09],[&amp;quot;+2^-39&amp;quot;,4.03897e-28,1.86265e-09],[&amp;quot;+2^-38&amp;quot;,8.07794e-28,1.86265e-09],[&amp;quot;+2^-37&amp;quot;,1.61559e-27,1.86265e-09],[&amp;quot;+2^-36&amp;quot;,3.23117e-27,1.86265e-09],[&amp;quot;+2^-35&amp;quot;,6.46235e-27,1.86265e-09],[&amp;quot;+2^-34&amp;quot;,1.29247e-26,1.86265e-09],[&amp;quot;+2^-33&amp;quot;,2.58494e-26,1.86265e-09],[&amp;quot;+2^-32&amp;quot;,5.16988e-26,1.86265e-09],[&amp;quot;+2^-31&amp;quot;,1.03398e-25,1.86265e-09],[&amp;quot;+2^-30&amp;quot;,2.06795e-25,1.86265e-09],[&amp;quot;+2^-29&amp;quot;,4.1359e-25,1.86265e-09],[&amp;quot;+2^-28&amp;quot;,8.27181e-25,1.86265e-09],[&amp;quot;+2^-27&amp;quot;,1.65436e-24,1.86265e-09],[&amp;quot;+2^-26&amp;quot;,9.92617e-24,5.58794e-09],[&amp;quot;+2^-25&amp;quot;,7.27919e-23,2.04891e-08],[&amp;quot;+2^-24&amp;quot;,5.691e-22,8.00937e-08],[&amp;quot;+2^-23&amp;quot;,4.52633e-21,3.18512e-07],[&amp;quot;+2^-22&amp;quot;,3.61577e-20,1.27219e-06],[&amp;quot;+2^-21&amp;quot;,2.89156e-19,5.08688e-06],[&amp;quot;+2^-20&amp;quot;,2.31304e-18,2.03457e-05],[&amp;quot;+2^-19&amp;quot;,1.85039e-17,8.13808e-05],[&amp;quot;+2^-18&amp;quot;,1.48029e-16,0.00032552],[&amp;quot;+2^-17&amp;quot;,1.18424e-15,0.00130208],[&amp;quot;+2^-16&amp;quot;,9.47387e-15,0.00520832],[&amp;quot;+2^-15&amp;quot;,7.5791e-14,0.0208333],[&amp;quot;+2^-14&amp;quot;,6.06328e-13,0.0833331],[&amp;quot;+2^-13&amp;quot;,4.85062e-12,0.333332],[&amp;quot;+2^-12&amp;quot;,1.45519e-11,0.5],[&amp;quot;+2^-11&amp;quot;,2.91037e-11,0.499998],[&amp;quot;+2^-10&amp;quot;,5.82082e-11,0.500004],[&amp;quot;+2^-9&amp;quot;,1.16423e-10,0.500033],[&amp;quot;+2^-8&amp;quot;,2.32917e-10,0.500185],[&amp;quot;+2^-7&amp;quot;,4.66402e-10,0.500795],[&amp;quot;+2^-6&amp;quot;,9.51472e-10,0.510818],[&amp;quot;+2^-5&amp;quot;,3.89213e-09,1.04478],[&amp;quot;+2^-4&amp;quot;,2.57737e-07,34.5929],[&amp;quot;+2^-3&amp;quot;,3.11475e-05,2090.28],[&amp;quot;+2^-2&amp;quot;,0.00318735,106950],[&amp;quot;+2^-1&amp;quot;,0.127635,2.14135e+06],[&amp;quot;+2^0&amp;quot;,0.940715,1.57826e+07],[&amp;quot;+2^1&amp;quot;,2.94633,4.94312e+07],[&amp;quot;+2^2&amp;quot;,6.96672,1.16882e+08],[&amp;quot;+2^3&amp;quot;,14.9718,1.33874e+08],[&amp;quot;+2^4&amp;quot;,30.9629,2.59736e+08],[&amp;quot;+2^5&amp;quot;,62.9354,5.27941e+08],[&amp;quot;+2^6&amp;quot;,126.876,1.06431e+09],[&amp;quot;+2^7&amp;quot;,254.754,2.13703e+09],[&amp;quot;+2^8&amp;quot;,510.509,4.28246e+09],[&amp;quot;+2^9&amp;quot;,1022.02,8.57331e+09],[&amp;quot;+2^10&amp;quot;,2045.04,1.7155e+10],[&amp;quot;+2^11&amp;quot;,4091.07,3.43184e+10],[&amp;quot;+2^12&amp;quot;,8183.15,6.86452e+10],[&amp;quot;+2^13&amp;quot;,16367.3,1.37299e+11],[&amp;quot;+2^14&amp;quot;,32735.6,2.74606e+11],[&amp;quot;+2^15&amp;quot;,65472.2,5.4922e+11],[&amp;quot;+2^16&amp;quot;,130945,1.09845e+12],[&amp;quot;+2^17&amp;quot;,261892,2.19691e+12],[&amp;quot;+2^18&amp;quot;,523784,4.39382e+12],[&amp;quot;+2^19&amp;quot;,1.04757e+06,8.78765e+12],[&amp;quot;+2^20&amp;quot;,2.09514e+06,1.75753e+13],[&amp;quot;+2^21&amp;quot;,4.19028e+06,3.51506e+13],[&amp;quot;+2^22&amp;quot;,8.38056e+06,7.03013e+13],[&amp;quot;+2^23&amp;quot;,1.67611e+07,1.40603e+14],[&amp;quot;+2^24&amp;quot;,3.35223e+07,2.81205e+14],[&amp;quot;+2^25&amp;quot;,6.70445e+07,5.6241e+14],[&amp;quot;+2^26&amp;quot;,1.34089e+08,1.12482e+15],[&amp;quot;+2^27&amp;quot;,2.68178e+08,2.24964e+15],[&amp;quot;+2^28&amp;quot;,5.36356e+08,4.49928e+15],[&amp;quot;+2^29&amp;quot;,1.07271e+09,8.99856e+15],[&amp;quot;+2^30&amp;quot;,2.14542e+09,1.79971e+16],[&amp;quot;+2^31&amp;quot;,4.29085e+09,3.59943e+16],[&amp;quot;+2^32&amp;quot;,8.58993e+09,7.20575e+16],[&amp;quot;+2^33&amp;quot;,1.71799e+10,1.44115e+17],[&amp;quot;+2^34&amp;quot;,3.43597e+10,2.8823e+17],[&amp;quot;+2^35&amp;quot;,6.87194e+10,5.7646e+17],[&amp;quot;+2^36&amp;quot;,1.37439e+11,1.15292e+18],[&amp;quot;+2^37&amp;quot;,2.74878e+11,2.30584e+18],[&amp;quot;+2^38&amp;quot;,5.49755e+11,4.61168e+18],[&amp;quot;+2^39&amp;quot;,1.09951e+12,9.22336e+18],[&amp;quot;+2^40&amp;quot;,2.19902e+12,1.84467e+19],[&amp;quot;+2^41&amp;quot;,4.39804e+12,3.68935e+19],[&amp;quot;+2^42&amp;quot;,8.79608e+12,7.37869e+19],[&amp;quot;+2^43&amp;quot;,1.75922e+13,1.47574e+20],[&amp;quot;+2^44&amp;quot;,3.51843e+13,2.95148e+20],[&amp;quot;+2^45&amp;quot;,7.03687e+13,5.90295e+20],[&amp;quot;+2^46&amp;quot;,1.40737e+14,1.18059e+21],[&amp;quot;+2^47&amp;quot;,2.81475e+14,2.36118e+21],[&amp;quot;+2^48&amp;quot;,5.62949e+14,4.72236e+21],[&amp;quot;+2^49&amp;quot;,1.1259e+15,9.44472e+21],[&amp;quot;+2^50&amp;quot;,2.2518e+15,1.88894e+22],[&amp;quot;+2^51&amp;quot;,4.5036e+15,3.77789e+22],[&amp;quot;+2^52&amp;quot;,9.00719e+15,7.55578e+22],[&amp;quot;+2^53&amp;quot;,1.80144e+16,1.51116e+23],[&amp;quot;+2^54&amp;quot;,3.60288e+16,3.02231e+23],[&amp;quot;+2^55&amp;quot;,7.20575e+16,6.04462e+23],[&amp;quot;+2^56&amp;quot;,1.44115e+17,1.20892e+24],[&amp;quot;+2^57&amp;quot;,2.8823e+17,2.41785e+24],[&amp;quot;+2^58&amp;quot;,5.7646e+17,4.8357e+24],[&amp;quot;+2^59&amp;quot;,1.15292e+18,9.6714e+24],[&amp;quot;+2^60&amp;quot;,2.30584e+18,1.93428e+25],[&amp;quot;+2^61&amp;quot;,4.61168e+18,3.86856e+25],[&amp;quot;+2^62&amp;quot;,9.22336e+18,7.73712e+25],[&amp;quot;+2^63&amp;quot;,1.84467e+19,1.54742e+26],[&amp;quot;+2^64&amp;quot;,0,0],[&amp;quot;+2^65&amp;quot;,0,0],[&amp;quot;+2^66&amp;quot;,0,0],[&amp;quot;+2^67&amp;quot;,0,0],[&amp;quot;+2^68&amp;quot;,0,0],[&amp;quot;+2^69&amp;quot;,0,0],[&amp;quot;+2^70&amp;quot;,0,0],[&amp;quot;+2^71&amp;quot;,0,0],[&amp;quot;+2^72&amp;quot;,0,0],[&amp;quot;+2^73&amp;quot;,0,0],[&amp;quot;+2^74&amp;quot;,0,0],[&amp;quot;+2^75&amp;quot;,0,0],[&amp;quot;+2^76&amp;quot;,0,0],[&amp;quot;+2^77&amp;quot;,0,0],[&amp;quot;+2^78&amp;quot;,0,0],[&amp;quot;+2^79&amp;quot;,0,0],[&amp;quot;+2^80&amp;quot;,0,0],[&amp;quot;+2^81&amp;quot;,0,0],[&amp;quot;+2^82&amp;quot;,0,0],[&amp;quot;+2^83&amp;quot;,0,0],[&amp;quot;+2^84&amp;quot;,0,0],[&amp;quot;+2^85&amp;quot;,0,0],[&amp;quot;+2^86&amp;quot;,0,0],[&amp;quot;+2^87&amp;quot;,0,0],[&amp;quot;+2^88&amp;quot;,0,0],[&amp;quot;+2^89&amp;quot;,0,0],[&amp;quot;+2^90&amp;quot;,0,0],[&amp;quot;+2^91&amp;quot;,0,0],[&amp;quot;+2^92&amp;quot;,0,0],[&amp;quot;+2^93&amp;quot;,0,0],[&amp;quot;+2^94&amp;quot;,0,0],[&amp;quot;+2^95&amp;quot;,0,0],[&amp;quot;+2^96&amp;quot;,0,0],[&amp;quot;+2^97&amp;quot;,0,0],[&amp;quot;+2^98&amp;quot;,0,0],[&amp;quot;+2^99&amp;quot;,0,0],[&amp;quot;+2^100&amp;quot;,0,0],[&amp;quot;+2^101&amp;quot;,0,0],[&amp;quot;+2^102&amp;quot;,0,0],[&amp;quot;+2^103&amp;quot;,0,0],[&amp;quot;+2^104&amp;quot;,0,0],[&amp;quot;+2^105&amp;quot;,0,0],[&amp;quot;+2^106&amp;quot;,0,0],[&amp;quot;+2^107&amp;quot;,0,0],[&amp;quot;+2^108&amp;quot;,0,0],[&amp;quot;+2^109&amp;quot;,0,0],[&amp;quot;+2^110&amp;quot;,0,0],[&amp;quot;+2^111&amp;quot;,0,0],[&amp;quot;+2^112&amp;quot;,0,0],[&amp;quot;+2^113&amp;quot;,0,0],[&amp;quot;+2^114&amp;quot;,0,0],[&amp;quot;+2^115&amp;quot;,0,0],[&amp;quot;+2^116&amp;quot;,0,0],[&amp;quot;+2^117&amp;quot;,0,0],[&amp;quot;+2^118&amp;quot;,0,0],[&amp;quot;+2^119&amp;quot;,0,0],[&amp;quot;+2^120&amp;quot;,0,0],[&amp;quot;+2^121&amp;quot;,0,0],[&amp;quot;+2^122&amp;quot;,0,0],[&amp;quot;+2^123&amp;quot;,0,0],[&amp;quot;+2^124&amp;quot;,0,0],[&amp;quot;+2^125&amp;quot;,0,0],[&amp;quot;+2^126&amp;quot;,0,0],[&amp;quot;+2^127&amp;quot;,0,0] ]&lt;br /&gt;
 }}&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Key observation:&#039;&#039;&#039;  &lt;br /&gt;
The Taylor approximation is highly accurate near zero but diverges extremely fast outside its convergence region.  &lt;br /&gt;
This makes it unsuitable as a standalone &amp;lt;code&amp;gt;tanh&amp;lt;/code&amp;gt; implementation, but very effective as a local approximation when combined with range-based dispatching.&lt;br /&gt;
&lt;br /&gt;
== Localized Approximation Functions ==&lt;br /&gt;
&lt;br /&gt;
Localized approximation functions are designed to provide highly accurate and computationally efficient evaluations of &amp;lt;code&amp;gt;tanh(x)&amp;lt;/code&amp;gt; within a restricted input range. &lt;br /&gt;
They are typically used as building blocks in range-based dispatch strategies, where different approximations are selected depending on the magnitude of the input value.&lt;br /&gt;
&lt;br /&gt;
By limiting the valid input range, these approximations can achieve very low ULP error with simple polynomial or rational expressions, avoiding expensive transcendental functions.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
!Id&lt;br /&gt;
!Formula&lt;br /&gt;
!ULP &amp;lt;= 2.0&lt;br /&gt;
!NaN&lt;br /&gt;
|-&lt;br /&gt;
|signum&lt;br /&gt;
|&amp;lt;math&amp;gt;\sgn{x}&amp;lt;/math&amp;gt;&lt;br /&gt;
| -inf &amp;lt; x &amp;lt; -8.31763&lt;br /&gt;
8.31763 &amp;lt; x &amp;lt; inf&lt;br /&gt;
| -&lt;br /&gt;
|-&lt;br /&gt;
|poly1&lt;br /&gt;
|&amp;lt;math&amp;gt;x&amp;lt;/math&amp;gt;&lt;br /&gt;
| -0.000704229 &amp;lt; x &amp;lt; 0.000704229&lt;br /&gt;
| -&lt;br /&gt;
|-&lt;br /&gt;
|poly3&lt;br /&gt;
|&amp;lt;math&amp;gt;x - \frac{1}{3} x^3&amp;lt;/math&amp;gt;&lt;br /&gt;
| -0.0291781 &amp;lt; x &amp;lt; 0.0291781 &lt;br /&gt;
| -&lt;br /&gt;
|-&lt;br /&gt;
|lambert7&lt;br /&gt;
|&amp;lt;math&amp;gt;\frac{x \cdot (135135 + x^2 \cdot (17325 + x^2 \cdot (378 + x^2)))}{135135 + x^2 \cdot (62370 + x^2 \cdot (3150 + 28 \cdot x^2))}&lt;br /&gt;
&amp;lt;/math&amp;gt;&lt;br /&gt;
| -0.000947416 &amp;lt; x &amp;lt; 0.000947207&lt;br /&gt;
| -inf &amp;lt; x &amp;lt; -1.51629e+06&lt;br /&gt;
1.51629e+06 &amp;lt; x &amp;lt; inf&lt;br /&gt;
|-&lt;br /&gt;
|pade&lt;br /&gt;
|&amp;lt;math&amp;gt;x + x \cdot \frac{x^2 \cdot \left( p_1 \cdot x^2 + p_0 \right)}{\left( x^2 + q_1 \right) \cdot x^2 + q_0} &lt;br /&gt;
&lt;br /&gt;
&amp;lt;/math&amp;gt; &amp;lt;math&amp;gt;\begin{align}&lt;br /&gt;
p_0 &amp;amp;= -0.2059432032 \\&lt;br /&gt;
p_1 &amp;amp;= -0.0009577527 \\&lt;br /&gt;
q_0 &amp;amp;= 0.6178299136 \\ &lt;br /&gt;
q_1 &amp;amp;= 0.25&lt;br /&gt;
\end{align}&lt;br /&gt;
&amp;lt;/math&amp;gt;&lt;br /&gt;
| -0.0016973 &amp;lt; x &amp;lt; 0.0016973 &lt;br /&gt;
| -inf &amp;lt; x &amp;lt; -6.14896e+18&lt;br /&gt;
6.14896e+18 &amp;lt; x &amp;lt; inf&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
The functions listed above are not intended to be used independently over the full input domain. &lt;br /&gt;
Instead, they are commonly combined with explicit range checks and sign handling to form a complete &amp;lt;code&amp;gt;tanh&amp;lt;/code&amp;gt; implementation.&lt;br /&gt;
&lt;br /&gt;
For example, low-order polynomials are well suited for very small &amp;lt;math&amp;gt;|x|&amp;lt;/math&amp;gt;, while rational or higher-order approximations extend the usable range before switching to exponential-based formulations or saturation.&lt;br /&gt;
&lt;br /&gt;
== External Links ==&lt;br /&gt;
&lt;br /&gt;
* https://yaikhom.com/2020-04-28-localised-approximation-of-hyperbolic-tangents.html#mjx-eqn-eqn7-th%20degree%20lambert%20approximant&lt;br /&gt;
*https://github.com/llvm/llvm-project/blob/main/libc/src/math/generic/tanhf.cpp&lt;br /&gt;
*https://forums.developer.nvidia.com/t/hardware-accelerated-tanh-on-turing/173291&lt;br /&gt;
[[Category:Numerical Precision]]&lt;/div&gt;</summary>
		<author><name>Timo.stripf</name></author>
	</entry>
	<entry>
		<id>https://www.emmtrix.com/w139/index.php?title=tanh_Software_Implementation&amp;diff=2934</id>
		<title>tanh Software Implementation</title>
		<link rel="alternate" type="text/html" href="https://www.emmtrix.com/w139/index.php?title=tanh_Software_Implementation&amp;diff=2934"/>
		<updated>2026-02-04T12:08:41Z</updated>

		<summary type="html">&lt;p&gt;Timo.stripf: /* Localized Approximation Functions */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;[[File:Hyperbolic Tangent.svg|thumb]]&lt;br /&gt;
The hyperbolic tangent function, commonly referred to as &amp;lt;code&amp;gt;tanh&amp;lt;/code&amp;gt;, is a mathematical function that maps real numbers to the range &amp;lt;math&amp;gt;(-1, 1)&amp;lt;/math&amp;gt;. It is defined as:&lt;br /&gt;
&lt;br /&gt;
:&amp;lt;math&amp;gt;\tanh(x) = \frac{e^x - e^{-x}}{e^x + e^{-x}}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
From a software implementation perspective, &amp;lt;code&amp;gt;tanh&amp;lt;/code&amp;gt; is a numerically challenging function due to the use of exponential functions, which can easily overflow for large inputs and suffer from catastrophic cancellation for very small values of &amp;lt;math&amp;gt;x&amp;lt;/math&amp;gt;. These issues are particularly relevant in single-precision floating-point (&amp;lt;code&amp;gt;float32&amp;lt;/code&amp;gt;) implementations, where both accuracy and performance are critical.&lt;br /&gt;
&lt;br /&gt;
In machine learning and deep learning, &amp;lt;code&amp;gt;tanh&amp;lt;/code&amp;gt; is widely used as an activation function because it introduces non-linearity and produces outputs centered around zero, which can improve gradient-based optimization. As a result, &amp;lt;code&amp;gt;tanh&amp;lt;/code&amp;gt; is often evaluated billions of times during training and inference, making efficient and numerically stable implementations essential.&lt;br /&gt;
&lt;br /&gt;
This article focuses on practical software implementations of &amp;lt;code&amp;gt;tanh&amp;lt;/code&amp;gt;, analyzing different computational formulations with respect to floating-point accuracy, ULP error, overflow behavior, and NaN generation. The goal is to provide a foundation for designing robust &amp;lt;code&amp;gt;tanh&amp;lt;/code&amp;gt; implementations suitable for performance-critical environments such as numerical libraries, embedded systems, and AI workloads.&lt;br /&gt;
&lt;br /&gt;
== Problem Overview ==&lt;br /&gt;
The hyperbolic tangent function poses several challenges when implemented in finite-precision floating-point arithmetic. While the mathematical definition is simple, direct translations into software often suffer from numerical instability and performance issues.&lt;br /&gt;
&lt;br /&gt;
A primary challenge is the use of exponential functions. For large input values, &amp;lt;code&amp;gt;exp(x)&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;exp(-x)&amp;lt;/code&amp;gt; can overflow in single-precision floating-point arithmetic, leading to infinities and, in some formulations, NaN results. Avoiding overflow therefore requires careful reformulation or explicit range handling.&lt;br /&gt;
&lt;br /&gt;
For very small input values, catastrophic cancellation becomes the dominant issue. Expressions such as &amp;lt;math&amp;gt;e^x - e^{-x}&amp;lt;/math&amp;gt; involve the subtraction of nearly equal numbers, causing a severe loss of significant bits and large ULP errors. In these regions, naive implementations may return zero or highly inaccurate results even though &amp;lt;math&amp;gt;\tanh(x) \approx x&amp;lt;/math&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
In addition to accuracy concerns, performance is a critical factor. In many applications, especially machine learning and numerical simulations, &amp;lt;code&amp;gt;tanh&amp;lt;/code&amp;gt; is evaluated extremely frequently. This makes it necessary to balance numerical robustness with the cost of transcendental function calls, branch complexity, and instruction-level efficiency.&lt;br /&gt;
&lt;br /&gt;
==Direct Exponential Formulations==&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+&lt;br /&gt;
!Id&lt;br /&gt;
!Formula&lt;br /&gt;
!Highest ULP Error&lt;br /&gt;
!ULP &amp;lt;= 2.0&lt;br /&gt;
!NaN&lt;br /&gt;
|-&lt;br /&gt;
|exp_v1&lt;br /&gt;
|&amp;lt;math&amp;gt;\frac{e^x - e^{-x}}{e^x + e^{-x}}&amp;lt;/math&amp;gt;&lt;br /&gt;
|x &amp;lt; 0: 8.55638e+08 at -2.98023e-08 with 0x0p+0 instead of -0x1p-25&lt;br /&gt;
&lt;br /&gt;
x &amp;gt; 0: 8.55638e+08 at 2.98023e-08 with 0x0p+0 instead of 0x1p-25&lt;br /&gt;
| -88.7246 &amp;lt; x &amp;lt; -7.14197&lt;br /&gt;
7.14197 &amp;lt; x &amp;lt; 88.7246&lt;br /&gt;
| -inf &amp;lt; x &amp;lt; -88.7246&lt;br /&gt;
88.7246 &amp;lt; x &amp;lt; inf&lt;br /&gt;
|-&lt;br /&gt;
|exp_v2&lt;br /&gt;
|&amp;lt;math&amp;gt;\frac{e^{2x} - 1}{e^{2x} + 1}&amp;lt;/math&amp;gt;&lt;br /&gt;
|x &amp;lt; 0: 8.47249e+08 at -1.49012e-08 with 0x0p+0 instead of -0x1p-26&lt;br /&gt;
x &amp;gt; 0: 8.55638e+08 at 2.98023e-08 with 0x0p+0 instead of 0x1.fffffep-26&lt;br /&gt;
| -inf &amp;lt; x &amp;lt; -0.547821&lt;br /&gt;
0.254822 &amp;lt; x &amp;lt; 8.31787&lt;br /&gt;
&lt;br /&gt;
8.38379 &amp;lt; x &amp;lt; 44.3623&lt;br /&gt;
|44.3623 &amp;lt; x &amp;lt; inf&lt;br /&gt;
|-&lt;br /&gt;
|exp_v3&lt;br /&gt;
|&amp;lt;math&amp;gt;1 - \frac{2}{e^{2x} + 1}&amp;lt;/math&amp;gt;&lt;br /&gt;
|x &amp;lt; 0: 8.59832e+08 at -4.47035e-08 with 0x0p+0 instead of -0x1.8p-25&lt;br /&gt;
x &amp;gt; 0: 8.68221e+08 at 8.9407e-08 with 0x0p+0 instead of 0x1.7ffffcp-24&lt;br /&gt;
| -inf &amp;lt; x &amp;lt; -6.26233&lt;br /&gt;
0.346382 &amp;lt; x &amp;lt; inf&lt;br /&gt;
| -&lt;br /&gt;
|-&lt;br /&gt;
|expm1&lt;br /&gt;
|&amp;lt;math&amp;gt;\frac{expm1(2x)}{expm1(2x) + 2}&amp;lt;/math&amp;gt; &lt;br /&gt;
|x &amp;lt; 0: 2.49585 at -3.95627 with -0x1.ffa00ap-1 instead of -0x1.ffa00ep-1&lt;br /&gt;
x &amp;gt; 0: 2.41657 at 0.0155837 with 0x1.fe9b66p-7 instead of 0x1.fe9b62p-7&lt;br /&gt;
| -inf &amp;lt; x &amp;lt; -5.57227&lt;br /&gt;
-0.42981 &amp;lt; x &amp;lt; 0.00777948&lt;br /&gt;
&lt;br /&gt;
0.125351 &amp;lt; x &amp;lt; 44.3623&lt;br /&gt;
|44.3623 &amp;lt; x &amp;lt; inf&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
The first equation is the definition of the hyperbolic tangent function. This equation requires two exponentials with different values. Since exponentials are expensive to compute, we can use the second equation to reduce the number of exponentials to one.&lt;br /&gt;
&lt;br /&gt;
The second equation has the drawback that both the numerator and the denominator can become infinity for large values of x which results in NaN. That happens for &amp;lt;math&amp;gt;|x| &amp;gt; \log(FLT\_MAX)/2 \approx 44&amp;lt;/math&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
The third equation solves this problem but has a slightly higher error for &amp;lt;math&amp;gt;x \approx 2^{-25}&amp;lt;/math&amp;gt;. In the following graph you can see the error chart for equation 3. The error charts for the other two equations are very similar. All three equations have an error of up to 16 mio ULPs for &amp;lt;math&amp;gt;x &amp;lt; 2^{-5}&amp;lt;/math&amp;gt; which means that up to 24 bits could be wrong.&lt;br /&gt;
&lt;br /&gt;
=== exp_v2 ===&lt;br /&gt;
&lt;br /&gt;
The following error graph shows the ULP error of the &amp;lt;code&amp;gt;exp_v2&amp;lt;/code&amp;gt; formulation across the full &amp;lt;code&amp;gt;float32&amp;lt;/code&amp;gt; input range. &lt;br /&gt;
This formulation reduces the number of exponential evaluations to one, but is particularly sensitive to catastrophic cancellation for very small input values and to overflow for large &amp;lt;math&amp;gt;|x|&amp;lt;/math&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
{{#widget:FloatErrorGraph|id=tanhf_exp_v2&lt;br /&gt;
  |data=[ [&amp;quot;-2^127&amp;quot;,0,1.17549e-38],[&amp;quot;-2^126&amp;quot;,0,1.17549e-38],[&amp;quot;-2^125&amp;quot;,0,1.17549e-38],[&amp;quot;-2^124&amp;quot;,0,1.17549e-38],[&amp;quot;-2^123&amp;quot;,0,1.17549e-38],[&amp;quot;-2^122&amp;quot;,0,1.17549e-38],[&amp;quot;-2^121&amp;quot;,0,1.17549e-38],[&amp;quot;-2^120&amp;quot;,0,1.17549e-38],[&amp;quot;-2^119&amp;quot;,0,1.17549e-38],[&amp;quot;-2^118&amp;quot;,0,1.17549e-38],[&amp;quot;-2^117&amp;quot;,0,1.17549e-38],[&amp;quot;-2^116&amp;quot;,0,1.17549e-38],[&amp;quot;-2^115&amp;quot;,0,1.17549e-38],[&amp;quot;-2^114&amp;quot;,0,1.17549e-38],[&amp;quot;-2^113&amp;quot;,0,1.17549e-38],[&amp;quot;-2^112&amp;quot;,0,1.17549e-38],[&amp;quot;-2^111&amp;quot;,0,1.17549e-38],[&amp;quot;-2^110&amp;quot;,0,1.17549e-38],[&amp;quot;-2^109&amp;quot;,0,1.17549e-38],[&amp;quot;-2^108&amp;quot;,0,1.17549e-38],[&amp;quot;-2^107&amp;quot;,0,1.17549e-38],[&amp;quot;-2^106&amp;quot;,0,1.17549e-38],[&amp;quot;-2^105&amp;quot;,0,1.17549e-38],[&amp;quot;-2^104&amp;quot;,0,1.17549e-38],[&amp;quot;-2^103&amp;quot;,0,1.17549e-38],[&amp;quot;-2^102&amp;quot;,0,1.17549e-38],[&amp;quot;-2^101&amp;quot;,0,1.17549e-38],[&amp;quot;-2^100&amp;quot;,0,1.17549e-38],[&amp;quot;-2^99&amp;quot;,0,1.17549e-38],[&amp;quot;-2^98&amp;quot;,0,1.17549e-38],[&amp;quot;-2^97&amp;quot;,0,1.17549e-38],[&amp;quot;-2^96&amp;quot;,0,1.17549e-38],[&amp;quot;-2^95&amp;quot;,0,1.17549e-38],[&amp;quot;-2^94&amp;quot;,0,1.17549e-38],[&amp;quot;-2^93&amp;quot;,0,1.17549e-38],[&amp;quot;-2^92&amp;quot;,0,1.17549e-38],[&amp;quot;-2^91&amp;quot;,0,1.17549e-38],[&amp;quot;-2^90&amp;quot;,0,1.17549e-38],[&amp;quot;-2^89&amp;quot;,0,1.17549e-38],[&amp;quot;-2^88&amp;quot;,0,1.17549e-38],[&amp;quot;-2^87&amp;quot;,0,1.17549e-38],[&amp;quot;-2^86&amp;quot;,0,1.17549e-38],[&amp;quot;-2^85&amp;quot;,0,1.17549e-38],[&amp;quot;-2^84&amp;quot;,0,1.17549e-38],[&amp;quot;-2^83&amp;quot;,0,1.17549e-38],[&amp;quot;-2^82&amp;quot;,0,1.17549e-38],[&amp;quot;-2^81&amp;quot;,0,1.17549e-38],[&amp;quot;-2^80&amp;quot;,0,1.17549e-38],[&amp;quot;-2^79&amp;quot;,0,1.17549e-38],[&amp;quot;-2^78&amp;quot;,0,1.17549e-38],[&amp;quot;-2^77&amp;quot;,0,1.17549e-38],[&amp;quot;-2^76&amp;quot;,0,1.17549e-38],[&amp;quot;-2^75&amp;quot;,0,1.17549e-38],[&amp;quot;-2^74&amp;quot;,0,1.17549e-38],[&amp;quot;-2^73&amp;quot;,0,1.17549e-38],[&amp;quot;-2^72&amp;quot;,0,1.17549e-38],[&amp;quot;-2^71&amp;quot;,0,1.17549e-38],[&amp;quot;-2^70&amp;quot;,0,1.17549e-38],[&amp;quot;-2^69&amp;quot;,0,1.17549e-38],[&amp;quot;-2^68&amp;quot;,0,1.17549e-38],[&amp;quot;-2^67&amp;quot;,0,1.17549e-38],[&amp;quot;-2^66&amp;quot;,0,1.17549e-38],[&amp;quot;-2^65&amp;quot;,0,1.17549e-38],[&amp;quot;-2^64&amp;quot;,0,1.17549e-38],[&amp;quot;-2^63&amp;quot;,0,1.17549e-38],[&amp;quot;-2^62&amp;quot;,0,1.17549e-38],[&amp;quot;-2^61&amp;quot;,0,1.17549e-38],[&amp;quot;-2^60&amp;quot;,0,1.17549e-38],[&amp;quot;-2^59&amp;quot;,0,1.17549e-38],[&amp;quot;-2^58&amp;quot;,0,1.17549e-38],[&amp;quot;-2^57&amp;quot;,0,1.17549e-38],[&amp;quot;-2^56&amp;quot;,0,1.17549e-38],[&amp;quot;-2^55&amp;quot;,0,1.17549e-38],[&amp;quot;-2^54&amp;quot;,0,1.17549e-38],[&amp;quot;-2^53&amp;quot;,0,1.17549e-38],[&amp;quot;-2^52&amp;quot;,0,1.17549e-38],[&amp;quot;-2^51&amp;quot;,0,1.17549e-38],[&amp;quot;-2^50&amp;quot;,0,1.17549e-38],[&amp;quot;-2^49&amp;quot;,0,1.17549e-38],[&amp;quot;-2^48&amp;quot;,0,1.17549e-38],[&amp;quot;-2^47&amp;quot;,0,1.17549e-38],[&amp;quot;-2^46&amp;quot;,0,1.17549e-38],[&amp;quot;-2^45&amp;quot;,0,1.17549e-38],[&amp;quot;-2^44&amp;quot;,0,1.17549e-38],[&amp;quot;-2^43&amp;quot;,0,1.17549e-38],[&amp;quot;-2^42&amp;quot;,0,1.17549e-38],[&amp;quot;-2^41&amp;quot;,0,1.17549e-38],[&amp;quot;-2^40&amp;quot;,0,1.17549e-38],[&amp;quot;-2^39&amp;quot;,0,1.17549e-38],[&amp;quot;-2^38&amp;quot;,0,1.17549e-38],[&amp;quot;-2^37&amp;quot;,0,1.17549e-38],[&amp;quot;-2^36&amp;quot;,0,1.17549e-38],[&amp;quot;-2^35&amp;quot;,0,1.17549e-38],[&amp;quot;-2^34&amp;quot;,0,1.17549e-38],[&amp;quot;-2^33&amp;quot;,0,1.17549e-38],[&amp;quot;-2^32&amp;quot;,0,1.17549e-38],[&amp;quot;-2^31&amp;quot;,0,1.17549e-38],[&amp;quot;-2^30&amp;quot;,0,1.17549e-38],[&amp;quot;-2^29&amp;quot;,0,1.17549e-38],[&amp;quot;-2^28&amp;quot;,0,1.17549e-38],[&amp;quot;-2^27&amp;quot;,0,1.17549e-38],[&amp;quot;-2^26&amp;quot;,0,1.17549e-38],[&amp;quot;-2^25&amp;quot;,0,1.17549e-38],[&amp;quot;-2^24&amp;quot;,0,1.17549e-38],[&amp;quot;-2^23&amp;quot;,0,1.17549e-38],[&amp;quot;-2^22&amp;quot;,0,1.17549e-38],[&amp;quot;-2^21&amp;quot;,0,1.17549e-38],[&amp;quot;-2^20&amp;quot;,0,1.17549e-38],[&amp;quot;-2^19&amp;quot;,0,1.17549e-38],[&amp;quot;-2^18&amp;quot;,0,1.17549e-38],[&amp;quot;-2^17&amp;quot;,0,1.17549e-38],[&amp;quot;-2^16&amp;quot;,0,1.17549e-38],[&amp;quot;-2^15&amp;quot;,0,1.17549e-38],[&amp;quot;-2^14&amp;quot;,0,1.17549e-38],[&amp;quot;-2^13&amp;quot;,0,1.17549e-38],[&amp;quot;-2^12&amp;quot;,0,1.17549e-38],[&amp;quot;-2^11&amp;quot;,0,1.17549e-38],[&amp;quot;-2^10&amp;quot;,0,1.17549e-38],[&amp;quot;-2^9&amp;quot;,0,1.17549e-38],[&amp;quot;-2^8&amp;quot;,0,1.17549e-38],[&amp;quot;-2^7&amp;quot;,0,1.17549e-38],[&amp;quot;-2^6&amp;quot;,0,1.17549e-38],[&amp;quot;-2^5&amp;quot;,0,1.17549e-38],[&amp;quot;-2^4&amp;quot;,2.53131e-14,4.24683e-07],[&amp;quot;-2^3&amp;quot;,5.95888e-08,0.999734],[&amp;quot;-2^2&amp;quot;,8.86696e-08,1.48763],[&amp;quot;-2^1&amp;quot;,8.91609e-08,1.49587],[&amp;quot;-2^0&amp;quot;,8.73743e-08,1.4659],[&amp;quot;-2^-1&amp;quot;,8.65382e-08,2.13222],[&amp;quot;-2^-2&amp;quot;,6.18481e-08,2.61428],[&amp;quot;-2^-3&amp;quot;,3.88071e-08,3.08594],[&amp;quot;-2^-4&amp;quot;,2.6234e-08,4.23594],[&amp;quot;-2^-5&amp;quot;,2.02799e-08,8.19726],[&amp;quot;-2^-6&amp;quot;,1.75183e-08,12.4495],[&amp;quot;-2^-7&amp;quot;,1.61937e-08,17.3878],[&amp;quot;-2^-8&amp;quot;,1.5566e-08,33.4278],[&amp;quot;-2^-9&amp;quot;,1.51688e-08,65.1495],[&amp;quot;-2^-10&amp;quot;,1.50281e-08,129.09],[&amp;quot;-2^-11&amp;quot;,1.49613e-08,257.033],[&amp;quot;-2^-12&amp;quot;,1.49296e-08,512.977],[&amp;quot;-2^-13&amp;quot;,1.49174e-08,1025.12],[&amp;quot;-2^-14&amp;quot;,1.4909e-08,2049.08],[&amp;quot;-2^-15&amp;quot;,1.49049e-08,4097.02],[&amp;quot;-2^-16&amp;quot;,1.4903e-08,8193],[&amp;quot;-2^-17&amp;quot;,1.49021e-08,16385],[&amp;quot;-2^-18&amp;quot;,1.48862e-08,32735],[&amp;quot;-2^-19&amp;quot;,1.48973e-08,65519],[&amp;quot;-2^-20&amp;quot;,1.49001e-08,131063],[&amp;quot;-2^-21&amp;quot;,1.49009e-08,262139],[&amp;quot;-2^-22&amp;quot;,1.49011e-08,524285],[&amp;quot;-2^-23&amp;quot;,1.49011e-08,1.04857e+06],[&amp;quot;-2^-24&amp;quot;,1.49011e-08,2.09715e+06],[&amp;quot;-2^-25&amp;quot;,1.49012e-08,4.1943e+06],[&amp;quot;-2^-26&amp;quot;,1.49007e-08,8.38835e+06],[&amp;quot;-2^-27&amp;quot;,1.49012e-08,8.47249e+08],[&amp;quot;-2^-28&amp;quot;,7.45058e-09,8.38861e+08],[&amp;quot;-2^-29&amp;quot;,3.72529e-09,8.30472e+08],[&amp;quot;-2^-30&amp;quot;,1.86265e-09,8.22084e+08],[&amp;quot;-2^-31&amp;quot;,9.31323e-10,8.13695e+08],[&amp;quot;-2^-32&amp;quot;,4.65661e-10,8.05306e+08],[&amp;quot;-2^-33&amp;quot;,2.32831e-10,7.96918e+08],[&amp;quot;-2^-34&amp;quot;,1.16415e-10,7.88529e+08],[&amp;quot;-2^-35&amp;quot;,5.82077e-11,7.80141e+08],[&amp;quot;-2^-36&amp;quot;,2.91038e-11,7.71752e+08],[&amp;quot;-2^-37&amp;quot;,1.45519e-11,7.63363e+08],[&amp;quot;-2^-38&amp;quot;,7.27596e-12,7.54975e+08],[&amp;quot;-2^-39&amp;quot;,3.63798e-12,7.46586e+08],[&amp;quot;-2^-40&amp;quot;,1.81899e-12,7.38198e+08],[&amp;quot;-2^-41&amp;quot;,9.09495e-13,7.29809e+08],[&amp;quot;-2^-42&amp;quot;,4.54747e-13,7.2142e+08],[&amp;quot;-2^-43&amp;quot;,2.27374e-13,7.13032e+08],[&amp;quot;-2^-44&amp;quot;,1.13687e-13,7.04643e+08],[&amp;quot;-2^-45&amp;quot;,5.68434e-14,6.96254e+08],[&amp;quot;-2^-46&amp;quot;,2.84217e-14,6.87866e+08],[&amp;quot;-2^-47&amp;quot;,1.42109e-14,6.79477e+08],[&amp;quot;-2^-48&amp;quot;,7.10543e-15,6.71089e+08],[&amp;quot;-2^-49&amp;quot;,3.55271e-15,6.627e+08],[&amp;quot;-2^-50&amp;quot;,1.77636e-15,6.54311e+08],[&amp;quot;-2^-51&amp;quot;,8.88178e-16,6.45923e+08],[&amp;quot;-2^-52&amp;quot;,4.44089e-16,6.37534e+08],[&amp;quot;-2^-53&amp;quot;,2.22045e-16,6.29146e+08],[&amp;quot;-2^-54&amp;quot;,1.11022e-16,6.20757e+08],[&amp;quot;-2^-55&amp;quot;,5.55111e-17,6.12368e+08],[&amp;quot;-2^-56&amp;quot;,2.77556e-17,6.0398e+08],[&amp;quot;-2^-57&amp;quot;,1.38778e-17,5.95591e+08],[&amp;quot;-2^-58&amp;quot;,6.93889e-18,5.87203e+08],[&amp;quot;-2^-59&amp;quot;,3.46945e-18,5.78814e+08],[&amp;quot;-2^-60&amp;quot;,1.73472e-18,5.70425e+08],[&amp;quot;-2^-61&amp;quot;,8.67362e-19,5.62037e+08],[&amp;quot;-2^-62&amp;quot;,4.33681e-19,5.53648e+08],[&amp;quot;-2^-63&amp;quot;,2.1684e-19,5.4526e+08],[&amp;quot;-2^-64&amp;quot;,1.0842e-19,5.36871e+08],[&amp;quot;-2^-65&amp;quot;,5.42101e-20,5.28482e+08],[&amp;quot;-2^-66&amp;quot;,2.71051e-20,5.20094e+08],[&amp;quot;-2^-67&amp;quot;,1.35525e-20,5.11705e+08],[&amp;quot;-2^-68&amp;quot;,6.77626e-21,5.03316e+08],[&amp;quot;-2^-69&amp;quot;,3.38813e-21,4.94928e+08],[&amp;quot;-2^-70&amp;quot;,1.69407e-21,4.86539e+08],[&amp;quot;-2^-71&amp;quot;,8.47033e-22,4.78151e+08],[&amp;quot;-2^-72&amp;quot;,4.23516e-22,4.69762e+08],[&amp;quot;-2^-73&amp;quot;,2.11758e-22,4.61373e+08],[&amp;quot;-2^-74&amp;quot;,1.05879e-22,4.52985e+08],[&amp;quot;-2^-75&amp;quot;,5.29396e-23,4.44596e+08],[&amp;quot;-2^-76&amp;quot;,2.64698e-23,4.36208e+08],[&amp;quot;-2^-77&amp;quot;,1.32349e-23,4.27819e+08],[&amp;quot;-2^-78&amp;quot;,6.61744e-24,4.1943e+08],[&amp;quot;-2^-79&amp;quot;,3.30872e-24,4.11042e+08],[&amp;quot;-2^-80&amp;quot;,1.65436e-24,4.02653e+08],[&amp;quot;-2^-81&amp;quot;,8.27181e-25,3.94265e+08],[&amp;quot;-2^-82&amp;quot;,4.1359e-25,3.85876e+08],[&amp;quot;-2^-83&amp;quot;,2.06795e-25,3.77487e+08],[&amp;quot;-2^-84&amp;quot;,1.03398e-25,3.69099e+08],[&amp;quot;-2^-85&amp;quot;,5.16988e-26,3.6071e+08],[&amp;quot;-2^-86&amp;quot;,2.58494e-26,3.52322e+08],[&amp;quot;-2^-87&amp;quot;,1.29247e-26,3.43933e+08],[&amp;quot;-2^-88&amp;quot;,6.46235e-27,3.35544e+08],[&amp;quot;-2^-89&amp;quot;,3.23117e-27,3.27156e+08],[&amp;quot;-2^-90&amp;quot;,1.61559e-27,3.18767e+08],[&amp;quot;-2^-91&amp;quot;,8.07794e-28,3.10378e+08],[&amp;quot;-2^-92&amp;quot;,4.03897e-28,3.0199e+08],[&amp;quot;-2^-93&amp;quot;,2.01948e-28,2.93601e+08],[&amp;quot;-2^-94&amp;quot;,1.00974e-28,2.85213e+08],[&amp;quot;-2^-95&amp;quot;,5.04871e-29,2.76824e+08],[&amp;quot;-2^-96&amp;quot;,2.52435e-29,2.68435e+08],[&amp;quot;-2^-97&amp;quot;,1.26218e-29,2.60047e+08],[&amp;quot;-2^-98&amp;quot;,6.31089e-30,2.51658e+08],[&amp;quot;-2^-99&amp;quot;,3.15544e-30,2.4327e+08],[&amp;quot;-2^-100&amp;quot;,1.57772e-30,2.34881e+08],[&amp;quot;-2^-101&amp;quot;,7.88861e-31,2.26492e+08],[&amp;quot;-2^-102&amp;quot;,3.9443e-31,2.18104e+08],[&amp;quot;-2^-103&amp;quot;,1.97215e-31,2.09715e+08],[&amp;quot;-2^-104&amp;quot;,9.86076e-32,2.01327e+08],[&amp;quot;-2^-105&amp;quot;,4.93038e-32,1.92938e+08],[&amp;quot;-2^-106&amp;quot;,2.46519e-32,1.84549e+08],[&amp;quot;-2^-107&amp;quot;,1.2326e-32,1.76161e+08],[&amp;quot;-2^-108&amp;quot;,6.16298e-33,1.67772e+08],[&amp;quot;-2^-109&amp;quot;,3.08149e-33,1.59384e+08],[&amp;quot;-2^-110&amp;quot;,1.54074e-33,1.50995e+08],[&amp;quot;-2^-111&amp;quot;,7.70372e-34,1.42606e+08],[&amp;quot;-2^-112&amp;quot;,3.85186e-34,1.34218e+08],[&amp;quot;-2^-113&amp;quot;,1.92593e-34,1.25829e+08],[&amp;quot;-2^-114&amp;quot;,9.62965e-35,1.17441e+08],[&amp;quot;-2^-115&amp;quot;,4.81482e-35,1.09052e+08],[&amp;quot;-2^-116&amp;quot;,2.40741e-35,1.00663e+08],[&amp;quot;-2^-117&amp;quot;,1.20371e-35,9.22747e+07],[&amp;quot;-2^-118&amp;quot;,6.01853e-36,8.38861e+07],[&amp;quot;-2^-119&amp;quot;,3.00927e-36,7.54975e+07],[&amp;quot;-2^-120&amp;quot;,1.50463e-36,6.71089e+07],[&amp;quot;-2^-121&amp;quot;,7.52316e-37,5.87203e+07],[&amp;quot;-2^-122&amp;quot;,3.76158e-37,5.03316e+07],[&amp;quot;-2^-123&amp;quot;,1.88079e-37,4.1943e+07],[&amp;quot;-2^-124&amp;quot;,9.40395e-38,3.35544e+07],[&amp;quot;-2^-125&amp;quot;,4.70198e-38,2.51658e+07],[&amp;quot;-2^-126&amp;quot;,2.35099e-38,1.67772e+07],[&amp;quot;-SN&amp;quot;,1.17546e-38,8.38835e+06],[&amp;quot;0&amp;quot;,0,1.17549e-38],[&amp;quot;+SN&amp;quot;,1.17546e-38,8.38835e+06],[&amp;quot;+2^-126&amp;quot;,2.35095e-38,1.6777e+07],[&amp;quot;+2^-125&amp;quot;,4.70191e-38,2.51656e+07],[&amp;quot;+2^-124&amp;quot;,9.40381e-38,3.35542e+07],[&amp;quot;+2^-123&amp;quot;,1.88076e-37,4.19428e+07],[&amp;quot;+2^-122&amp;quot;,3.76152e-37,5.03314e+07],[&amp;quot;+2^-121&amp;quot;,7.52305e-37,5.872e+07],[&amp;quot;+2^-120&amp;quot;,1.50461e-36,6.71086e+07],[&amp;quot;+2^-119&amp;quot;,3.00922e-36,7.54972e+07],[&amp;quot;+2^-118&amp;quot;,6.01844e-36,8.38858e+07],[&amp;quot;+2^-117&amp;quot;,1.20369e-35,9.22744e+07],[&amp;quot;+2^-116&amp;quot;,2.40738e-35,1.00663e+08],[&amp;quot;+2^-115&amp;quot;,4.81475e-35,1.09052e+08],[&amp;quot;+2^-114&amp;quot;,9.6295e-35,1.1744e+08],[&amp;quot;+2^-113&amp;quot;,1.9259e-34,1.25829e+08],[&amp;quot;+2^-112&amp;quot;,3.8518e-34,1.34217e+08],[&amp;quot;+2^-111&amp;quot;,7.7036e-34,1.42606e+08],[&amp;quot;+2^-110&amp;quot;,1.54072e-33,1.50995e+08],[&amp;quot;+2^-109&amp;quot;,3.08144e-33,1.59383e+08],[&amp;quot;+2^-108&amp;quot;,6.16288e-33,1.67772e+08],[&amp;quot;+2^-107&amp;quot;,1.23258e-32,1.76161e+08],[&amp;quot;+2^-106&amp;quot;,2.46515e-32,1.84549e+08],[&amp;quot;+2^-105&amp;quot;,4.93031e-32,1.92938e+08],[&amp;quot;+2^-104&amp;quot;,9.86061e-32,2.01326e+08],[&amp;quot;+2^-103&amp;quot;,1.97212e-31,2.09715e+08],[&amp;quot;+2^-102&amp;quot;,3.94424e-31,2.18104e+08],[&amp;quot;+2^-101&amp;quot;,7.88849e-31,2.26492e+08],[&amp;quot;+2^-100&amp;quot;,1.5777e-30,2.34881e+08],[&amp;quot;+2^-99&amp;quot;,3.1554e-30,2.43269e+08],[&amp;quot;+2^-98&amp;quot;,6.31079e-30,2.51658e+08],[&amp;quot;+2^-97&amp;quot;,1.26216e-29,2.60047e+08],[&amp;quot;+2^-96&amp;quot;,2.52432e-29,2.68435e+08],[&amp;quot;+2^-95&amp;quot;,5.04863e-29,2.76824e+08],[&amp;quot;+2^-94&amp;quot;,1.00973e-28,2.85212e+08],[&amp;quot;+2^-93&amp;quot;,2.01945e-28,2.93601e+08],[&amp;quot;+2^-92&amp;quot;,4.03891e-28,3.0199e+08],[&amp;quot;+2^-91&amp;quot;,8.07781e-28,3.10378e+08],[&amp;quot;+2^-90&amp;quot;,1.61556e-27,3.18767e+08],[&amp;quot;+2^-89&amp;quot;,3.23112e-27,3.27155e+08],[&amp;quot;+2^-88&amp;quot;,6.46225e-27,3.35544e+08],[&amp;quot;+2^-87&amp;quot;,1.29245e-26,3.43933e+08],[&amp;quot;+2^-86&amp;quot;,2.5849e-26,3.52321e+08],[&amp;quot;+2^-85&amp;quot;,5.1698e-26,3.6071e+08],[&amp;quot;+2^-84&amp;quot;,1.03396e-25,3.69098e+08],[&amp;quot;+2^-83&amp;quot;,2.06792e-25,3.77487e+08],[&amp;quot;+2^-82&amp;quot;,4.13584e-25,3.85876e+08],[&amp;quot;+2^-81&amp;quot;,8.27168e-25,3.94264e+08],[&amp;quot;+2^-80&amp;quot;,1.65434e-24,4.02653e+08],[&amp;quot;+2^-79&amp;quot;,3.30867e-24,4.11042e+08],[&amp;quot;+2^-78&amp;quot;,6.61734e-24,4.1943e+08],[&amp;quot;+2^-77&amp;quot;,1.32347e-23,4.27819e+08],[&amp;quot;+2^-76&amp;quot;,2.64694e-23,4.36207e+08],[&amp;quot;+2^-75&amp;quot;,5.29388e-23,4.44596e+08],[&amp;quot;+2^-74&amp;quot;,1.05878e-22,4.52985e+08],[&amp;quot;+2^-73&amp;quot;,2.11755e-22,4.61373e+08],[&amp;quot;+2^-72&amp;quot;,4.2351e-22,4.69762e+08],[&amp;quot;+2^-71&amp;quot;,8.4702e-22,4.7815e+08],[&amp;quot;+2^-70&amp;quot;,1.69404e-21,4.86539e+08],[&amp;quot;+2^-69&amp;quot;,3.38808e-21,4.94928e+08],[&amp;quot;+2^-68&amp;quot;,6.77616e-21,5.03316e+08],[&amp;quot;+2^-67&amp;quot;,1.35523e-20,5.11705e+08],[&amp;quot;+2^-66&amp;quot;,2.71046e-20,5.20093e+08],[&amp;quot;+2^-65&amp;quot;,5.42093e-20,5.28482e+08],[&amp;quot;+2^-64&amp;quot;,1.08419e-19,5.36871e+08],[&amp;quot;+2^-63&amp;quot;,2.16837e-19,5.45259e+08],[&amp;quot;+2^-62&amp;quot;,4.33674e-19,5.53648e+08],[&amp;quot;+2^-61&amp;quot;,8.67349e-19,5.62036e+08],[&amp;quot;+2^-60&amp;quot;,1.7347e-18,5.70425e+08],[&amp;quot;+2^-59&amp;quot;,3.46939e-18,5.78814e+08],[&amp;quot;+2^-58&amp;quot;,6.93879e-18,5.87202e+08],[&amp;quot;+2^-57&amp;quot;,1.38776e-17,5.95591e+08],[&amp;quot;+2^-56&amp;quot;,2.77552e-17,6.0398e+08],[&amp;quot;+2^-55&amp;quot;,5.55103e-17,6.12368e+08],[&amp;quot;+2^-54&amp;quot;,1.11021e-16,6.20757e+08],[&amp;quot;+2^-53&amp;quot;,2.22041e-16,6.29145e+08],[&amp;quot;+2^-52&amp;quot;,4.44082e-16,6.37534e+08],[&amp;quot;+2^-51&amp;quot;,8.88165e-16,6.45923e+08],[&amp;quot;+2^-50&amp;quot;,1.77633e-15,6.54311e+08],[&amp;quot;+2^-49&amp;quot;,3.55266e-15,6.627e+08],[&amp;quot;+2^-48&amp;quot;,7.10532e-15,6.71088e+08],[&amp;quot;+2^-47&amp;quot;,1.42106e-14,6.79477e+08],[&amp;quot;+2^-46&amp;quot;,2.84213e-14,6.87866e+08],[&amp;quot;+2^-45&amp;quot;,5.68426e-14,6.96254e+08],[&amp;quot;+2^-44&amp;quot;,1.13685e-13,7.04643e+08],[&amp;quot;+2^-43&amp;quot;,2.2737e-13,7.13031e+08],[&amp;quot;+2^-42&amp;quot;,4.5474e-13,7.2142e+08],[&amp;quot;+2^-41&amp;quot;,9.09481e-13,7.29809e+08],[&amp;quot;+2^-40&amp;quot;,1.81896e-12,7.38197e+08],[&amp;quot;+2^-39&amp;quot;,3.63792e-12,7.46586e+08],[&amp;quot;+2^-38&amp;quot;,7.27585e-12,7.54974e+08],[&amp;quot;+2^-37&amp;quot;,1.45517e-11,7.63363e+08],[&amp;quot;+2^-36&amp;quot;,2.91034e-11,7.71752e+08],[&amp;quot;+2^-35&amp;quot;,5.82068e-11,7.8014e+08],[&amp;quot;+2^-34&amp;quot;,1.16414e-10,7.88529e+08],[&amp;quot;+2^-33&amp;quot;,2.32827e-10,7.96918e+08],[&amp;quot;+2^-32&amp;quot;,4.65654e-10,8.05306e+08],[&amp;quot;+2^-31&amp;quot;,9.31308e-10,8.13695e+08],[&amp;quot;+2^-30&amp;quot;,1.86262e-09,8.22083e+08],[&amp;quot;+2^-29&amp;quot;,3.72523e-09,8.30472e+08],[&amp;quot;+2^-28&amp;quot;,7.45047e-09,8.38861e+08],[&amp;quot;+2^-27&amp;quot;,1.49009e-08,8.47249e+08],[&amp;quot;+2^-26&amp;quot;,2.98019e-08,8.55638e+08],[&amp;quot;+2^-25&amp;quot;,2.98023e-08,8.38861e+06],[&amp;quot;+2^-24&amp;quot;,2.98023e-08,4.1943e+06],[&amp;quot;+2^-23&amp;quot;,2.98023e-08,2.09715e+06],[&amp;quot;+2^-22&amp;quot;,2.98023e-08,1.04857e+06],[&amp;quot;+2^-21&amp;quot;,2.98021e-08,524284],[&amp;quot;+2^-20&amp;quot;,2.98014e-08,262136],[&amp;quot;+2^-19&amp;quot;,2.97987e-08,131056],[&amp;quot;+2^-18&amp;quot;,2.97878e-08,65504],[&amp;quot;+2^-17&amp;quot;,2.98014e-08,32767],[&amp;quot;+2^-16&amp;quot;,2.98005e-08,16384],[&amp;quot;+2^-15&amp;quot;,2.98023e-08,8192],[&amp;quot;+2^-14&amp;quot;,2.98025e-08,4351.99],[&amp;quot;+2^-13&amp;quot;,2.98044e-08,2815.96],[&amp;quot;+2^-12&amp;quot;,2.98091e-08,1024.23],[&amp;quot;+2^-11&amp;quot;,2.9812e-08,512.167],[&amp;quot;+2^-10&amp;quot;,2.98154e-08,256.113],[&amp;quot;+2^-9&amp;quot;,2.98591e-08,128.244],[&amp;quot;+2^-8&amp;quot;,3.00204e-08,85.3328],[&amp;quot;+2^-7&amp;quot;,3.01947e-08,43.675],[&amp;quot;+2^-6&amp;quot;,3.05713e-08,30.45],[&amp;quot;+2^-5&amp;quot;,3.14319e-08,8.43743],[&amp;quot;+2^-4&amp;quot;,3.31291e-08,7.77058],[&amp;quot;+2^-3&amp;quot;,3.58597e-08,3.99659],[&amp;quot;+2^-2&amp;quot;,4.14639e-08,2.29246],[&amp;quot;+2^-1&amp;quot;,8.02409e-08,1.34622],[&amp;quot;+2^0&amp;quot;,8.66201e-08,1.45324],[&amp;quot;+2^1&amp;quot;,8.8654e-08,1.48737],[&amp;quot;+2^2&amp;quot;,6.7976e-08,1.14045],[&amp;quot;+2^3&amp;quot;,1.33955e-07,2.2474],[&amp;quot;+2^4&amp;quot;,2.53131e-14,4.24683e-07],[&amp;quot;+2^5&amp;quot;,0,null],[&amp;quot;+2^6&amp;quot;,0,null],[&amp;quot;+2^7&amp;quot;,0,null],[&amp;quot;+2^8&amp;quot;,0,null],[&amp;quot;+2^9&amp;quot;,0,null],[&amp;quot;+2^10&amp;quot;,0,null],[&amp;quot;+2^11&amp;quot;,0,null],[&amp;quot;+2^12&amp;quot;,0,null],[&amp;quot;+2^13&amp;quot;,0,null],[&amp;quot;+2^14&amp;quot;,0,null],[&amp;quot;+2^15&amp;quot;,0,null],[&amp;quot;+2^16&amp;quot;,0,null],[&amp;quot;+2^17&amp;quot;,0,null],[&amp;quot;+2^18&amp;quot;,0,null],[&amp;quot;+2^19&amp;quot;,0,null],[&amp;quot;+2^20&amp;quot;,0,null],[&amp;quot;+2^21&amp;quot;,0,null],[&amp;quot;+2^22&amp;quot;,0,null],[&amp;quot;+2^23&amp;quot;,0,null],[&amp;quot;+2^24&amp;quot;,0,null],[&amp;quot;+2^25&amp;quot;,0,null],[&amp;quot;+2^26&amp;quot;,0,null],[&amp;quot;+2^27&amp;quot;,0,null],[&amp;quot;+2^28&amp;quot;,0,null],[&amp;quot;+2^29&amp;quot;,0,null],[&amp;quot;+2^30&amp;quot;,0,null],[&amp;quot;+2^31&amp;quot;,0,null],[&amp;quot;+2^32&amp;quot;,0,null],[&amp;quot;+2^33&amp;quot;,0,null],[&amp;quot;+2^34&amp;quot;,0,null],[&amp;quot;+2^35&amp;quot;,0,null],[&amp;quot;+2^36&amp;quot;,0,null],[&amp;quot;+2^37&amp;quot;,0,null],[&amp;quot;+2^38&amp;quot;,0,null],[&amp;quot;+2^39&amp;quot;,0,null],[&amp;quot;+2^40&amp;quot;,0,null],[&amp;quot;+2^41&amp;quot;,0,null],[&amp;quot;+2^42&amp;quot;,0,null],[&amp;quot;+2^43&amp;quot;,0,null],[&amp;quot;+2^44&amp;quot;,0,null],[&amp;quot;+2^45&amp;quot;,0,null],[&amp;quot;+2^46&amp;quot;,0,null],[&amp;quot;+2^47&amp;quot;,0,null],[&amp;quot;+2^48&amp;quot;,0,null],[&amp;quot;+2^49&amp;quot;,0,null],[&amp;quot;+2^50&amp;quot;,0,null],[&amp;quot;+2^51&amp;quot;,0,null],[&amp;quot;+2^52&amp;quot;,0,null],[&amp;quot;+2^53&amp;quot;,0,null],[&amp;quot;+2^54&amp;quot;,0,null],[&amp;quot;+2^55&amp;quot;,0,null],[&amp;quot;+2^56&amp;quot;,0,null],[&amp;quot;+2^57&amp;quot;,0,null],[&amp;quot;+2^58&amp;quot;,0,null],[&amp;quot;+2^59&amp;quot;,0,null],[&amp;quot;+2^60&amp;quot;,0,null],[&amp;quot;+2^61&amp;quot;,0,null],[&amp;quot;+2^62&amp;quot;,0,null],[&amp;quot;+2^63&amp;quot;,0,null],[&amp;quot;+2^64&amp;quot;,0,null],[&amp;quot;+2^65&amp;quot;,0,null],[&amp;quot;+2^66&amp;quot;,0,null],[&amp;quot;+2^67&amp;quot;,0,null],[&amp;quot;+2^68&amp;quot;,0,null],[&amp;quot;+2^69&amp;quot;,0,null],[&amp;quot;+2^70&amp;quot;,0,null],[&amp;quot;+2^71&amp;quot;,0,null],[&amp;quot;+2^72&amp;quot;,0,null],[&amp;quot;+2^73&amp;quot;,0,null],[&amp;quot;+2^74&amp;quot;,0,null],[&amp;quot;+2^75&amp;quot;,0,null],[&amp;quot;+2^76&amp;quot;,0,null],[&amp;quot;+2^77&amp;quot;,0,null],[&amp;quot;+2^78&amp;quot;,0,null],[&amp;quot;+2^79&amp;quot;,0,null],[&amp;quot;+2^80&amp;quot;,0,null],[&amp;quot;+2^81&amp;quot;,0,null],[&amp;quot;+2^82&amp;quot;,0,null],[&amp;quot;+2^83&amp;quot;,0,null],[&amp;quot;+2^84&amp;quot;,0,null],[&amp;quot;+2^85&amp;quot;,0,null],[&amp;quot;+2^86&amp;quot;,0,null],[&amp;quot;+2^87&amp;quot;,0,null],[&amp;quot;+2^88&amp;quot;,0,null],[&amp;quot;+2^89&amp;quot;,0,null],[&amp;quot;+2^90&amp;quot;,0,null],[&amp;quot;+2^91&amp;quot;,0,null],[&amp;quot;+2^92&amp;quot;,0,null],[&amp;quot;+2^93&amp;quot;,0,null],[&amp;quot;+2^94&amp;quot;,0,null],[&amp;quot;+2^95&amp;quot;,0,null],[&amp;quot;+2^96&amp;quot;,0,null],[&amp;quot;+2^97&amp;quot;,0,null],[&amp;quot;+2^98&amp;quot;,0,null],[&amp;quot;+2^99&amp;quot;,0,null],[&amp;quot;+2^100&amp;quot;,0,null],[&amp;quot;+2^101&amp;quot;,0,null],[&amp;quot;+2^102&amp;quot;,0,null],[&amp;quot;+2^103&amp;quot;,0,null],[&amp;quot;+2^104&amp;quot;,0,null],[&amp;quot;+2^105&amp;quot;,0,null],[&amp;quot;+2^106&amp;quot;,0,null],[&amp;quot;+2^107&amp;quot;,0,null],[&amp;quot;+2^108&amp;quot;,0,null],[&amp;quot;+2^109&amp;quot;,0,null],[&amp;quot;+2^110&amp;quot;,0,null],[&amp;quot;+2^111&amp;quot;,0,null],[&amp;quot;+2^112&amp;quot;,0,null],[&amp;quot;+2^113&amp;quot;,0,null],[&amp;quot;+2^114&amp;quot;,0,null],[&amp;quot;+2^115&amp;quot;,0,null],[&amp;quot;+2^116&amp;quot;,0,null],[&amp;quot;+2^117&amp;quot;,0,null],[&amp;quot;+2^118&amp;quot;,0,null],[&amp;quot;+2^119&amp;quot;,0,null],[&amp;quot;+2^120&amp;quot;,0,null],[&amp;quot;+2^121&amp;quot;,0,null],[&amp;quot;+2^122&amp;quot;,0,null],[&amp;quot;+2^123&amp;quot;,0,null],[&amp;quot;+2^124&amp;quot;,0,null],[&amp;quot;+2^125&amp;quot;,0,null],[&amp;quot;+2^126&amp;quot;,0,null],[&amp;quot;+2^127&amp;quot;,0,null] ]&lt;br /&gt;
 }}&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Key observation:&#039;&#039;&#039;  &lt;br /&gt;
For &amp;lt;math&amp;gt;|x| \ll 1&amp;lt;/math&amp;gt;, the ULP error grows rapidly and reaches extremely large values due to loss of significance. &lt;br /&gt;
For sufficiently large &amp;lt;math&amp;gt;x&amp;lt;/math&amp;gt;, both numerator and denominator overflow, leading to NaN results.&lt;br /&gt;
&lt;br /&gt;
=== expm1 ===&lt;br /&gt;
&lt;br /&gt;
This graph visualizes the ULP error of the &amp;lt;code&amp;gt;expm1&amp;lt;/code&amp;gt;-based formulation.&lt;br /&gt;
Using &amp;lt;code&amp;gt;expm1&amp;lt;/code&amp;gt; significantly improves numerical accuracy for small input values by avoiding catastrophic cancellation in the expression &amp;lt;math&amp;gt;e^{2x} - 1&amp;lt;/math&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
{{#widget:FloatErrorGraph|id=tanhf_expm1&lt;br /&gt;
  |data=[ [&amp;quot;-2^127&amp;quot;,0,1.17549e-38],[&amp;quot;-2^126&amp;quot;,0,1.17549e-38],[&amp;quot;-2^125&amp;quot;,0,1.17549e-38],[&amp;quot;-2^124&amp;quot;,0,1.17549e-38],[&amp;quot;-2^123&amp;quot;,0,1.17549e-38],[&amp;quot;-2^122&amp;quot;,0,1.17549e-38],[&amp;quot;-2^121&amp;quot;,0,1.17549e-38],[&amp;quot;-2^120&amp;quot;,0,1.17549e-38],[&amp;quot;-2^119&amp;quot;,0,1.17549e-38],[&amp;quot;-2^118&amp;quot;,0,1.17549e-38],[&amp;quot;-2^117&amp;quot;,0,1.17549e-38],[&amp;quot;-2^116&amp;quot;,0,1.17549e-38],[&amp;quot;-2^115&amp;quot;,0,1.17549e-38],[&amp;quot;-2^114&amp;quot;,0,1.17549e-38],[&amp;quot;-2^113&amp;quot;,0,1.17549e-38],[&amp;quot;-2^112&amp;quot;,0,1.17549e-38],[&amp;quot;-2^111&amp;quot;,0,1.17549e-38],[&amp;quot;-2^110&amp;quot;,0,1.17549e-38],[&amp;quot;-2^109&amp;quot;,0,1.17549e-38],[&amp;quot;-2^108&amp;quot;,0,1.17549e-38],[&amp;quot;-2^107&amp;quot;,0,1.17549e-38],[&amp;quot;-2^106&amp;quot;,0,1.17549e-38],[&amp;quot;-2^105&amp;quot;,0,1.17549e-38],[&amp;quot;-2^104&amp;quot;,0,1.17549e-38],[&amp;quot;-2^103&amp;quot;,0,1.17549e-38],[&amp;quot;-2^102&amp;quot;,0,1.17549e-38],[&amp;quot;-2^101&amp;quot;,0,1.17549e-38],[&amp;quot;-2^100&amp;quot;,0,1.17549e-38],[&amp;quot;-2^99&amp;quot;,0,1.17549e-38],[&amp;quot;-2^98&amp;quot;,0,1.17549e-38],[&amp;quot;-2^97&amp;quot;,0,1.17549e-38],[&amp;quot;-2^96&amp;quot;,0,1.17549e-38],[&amp;quot;-2^95&amp;quot;,0,1.17549e-38],[&amp;quot;-2^94&amp;quot;,0,1.17549e-38],[&amp;quot;-2^93&amp;quot;,0,1.17549e-38],[&amp;quot;-2^92&amp;quot;,0,1.17549e-38],[&amp;quot;-2^91&amp;quot;,0,1.17549e-38],[&amp;quot;-2^90&amp;quot;,0,1.17549e-38],[&amp;quot;-2^89&amp;quot;,0,1.17549e-38],[&amp;quot;-2^88&amp;quot;,0,1.17549e-38],[&amp;quot;-2^87&amp;quot;,0,1.17549e-38],[&amp;quot;-2^86&amp;quot;,0,1.17549e-38],[&amp;quot;-2^85&amp;quot;,0,1.17549e-38],[&amp;quot;-2^84&amp;quot;,0,1.17549e-38],[&amp;quot;-2^83&amp;quot;,0,1.17549e-38],[&amp;quot;-2^82&amp;quot;,0,1.17549e-38],[&amp;quot;-2^81&amp;quot;,0,1.17549e-38],[&amp;quot;-2^80&amp;quot;,0,1.17549e-38],[&amp;quot;-2^79&amp;quot;,0,1.17549e-38],[&amp;quot;-2^78&amp;quot;,0,1.17549e-38],[&amp;quot;-2^77&amp;quot;,0,1.17549e-38],[&amp;quot;-2^76&amp;quot;,0,1.17549e-38],[&amp;quot;-2^75&amp;quot;,0,1.17549e-38],[&amp;quot;-2^74&amp;quot;,0,1.17549e-38],[&amp;quot;-2^73&amp;quot;,0,1.17549e-38],[&amp;quot;-2^72&amp;quot;,0,1.17549e-38],[&amp;quot;-2^71&amp;quot;,0,1.17549e-38],[&amp;quot;-2^70&amp;quot;,0,1.17549e-38],[&amp;quot;-2^69&amp;quot;,0,1.17549e-38],[&amp;quot;-2^68&amp;quot;,0,1.17549e-38],[&amp;quot;-2^67&amp;quot;,0,1.17549e-38],[&amp;quot;-2^66&amp;quot;,0,1.17549e-38],[&amp;quot;-2^65&amp;quot;,0,1.17549e-38],[&amp;quot;-2^64&amp;quot;,0,1.17549e-38],[&amp;quot;-2^63&amp;quot;,0,1.17549e-38],[&amp;quot;-2^62&amp;quot;,0,1.17549e-38],[&amp;quot;-2^61&amp;quot;,0,1.17549e-38],[&amp;quot;-2^60&amp;quot;,0,1.17549e-38],[&amp;quot;-2^59&amp;quot;,0,1.17549e-38],[&amp;quot;-2^58&amp;quot;,0,1.17549e-38],[&amp;quot;-2^57&amp;quot;,0,1.17549e-38],[&amp;quot;-2^56&amp;quot;,0,1.17549e-38],[&amp;quot;-2^55&amp;quot;,0,1.17549e-38],[&amp;quot;-2^54&amp;quot;,0,1.17549e-38],[&amp;quot;-2^53&amp;quot;,0,1.17549e-38],[&amp;quot;-2^52&amp;quot;,0,1.17549e-38],[&amp;quot;-2^51&amp;quot;,0,1.17549e-38],[&amp;quot;-2^50&amp;quot;,0,1.17549e-38],[&amp;quot;-2^49&amp;quot;,0,1.17549e-38],[&amp;quot;-2^48&amp;quot;,0,1.17549e-38],[&amp;quot;-2^47&amp;quot;,0,1.17549e-38],[&amp;quot;-2^46&amp;quot;,0,1.17549e-38],[&amp;quot;-2^45&amp;quot;,0,1.17549e-38],[&amp;quot;-2^44&amp;quot;,0,1.17549e-38],[&amp;quot;-2^43&amp;quot;,0,1.17549e-38],[&amp;quot;-2^42&amp;quot;,0,1.17549e-38],[&amp;quot;-2^41&amp;quot;,0,1.17549e-38],[&amp;quot;-2^40&amp;quot;,0,1.17549e-38],[&amp;quot;-2^39&amp;quot;,0,1.17549e-38],[&amp;quot;-2^38&amp;quot;,0,1.17549e-38],[&amp;quot;-2^37&amp;quot;,0,1.17549e-38],[&amp;quot;-2^36&amp;quot;,0,1.17549e-38],[&amp;quot;-2^35&amp;quot;,0,1.17549e-38],[&amp;quot;-2^34&amp;quot;,0,1.17549e-38],[&amp;quot;-2^33&amp;quot;,0,1.17549e-38],[&amp;quot;-2^32&amp;quot;,0,1.17549e-38],[&amp;quot;-2^31&amp;quot;,0,1.17549e-38],[&amp;quot;-2^30&amp;quot;,0,1.17549e-38],[&amp;quot;-2^29&amp;quot;,0,1.17549e-38],[&amp;quot;-2^28&amp;quot;,0,1.17549e-38],[&amp;quot;-2^27&amp;quot;,0,1.17549e-38],[&amp;quot;-2^26&amp;quot;,0,1.17549e-38],[&amp;quot;-2^25&amp;quot;,0,1.17549e-38],[&amp;quot;-2^24&amp;quot;,0,1.17549e-38],[&amp;quot;-2^23&amp;quot;,0,1.17549e-38],[&amp;quot;-2^22&amp;quot;,0,1.17549e-38],[&amp;quot;-2^21&amp;quot;,0,1.17549e-38],[&amp;quot;-2^20&amp;quot;,0,1.17549e-38],[&amp;quot;-2^19&amp;quot;,0,1.17549e-38],[&amp;quot;-2^18&amp;quot;,0,1.17549e-38],[&amp;quot;-2^17&amp;quot;,0,1.17549e-38],[&amp;quot;-2^16&amp;quot;,0,1.17549e-38],[&amp;quot;-2^15&amp;quot;,0,1.17549e-38],[&amp;quot;-2^14&amp;quot;,0,1.17549e-38],[&amp;quot;-2^13&amp;quot;,0,1.17549e-38],[&amp;quot;-2^12&amp;quot;,0,1.17549e-38],[&amp;quot;-2^11&amp;quot;,0,1.17549e-38],[&amp;quot;-2^10&amp;quot;,0,1.17549e-38],[&amp;quot;-2^9&amp;quot;,0,1.17549e-38],[&amp;quot;-2^8&amp;quot;,0,1.17549e-38],[&amp;quot;-2^7&amp;quot;,0,1.17549e-38],[&amp;quot;-2^6&amp;quot;,0,1.17549e-38],[&amp;quot;-2^5&amp;quot;,0,1.17549e-38],[&amp;quot;-2^4&amp;quot;,2.53131e-14,4.24683e-07],[&amp;quot;-2^3&amp;quot;,1.19138e-07,1.9988],[&amp;quot;-2^2&amp;quot;,1.46791e-07,2.46274],[&amp;quot;-2^1&amp;quot;,1.47723e-07,2.47839],[&amp;quot;-2^0&amp;quot;,1.40758e-07,2.36153],[&amp;quot;-2^-1&amp;quot;,1.13422e-07,2.23445],[&amp;quot;-2^-2&amp;quot;,6.38012e-08,2.14081],[&amp;quot;-2^-3&amp;quot;,2.69538e-08,1.80884],[&amp;quot;-2^-4&amp;quot;,1.21308e-08,1.62817],[&amp;quot;-2^-5&amp;quot;,5.59535e-09,1.50199],[&amp;quot;-2^-6&amp;quot;,2.69177e-09,1.44513],[&amp;quot;-2^-7&amp;quot;,1.31289e-09,1.4097],[&amp;quot;-2^-8&amp;quot;,6.49688e-10,1.3952],[&amp;quot;-2^-9&amp;quot;,3.32043e-10,1.42611],[&amp;quot;-2^-10&amp;quot;,1.66275e-10,1.4283],[&amp;quot;-2^-11&amp;quot;,8.08595e-11,1.38916],[&amp;quot;-2^-12&amp;quot;,3.8939e-11,1.33794],[&amp;quot;-2^-13&amp;quot;,1.9401e-11,1.33323],[&amp;quot;-2^-14&amp;quot;,7.88212e-12,1.08331],[&amp;quot;-2^-15&amp;quot;,3.71372e-12,1.02082],[&amp;quot;-2^-16&amp;quot;,1.82845e-12,1.0052],[&amp;quot;-2^-17&amp;quot;,9.10676e-13,1.0013],[&amp;quot;-2^-18&amp;quot;,4.54895e-13,1.00032],[&amp;quot;-2^-19&amp;quot;,2.27392e-13,1.00008],[&amp;quot;-2^-20&amp;quot;,1.13689e-13,1.00002],[&amp;quot;-2^-21&amp;quot;,5.68437e-14,1],[&amp;quot;-2^-22&amp;quot;,2.84217e-14,1],[&amp;quot;-2^-23&amp;quot;,1.42109e-14,1],[&amp;quot;-2^-24&amp;quot;,7.10543e-15,1],[&amp;quot;-2^-25&amp;quot;,3.55271e-15,1],[&amp;quot;-2^-26&amp;quot;,9.92617e-24,5.58794e-09],[&amp;quot;-2^-27&amp;quot;,1.65436e-24,1.86265e-09],[&amp;quot;-2^-28&amp;quot;,8.27181e-25,1.86265e-09],[&amp;quot;-2^-29&amp;quot;,4.1359e-25,1.86265e-09],[&amp;quot;-2^-30&amp;quot;,2.06795e-25,1.86265e-09],[&amp;quot;-2^-31&amp;quot;,1.03398e-25,1.86265e-09],[&amp;quot;-2^-32&amp;quot;,5.16988e-26,1.86265e-09],[&amp;quot;-2^-33&amp;quot;,2.58494e-26,1.86265e-09],[&amp;quot;-2^-34&amp;quot;,1.29247e-26,1.86265e-09],[&amp;quot;-2^-35&amp;quot;,6.46235e-27,1.86265e-09],[&amp;quot;-2^-36&amp;quot;,3.23117e-27,1.86265e-09],[&amp;quot;-2^-37&amp;quot;,1.61559e-27,1.86265e-09],[&amp;quot;-2^-38&amp;quot;,8.07794e-28,1.86265e-09],[&amp;quot;-2^-39&amp;quot;,4.03897e-28,1.86265e-09],[&amp;quot;-2^-40&amp;quot;,2.01948e-28,1.86265e-09],[&amp;quot;-2^-41&amp;quot;,1.00974e-28,1.86265e-09],[&amp;quot;-2^-42&amp;quot;,5.04871e-29,1.86265e-09],[&amp;quot;-2^-43&amp;quot;,2.52435e-29,1.86265e-09],[&amp;quot;-2^-44&amp;quot;,1.26218e-29,1.86265e-09],[&amp;quot;-2^-45&amp;quot;,6.31089e-30,1.86265e-09],[&amp;quot;-2^-46&amp;quot;,3.15544e-30,1.86265e-09],[&amp;quot;-2^-47&amp;quot;,1.57772e-30,1.86265e-09],[&amp;quot;-2^-48&amp;quot;,7.88861e-31,1.86265e-09],[&amp;quot;-2^-49&amp;quot;,3.9443e-31,1.86265e-09],[&amp;quot;-2^-50&amp;quot;,1.97215e-31,1.86265e-09],[&amp;quot;-2^-51&amp;quot;,9.86076e-32,1.86265e-09],[&amp;quot;-2^-52&amp;quot;,4.93038e-32,1.86265e-09],[&amp;quot;-2^-53&amp;quot;,2.46519e-32,1.86265e-09],[&amp;quot;-2^-54&amp;quot;,1.2326e-32,1.86265e-09],[&amp;quot;-2^-55&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-56&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-57&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-58&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-59&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-60&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-61&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-62&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-63&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-64&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-65&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-66&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-67&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-68&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-69&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-70&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-71&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-72&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-73&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-74&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-75&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-76&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-77&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-78&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-79&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-80&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-81&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-82&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-83&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-84&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-85&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-86&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-87&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-88&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-89&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-90&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-91&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-92&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-93&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-94&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-95&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-96&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-97&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-98&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-99&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-100&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-101&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-102&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-103&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-104&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-105&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-106&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-107&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-108&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-109&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-110&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-111&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-112&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-113&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-114&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-115&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-116&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-117&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-118&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-119&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-120&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-121&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-122&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-123&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-124&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-125&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-126&amp;quot;,0,1.17549e-38],[&amp;quot;-SN&amp;quot;,0,1.17549e-38],[&amp;quot;0&amp;quot;,0,1.17549e-38],[&amp;quot;+SN&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-126&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-125&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-124&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-123&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-122&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-121&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-120&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-119&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-118&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-117&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-116&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-115&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-114&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-113&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-112&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-111&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-110&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-109&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-108&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-107&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-106&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-105&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-104&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-103&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-102&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-101&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-100&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-99&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-98&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-97&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-96&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-95&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-94&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-93&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-92&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-91&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-90&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-89&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-88&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-87&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-86&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-85&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-84&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-83&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-82&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-81&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-80&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-79&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-78&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-77&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-76&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-75&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-74&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-73&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-72&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-71&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-70&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-69&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-68&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-67&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-66&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-65&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-64&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-63&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-62&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-61&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-60&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-59&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-58&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-57&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-56&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-55&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-54&amp;quot;,1.2326e-32,1.86265e-09],[&amp;quot;+2^-53&amp;quot;,2.46519e-32,1.86265e-09],[&amp;quot;+2^-52&amp;quot;,4.93038e-32,1.86265e-09],[&amp;quot;+2^-51&amp;quot;,9.86076e-32,1.86265e-09],[&amp;quot;+2^-50&amp;quot;,1.97215e-31,1.86265e-09],[&amp;quot;+2^-49&amp;quot;,3.9443e-31,1.86265e-09],[&amp;quot;+2^-48&amp;quot;,7.88861e-31,1.86265e-09],[&amp;quot;+2^-47&amp;quot;,1.57772e-30,1.86265e-09],[&amp;quot;+2^-46&amp;quot;,3.15544e-30,1.86265e-09],[&amp;quot;+2^-45&amp;quot;,6.31089e-30,1.86265e-09],[&amp;quot;+2^-44&amp;quot;,1.26218e-29,1.86265e-09],[&amp;quot;+2^-43&amp;quot;,2.52435e-29,1.86265e-09],[&amp;quot;+2^-42&amp;quot;,5.04871e-29,1.86265e-09],[&amp;quot;+2^-41&amp;quot;,1.00974e-28,1.86265e-09],[&amp;quot;+2^-40&amp;quot;,2.01948e-28,1.86265e-09],[&amp;quot;+2^-39&amp;quot;,4.03897e-28,1.86265e-09],[&amp;quot;+2^-38&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-37&amp;quot;,1.61559e-27,1.86265e-09],[&amp;quot;+2^-36&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-35&amp;quot;,6.46235e-27,1.86265e-09],[&amp;quot;+2^-34&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-33&amp;quot;,2.58494e-26,1.86265e-09],[&amp;quot;+2^-32&amp;quot;,5.16988e-26,1.86265e-09],[&amp;quot;+2^-31&amp;quot;,1.03398e-25,1.86265e-09],[&amp;quot;+2^-30&amp;quot;,2.06795e-25,1.86265e-09],[&amp;quot;+2^-29&amp;quot;,4.1359e-25,1.86265e-09],[&amp;quot;+2^-28&amp;quot;,8.27181e-25,1.86265e-09],[&amp;quot;+2^-27&amp;quot;,1.65436e-24,1.86265e-09],[&amp;quot;+2^-26&amp;quot;,9.92617e-24,5.58794e-09],[&amp;quot;+2^-25&amp;quot;,3.55271e-15,1],[&amp;quot;+2^-24&amp;quot;,7.10543e-15,1],[&amp;quot;+2^-23&amp;quot;,1.42109e-14,1],[&amp;quot;+2^-22&amp;quot;,2.84217e-14,1],[&amp;quot;+2^-21&amp;quot;,5.68437e-14,1],[&amp;quot;+2^-20&amp;quot;,1.13689e-13,1.00002],[&amp;quot;+2^-19&amp;quot;,2.27391e-13,1.00008],[&amp;quot;+2^-18&amp;quot;,4.54892e-13,1.00032],[&amp;quot;+2^-17&amp;quot;,9.10665e-13,1.00129],[&amp;quot;+2^-16&amp;quot;,1.82841e-12,1.00518],[&amp;quot;+2^-15&amp;quot;,3.71353e-12,1.02077],[&amp;quot;+2^-14&amp;quot;,1.39798e-11,1.92136],[&amp;quot;+2^-13&amp;quot;,2.63239e-11,1.80896],[&amp;quot;+2^-12&amp;quot;,5.37712e-11,1.84757],[&amp;quot;+2^-11&amp;quot;,1.09932e-10,1.88861],[&amp;quot;+2^-10&amp;quot;,2.24787e-10,1.93091],[&amp;quot;+2^-9&amp;quot;,4.3452e-10,1.86625],[&amp;quot;+2^-8&amp;quot;,1.01783e-09,2.18576],[&amp;quot;+2^-7&amp;quot;,2.10562e-09,2.26089],[&amp;quot;+2^-6&amp;quot;,4.03954e-09,2.16871],[&amp;quot;+2^-5&amp;quot;,8.15746e-09,2.18975],[&amp;quot;+2^-4&amp;quot;,1.5545e-08,2.08641],[&amp;quot;+2^-3&amp;quot;,2.62153e-08,2.05864],[&amp;quot;+2^-2&amp;quot;,4.09295e-08,1.73804],[&amp;quot;+2^-1&amp;quot;,8.02409e-08,1.34622],[&amp;quot;+2^0&amp;quot;,8.66201e-08,1.45324],[&amp;quot;+2^1&amp;quot;,8.8654e-08,1.48737],[&amp;quot;+2^2&amp;quot;,6.7976e-08,1.14045],[&amp;quot;+2^3&amp;quot;,7.44117e-08,1.24842],[&amp;quot;+2^4&amp;quot;,2.53131e-14,4.24683e-07],[&amp;quot;+2^5&amp;quot;,0,null],[&amp;quot;+2^6&amp;quot;,0,null],[&amp;quot;+2^7&amp;quot;,0,null],[&amp;quot;+2^8&amp;quot;,0,null],[&amp;quot;+2^9&amp;quot;,0,null],[&amp;quot;+2^10&amp;quot;,0,null],[&amp;quot;+2^11&amp;quot;,0,null],[&amp;quot;+2^12&amp;quot;,0,null],[&amp;quot;+2^13&amp;quot;,0,null],[&amp;quot;+2^14&amp;quot;,0,null],[&amp;quot;+2^15&amp;quot;,0,null],[&amp;quot;+2^16&amp;quot;,0,null],[&amp;quot;+2^17&amp;quot;,0,null],[&amp;quot;+2^18&amp;quot;,0,null],[&amp;quot;+2^19&amp;quot;,0,null],[&amp;quot;+2^20&amp;quot;,0,null],[&amp;quot;+2^21&amp;quot;,0,null],[&amp;quot;+2^22&amp;quot;,0,null],[&amp;quot;+2^23&amp;quot;,0,null],[&amp;quot;+2^24&amp;quot;,0,null],[&amp;quot;+2^25&amp;quot;,0,null],[&amp;quot;+2^26&amp;quot;,0,null],[&amp;quot;+2^27&amp;quot;,0,null],[&amp;quot;+2^28&amp;quot;,0,null],[&amp;quot;+2^29&amp;quot;,0,null],[&amp;quot;+2^30&amp;quot;,0,null],[&amp;quot;+2^31&amp;quot;,0,null],[&amp;quot;+2^32&amp;quot;,0,null],[&amp;quot;+2^33&amp;quot;,0,null],[&amp;quot;+2^34&amp;quot;,0,null],[&amp;quot;+2^35&amp;quot;,0,null],[&amp;quot;+2^36&amp;quot;,0,null],[&amp;quot;+2^37&amp;quot;,0,null],[&amp;quot;+2^38&amp;quot;,0,null],[&amp;quot;+2^39&amp;quot;,0,null],[&amp;quot;+2^40&amp;quot;,0,null],[&amp;quot;+2^41&amp;quot;,0,null],[&amp;quot;+2^42&amp;quot;,0,null],[&amp;quot;+2^43&amp;quot;,0,null],[&amp;quot;+2^44&amp;quot;,0,null],[&amp;quot;+2^45&amp;quot;,0,null],[&amp;quot;+2^46&amp;quot;,0,null],[&amp;quot;+2^47&amp;quot;,0,null],[&amp;quot;+2^48&amp;quot;,0,null],[&amp;quot;+2^49&amp;quot;,0,null],[&amp;quot;+2^50&amp;quot;,0,null],[&amp;quot;+2^51&amp;quot;,0,null],[&amp;quot;+2^52&amp;quot;,0,null],[&amp;quot;+2^53&amp;quot;,0,null],[&amp;quot;+2^54&amp;quot;,0,null],[&amp;quot;+2^55&amp;quot;,0,null],[&amp;quot;+2^56&amp;quot;,0,null],[&amp;quot;+2^57&amp;quot;,0,null],[&amp;quot;+2^58&amp;quot;,0,null],[&amp;quot;+2^59&amp;quot;,0,null],[&amp;quot;+2^60&amp;quot;,0,null],[&amp;quot;+2^61&amp;quot;,0,null],[&amp;quot;+2^62&amp;quot;,0,null],[&amp;quot;+2^63&amp;quot;,0,null],[&amp;quot;+2^64&amp;quot;,0,null],[&amp;quot;+2^65&amp;quot;,0,null],[&amp;quot;+2^66&amp;quot;,0,null],[&amp;quot;+2^67&amp;quot;,0,null],[&amp;quot;+2^68&amp;quot;,0,null],[&amp;quot;+2^69&amp;quot;,0,null],[&amp;quot;+2^70&amp;quot;,0,null],[&amp;quot;+2^71&amp;quot;,0,null],[&amp;quot;+2^72&amp;quot;,0,null],[&amp;quot;+2^73&amp;quot;,0,null],[&amp;quot;+2^74&amp;quot;,0,null],[&amp;quot;+2^75&amp;quot;,0,null],[&amp;quot;+2^76&amp;quot;,0,null],[&amp;quot;+2^77&amp;quot;,0,null],[&amp;quot;+2^78&amp;quot;,0,null],[&amp;quot;+2^79&amp;quot;,0,null],[&amp;quot;+2^80&amp;quot;,0,null],[&amp;quot;+2^81&amp;quot;,0,null],[&amp;quot;+2^82&amp;quot;,0,null],[&amp;quot;+2^83&amp;quot;,0,null],[&amp;quot;+2^84&amp;quot;,0,null],[&amp;quot;+2^85&amp;quot;,0,null],[&amp;quot;+2^86&amp;quot;,0,null],[&amp;quot;+2^87&amp;quot;,0,null],[&amp;quot;+2^88&amp;quot;,0,null],[&amp;quot;+2^89&amp;quot;,0,null],[&amp;quot;+2^90&amp;quot;,0,null],[&amp;quot;+2^91&amp;quot;,0,null],[&amp;quot;+2^92&amp;quot;,0,null],[&amp;quot;+2^93&amp;quot;,0,null],[&amp;quot;+2^94&amp;quot;,0,null],[&amp;quot;+2^95&amp;quot;,0,null],[&amp;quot;+2^96&amp;quot;,0,null],[&amp;quot;+2^97&amp;quot;,0,null],[&amp;quot;+2^98&amp;quot;,0,null],[&amp;quot;+2^99&amp;quot;,0,null],[&amp;quot;+2^100&amp;quot;,0,null],[&amp;quot;+2^101&amp;quot;,0,null],[&amp;quot;+2^102&amp;quot;,0,null],[&amp;quot;+2^103&amp;quot;,0,null],[&amp;quot;+2^104&amp;quot;,0,null],[&amp;quot;+2^105&amp;quot;,0,null],[&amp;quot;+2^106&amp;quot;,0,null],[&amp;quot;+2^107&amp;quot;,0,null],[&amp;quot;+2^108&amp;quot;,0,null],[&amp;quot;+2^109&amp;quot;,0,null],[&amp;quot;+2^110&amp;quot;,0,null],[&amp;quot;+2^111&amp;quot;,0,null],[&amp;quot;+2^112&amp;quot;,0,null],[&amp;quot;+2^113&amp;quot;,0,null],[&amp;quot;+2^114&amp;quot;,0,null],[&amp;quot;+2^115&amp;quot;,0,null],[&amp;quot;+2^116&amp;quot;,0,null],[&amp;quot;+2^117&amp;quot;,0,null],[&amp;quot;+2^118&amp;quot;,0,null],[&amp;quot;+2^119&amp;quot;,0,null],[&amp;quot;+2^120&amp;quot;,0,null],[&amp;quot;+2^121&amp;quot;,0,null],[&amp;quot;+2^122&amp;quot;,0,null],[&amp;quot;+2^123&amp;quot;,0,null],[&amp;quot;+2^124&amp;quot;,0,null],[&amp;quot;+2^125&amp;quot;,0,null],[&amp;quot;+2^126&amp;quot;,0,null],[&amp;quot;+2^127&amp;quot;,0,null] ]&lt;br /&gt;
 }}&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Key observation:&#039;&#039;&#039;  &lt;br /&gt;
Compared to direct exponential formulations, the &amp;lt;code&amp;gt;expm1&amp;lt;/code&amp;gt; approach maintains low ULP error for small and medium values of &amp;lt;math&amp;gt;x&amp;lt;/math&amp;gt;. &lt;br /&gt;
Overflow is still present for very large inputs, but the overall error behavior is substantially more stable.&lt;br /&gt;
&lt;br /&gt;
== Numerical Issues and Error Metrics ==&lt;br /&gt;
&lt;br /&gt;
To evaluate and compare different software implementations of &amp;lt;code&amp;gt;tanh&amp;lt;/code&amp;gt;, several numerical error metrics are used throughout this article. These metrics make it possible to reason about accuracy, robustness, and failure modes in floating-point arithmetic.&lt;br /&gt;
&lt;br /&gt;
=== ULP Error ===&lt;br /&gt;
ULP (Unit in the Last Place) measures the distance between two adjacent representable floating-point numbers. The ULP error of a computed result is defined as the difference, measured in ULPs, between the computed value and the correctly rounded reference value.&lt;br /&gt;
&lt;br /&gt;
An error of &amp;lt;code&amp;gt;ULP ≤ 2&amp;lt;/code&amp;gt; is commonly considered acceptable for single-precision transcendental functions, as it indicates that the result is very close to the correctly rounded value.&lt;br /&gt;
&lt;br /&gt;
Large ULP errors typically indicate catastrophic cancellation, loss of significant bits, or severe rounding effects.&lt;br /&gt;
&lt;br /&gt;
=== NaN and Overflow ===&lt;br /&gt;
NaN (Not a Number) results occur when undefined floating-point operations are performed, such as &amp;lt;code&amp;gt;inf / inf&amp;lt;/code&amp;gt;. In &amp;lt;code&amp;gt;tanh&amp;lt;/code&amp;gt; implementations, this often happens when both the numerator and denominator of a formulation overflow to infinity.&lt;br /&gt;
&lt;br /&gt;
Tracking NaN generation is critical, as NaNs can silently propagate through numerical pipelines and invalidate downstream results.&lt;br /&gt;
&lt;br /&gt;
=== Error Graphs ===&lt;br /&gt;
The error graphs shown in the following sections visualize ULP error across the full &amp;lt;code&amp;gt;float32&amp;lt;/code&amp;gt; input domain. They make it possible to identify:&lt;br /&gt;
* regions with catastrophic cancellation,&lt;br /&gt;
* input ranges that trigger overflow or NaNs,&lt;br /&gt;
* and numerical stability differences between formulations.&lt;br /&gt;
&lt;br /&gt;
These graphs are a key tool for understanding not only the maximum error, but also how errors are distributed across different input magnitudes.&lt;br /&gt;
&lt;br /&gt;
== Taylor Expansion (Local Approximation) ==&lt;br /&gt;
&lt;br /&gt;
Taylor series approximations of &amp;lt;code&amp;gt;tanh(x)&amp;lt;/code&amp;gt; expand the function around &amp;lt;math&amp;gt;x = 0&amp;lt;/math&amp;gt; and therefore provide a very accurate approximation only for sufficiently small input magnitudes.&lt;br /&gt;
&lt;br /&gt;
For small &amp;lt;math&amp;gt;|x|&amp;lt;/math&amp;gt;, the hyperbolic tangent behaves approximately linear (&amp;lt;math&amp;gt;\tanh(x) \approx x&amp;lt;/math&amp;gt;), and low-order Taylor polynomials can achieve excellent accuracy with minimal computational cost. This makes Taylor-based approximations attractive as a fast path in range-reduced implementations.&lt;br /&gt;
&lt;br /&gt;
However, Taylor expansions have a strictly limited radius of convergence. Outside a narrow neighborhood around zero, the approximation error grows extremely rapidly and quickly becomes unusable. As a result, Taylor polynomials are not suitable as standalone implementations of &amp;lt;code&amp;gt;tanh&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
{{#widget:FloatErrorGraph|id=taylor&lt;br /&gt;
  |data=[ [&amp;quot;-2^127&amp;quot;,0,0],[&amp;quot;-2^126&amp;quot;,0,0],[&amp;quot;-2^125&amp;quot;,0,0],[&amp;quot;-2^124&amp;quot;,0,0],[&amp;quot;-2^123&amp;quot;,0,0],[&amp;quot;-2^122&amp;quot;,0,0],[&amp;quot;-2^121&amp;quot;,0,0],[&amp;quot;-2^120&amp;quot;,0,0],[&amp;quot;-2^119&amp;quot;,0,0],[&amp;quot;-2^118&amp;quot;,0,0],[&amp;quot;-2^117&amp;quot;,0,0],[&amp;quot;-2^116&amp;quot;,0,0],[&amp;quot;-2^115&amp;quot;,0,0],[&amp;quot;-2^114&amp;quot;,0,0],[&amp;quot;-2^113&amp;quot;,0,0],[&amp;quot;-2^112&amp;quot;,0,0],[&amp;quot;-2^111&amp;quot;,0,0],[&amp;quot;-2^110&amp;quot;,0,0],[&amp;quot;-2^109&amp;quot;,0,0],[&amp;quot;-2^108&amp;quot;,0,0],[&amp;quot;-2^107&amp;quot;,0,0],[&amp;quot;-2^106&amp;quot;,0,0],[&amp;quot;-2^105&amp;quot;,0,0],[&amp;quot;-2^104&amp;quot;,0,0],[&amp;quot;-2^103&amp;quot;,0,0],[&amp;quot;-2^102&amp;quot;,0,0],[&amp;quot;-2^101&amp;quot;,0,0],[&amp;quot;-2^100&amp;quot;,0,0],[&amp;quot;-2^99&amp;quot;,0,0],[&amp;quot;-2^98&amp;quot;,0,0],[&amp;quot;-2^97&amp;quot;,0,0],[&amp;quot;-2^96&amp;quot;,0,0],[&amp;quot;-2^95&amp;quot;,0,0],[&amp;quot;-2^94&amp;quot;,0,0],[&amp;quot;-2^93&amp;quot;,0,0],[&amp;quot;-2^92&amp;quot;,0,0],[&amp;quot;-2^91&amp;quot;,0,0],[&amp;quot;-2^90&amp;quot;,0,0],[&amp;quot;-2^89&amp;quot;,0,0],[&amp;quot;-2^88&amp;quot;,0,0],[&amp;quot;-2^87&amp;quot;,0,0],[&amp;quot;-2^86&amp;quot;,0,0],[&amp;quot;-2^85&amp;quot;,0,0],[&amp;quot;-2^84&amp;quot;,0,0],[&amp;quot;-2^83&amp;quot;,0,0],[&amp;quot;-2^82&amp;quot;,0,0],[&amp;quot;-2^81&amp;quot;,0,0],[&amp;quot;-2^80&amp;quot;,0,0],[&amp;quot;-2^79&amp;quot;,0,0],[&amp;quot;-2^78&amp;quot;,0,0],[&amp;quot;-2^77&amp;quot;,0,0],[&amp;quot;-2^76&amp;quot;,0,0],[&amp;quot;-2^75&amp;quot;,0,0],[&amp;quot;-2^74&amp;quot;,0,0],[&amp;quot;-2^73&amp;quot;,0,0],[&amp;quot;-2^72&amp;quot;,0,0],[&amp;quot;-2^71&amp;quot;,0,0],[&amp;quot;-2^70&amp;quot;,0,0],[&amp;quot;-2^69&amp;quot;,0,0],[&amp;quot;-2^68&amp;quot;,0,0],[&amp;quot;-2^67&amp;quot;,0,0],[&amp;quot;-2^66&amp;quot;,0,0],[&amp;quot;-2^65&amp;quot;,0,0],[&amp;quot;-2^64&amp;quot;,0,0],[&amp;quot;-2^63&amp;quot;,1.84467e+19,3.09485e+26],[&amp;quot;-2^62&amp;quot;,9.22336e+18,1.54742e+26],[&amp;quot;-2^61&amp;quot;,4.61168e+18,7.73712e+25],[&amp;quot;-2^60&amp;quot;,2.30584e+18,3.86856e+25],[&amp;quot;-2^59&amp;quot;,1.15292e+18,1.93428e+25],[&amp;quot;-2^58&amp;quot;,5.7646e+17,9.6714e+24],[&amp;quot;-2^57&amp;quot;,2.8823e+17,4.8357e+24],[&amp;quot;-2^56&amp;quot;,1.44115e+17,2.41785e+24],[&amp;quot;-2^55&amp;quot;,7.20575e+16,1.20892e+24],[&amp;quot;-2^54&amp;quot;,3.60288e+16,6.04462e+23],[&amp;quot;-2^53&amp;quot;,1.80144e+16,3.02231e+23],[&amp;quot;-2^52&amp;quot;,9.00719e+15,1.51116e+23],[&amp;quot;-2^51&amp;quot;,4.5036e+15,7.55578e+22],[&amp;quot;-2^50&amp;quot;,2.2518e+15,3.77789e+22],[&amp;quot;-2^49&amp;quot;,1.1259e+15,1.88894e+22],[&amp;quot;-2^48&amp;quot;,5.62949e+14,9.44472e+21],[&amp;quot;-2^47&amp;quot;,2.81475e+14,4.72236e+21],[&amp;quot;-2^46&amp;quot;,1.40737e+14,2.36118e+21],[&amp;quot;-2^45&amp;quot;,7.03687e+13,1.18059e+21],[&amp;quot;-2^44&amp;quot;,3.51843e+13,5.90295e+20],[&amp;quot;-2^43&amp;quot;,1.75922e+13,2.95148e+20],[&amp;quot;-2^42&amp;quot;,8.79608e+12,1.47574e+20],[&amp;quot;-2^41&amp;quot;,4.39804e+12,7.37869e+19],[&amp;quot;-2^40&amp;quot;,2.19902e+12,3.68935e+19],[&amp;quot;-2^39&amp;quot;,1.09951e+12,1.84467e+19],[&amp;quot;-2^38&amp;quot;,5.49755e+11,9.22336e+18],[&amp;quot;-2^37&amp;quot;,2.74878e+11,4.61168e+18],[&amp;quot;-2^36&amp;quot;,1.37439e+11,2.30584e+18],[&amp;quot;-2^35&amp;quot;,6.87194e+10,1.15292e+18],[&amp;quot;-2^34&amp;quot;,3.43597e+10,5.7646e+17],[&amp;quot;-2^33&amp;quot;,1.71799e+10,2.8823e+17],[&amp;quot;-2^32&amp;quot;,8.58993e+09,1.44115e+17],[&amp;quot;-2^31&amp;quot;,4.29085e+09,7.19885e+16],[&amp;quot;-2^30&amp;quot;,2.14542e+09,3.59943e+16],[&amp;quot;-2^29&amp;quot;,1.07271e+09,1.79971e+16],[&amp;quot;-2^28&amp;quot;,5.36356e+08,8.99856e+15],[&amp;quot;-2^27&amp;quot;,2.68178e+08,4.49928e+15],[&amp;quot;-2^26&amp;quot;,1.34089e+08,2.24964e+15],[&amp;quot;-2^25&amp;quot;,6.70445e+07,1.12482e+15],[&amp;quot;-2^24&amp;quot;,3.35223e+07,5.6241e+14],[&amp;quot;-2^23&amp;quot;,1.67611e+07,2.81205e+14],[&amp;quot;-2^22&amp;quot;,8.38056e+06,1.40603e+14],[&amp;quot;-2^21&amp;quot;,4.19028e+06,7.03013e+13],[&amp;quot;-2^20&amp;quot;,2.09514e+06,3.51506e+13],[&amp;quot;-2^19&amp;quot;,1.04757e+06,1.75753e+13],[&amp;quot;-2^18&amp;quot;,523784,8.78764e+12],[&amp;quot;-2^17&amp;quot;,261892,4.39381e+12],[&amp;quot;-2^16&amp;quot;,130945,2.1969e+12],[&amp;quot;-2^15&amp;quot;,65472.2,1.09844e+12],[&amp;quot;-2^14&amp;quot;,32735.6,5.49212e+11],[&amp;quot;-2^13&amp;quot;,16367.3,2.74598e+11],[&amp;quot;-2^12&amp;quot;,8183.15,1.3729e+11],[&amp;quot;-2^11&amp;quot;,4091.07,6.86368e+10],[&amp;quot;-2^10&amp;quot;,2045.04,3.431e+10],[&amp;quot;-2^9&amp;quot;,1022.02,1.71466e+10],[&amp;quot;-2^8&amp;quot;,510.509,8.56492e+09],[&amp;quot;-2^7&amp;quot;,254.754,4.27406e+09],[&amp;quot;-2^6&amp;quot;,126.876,2.12862e+09],[&amp;quot;-2^5&amp;quot;,62.9354,1.05588e+09],[&amp;quot;-2^4&amp;quot;,30.9629,5.19471e+08],[&amp;quot;-2^3&amp;quot;,14.9718,2.51185e+08],[&amp;quot;-2^2&amp;quot;,6.96672,1.16882e+08],[&amp;quot;-2^1&amp;quot;,2.94633,4.94312e+07],[&amp;quot;-2^0&amp;quot;,0.940715,1.57826e+07],[&amp;quot;-2^-1&amp;quot;,0.127635,2.14135e+06],[&amp;quot;-2^-2&amp;quot;,0.00318735,106950],[&amp;quot;-2^-3&amp;quot;,3.11475e-05,2090.28],[&amp;quot;-2^-4&amp;quot;,2.57737e-07,34.5929],[&amp;quot;-2^-5&amp;quot;,3.89213e-09,1.04478],[&amp;quot;-2^-6&amp;quot;,9.51472e-10,0.510818],[&amp;quot;-2^-7&amp;quot;,4.66402e-10,0.500795],[&amp;quot;-2^-8&amp;quot;,2.32917e-10,0.500185],[&amp;quot;-2^-9&amp;quot;,1.16423e-10,0.500033],[&amp;quot;-2^-10&amp;quot;,5.82082e-11,0.500004],[&amp;quot;-2^-11&amp;quot;,2.91037e-11,0.499998],[&amp;quot;-2^-12&amp;quot;,1.45519e-11,0.5],[&amp;quot;-2^-13&amp;quot;,4.85062e-12,0.333332],[&amp;quot;-2^-14&amp;quot;,6.06328e-13,0.0833331],[&amp;quot;-2^-15&amp;quot;,7.5791e-14,0.0208333],[&amp;quot;-2^-16&amp;quot;,9.47387e-15,0.00520832],[&amp;quot;-2^-17&amp;quot;,1.18424e-15,0.00130208],[&amp;quot;-2^-18&amp;quot;,1.48029e-16,0.00032552],[&amp;quot;-2^-19&amp;quot;,1.85039e-17,8.13808e-05],[&amp;quot;-2^-20&amp;quot;,2.31304e-18,2.03457e-05],[&amp;quot;-2^-21&amp;quot;,2.89156e-19,5.08688e-06],[&amp;quot;-2^-22&amp;quot;,3.61577e-20,1.27219e-06],[&amp;quot;-2^-23&amp;quot;,4.52633e-21,3.18512e-07],[&amp;quot;-2^-24&amp;quot;,5.691e-22,8.00937e-08],[&amp;quot;-2^-25&amp;quot;,7.27919e-23,2.04891e-08],[&amp;quot;-2^-26&amp;quot;,9.92617e-24,5.58794e-09],[&amp;quot;-2^-27&amp;quot;,1.65436e-24,1.86265e-09],[&amp;quot;-2^-28&amp;quot;,8.27181e-25,1.86265e-09],[&amp;quot;-2^-29&amp;quot;,4.1359e-25,1.86265e-09],[&amp;quot;-2^-30&amp;quot;,2.06795e-25,1.86265e-09],[&amp;quot;-2^-31&amp;quot;,1.03398e-25,1.86265e-09],[&amp;quot;-2^-32&amp;quot;,5.16988e-26,1.86265e-09],[&amp;quot;-2^-33&amp;quot;,2.58494e-26,1.86265e-09],[&amp;quot;-2^-34&amp;quot;,1.29247e-26,1.86265e-09],[&amp;quot;-2^-35&amp;quot;,6.46235e-27,1.86265e-09],[&amp;quot;-2^-36&amp;quot;,3.23117e-27,1.86265e-09],[&amp;quot;-2^-37&amp;quot;,1.61559e-27,1.86265e-09],[&amp;quot;-2^-38&amp;quot;,8.07794e-28,1.86265e-09],[&amp;quot;-2^-39&amp;quot;,4.03897e-28,1.86265e-09],[&amp;quot;-2^-40&amp;quot;,2.01948e-28,1.86265e-09],[&amp;quot;-2^-41&amp;quot;,1.00974e-28,1.86265e-09],[&amp;quot;-2^-42&amp;quot;,5.04871e-29,1.86265e-09],[&amp;quot;-2^-43&amp;quot;,2.52435e-29,1.86265e-09],[&amp;quot;-2^-44&amp;quot;,1.26218e-29,1.86265e-09],[&amp;quot;-2^-45&amp;quot;,6.31089e-30,1.86265e-09],[&amp;quot;-2^-46&amp;quot;,3.15544e-30,1.86265e-09],[&amp;quot;-2^-47&amp;quot;,1.57772e-30,1.86265e-09],[&amp;quot;-2^-48&amp;quot;,7.88861e-31,1.86265e-09],[&amp;quot;-2^-49&amp;quot;,3.9443e-31,1.86265e-09],[&amp;quot;-2^-50&amp;quot;,1.97215e-31,1.86265e-09],[&amp;quot;-2^-51&amp;quot;,9.86076e-32,1.86265e-09],[&amp;quot;-2^-52&amp;quot;,4.93038e-32,1.86265e-09],[&amp;quot;-2^-53&amp;quot;,2.46519e-32,1.86265e-09],[&amp;quot;-2^-54&amp;quot;,1.2326e-32,1.86265e-09],[&amp;quot;-2^-55&amp;quot;,0,0],[&amp;quot;-2^-56&amp;quot;,0,0],[&amp;quot;-2^-57&amp;quot;,0,0],[&amp;quot;-2^-58&amp;quot;,0,0],[&amp;quot;-2^-59&amp;quot;,0,0],[&amp;quot;-2^-60&amp;quot;,0,0],[&amp;quot;-2^-61&amp;quot;,0,0],[&amp;quot;-2^-62&amp;quot;,0,0],[&amp;quot;-2^-63&amp;quot;,0,0],[&amp;quot;-2^-64&amp;quot;,0,0],[&amp;quot;-2^-65&amp;quot;,0,0],[&amp;quot;-2^-66&amp;quot;,0,0],[&amp;quot;-2^-67&amp;quot;,0,0],[&amp;quot;-2^-68&amp;quot;,0,0],[&amp;quot;-2^-69&amp;quot;,0,0],[&amp;quot;-2^-70&amp;quot;,0,0],[&amp;quot;-2^-71&amp;quot;,0,0],[&amp;quot;-2^-72&amp;quot;,0,0],[&amp;quot;-2^-73&amp;quot;,0,0],[&amp;quot;-2^-74&amp;quot;,0,0],[&amp;quot;-2^-75&amp;quot;,0,0],[&amp;quot;-2^-76&amp;quot;,0,0],[&amp;quot;-2^-77&amp;quot;,0,0],[&amp;quot;-2^-78&amp;quot;,0,0],[&amp;quot;-2^-79&amp;quot;,0,0],[&amp;quot;-2^-80&amp;quot;,0,0],[&amp;quot;-2^-81&amp;quot;,0,0],[&amp;quot;-2^-82&amp;quot;,0,0],[&amp;quot;-2^-83&amp;quot;,0,0],[&amp;quot;-2^-84&amp;quot;,0,0],[&amp;quot;-2^-85&amp;quot;,0,0],[&amp;quot;-2^-86&amp;quot;,0,0],[&amp;quot;-2^-87&amp;quot;,0,0],[&amp;quot;-2^-88&amp;quot;,0,0],[&amp;quot;-2^-89&amp;quot;,0,0],[&amp;quot;-2^-90&amp;quot;,0,0],[&amp;quot;-2^-91&amp;quot;,0,0],[&amp;quot;-2^-92&amp;quot;,0,0],[&amp;quot;-2^-93&amp;quot;,0,0],[&amp;quot;-2^-94&amp;quot;,0,0],[&amp;quot;-2^-95&amp;quot;,0,0],[&amp;quot;-2^-96&amp;quot;,0,0],[&amp;quot;-2^-97&amp;quot;,0,0],[&amp;quot;-2^-98&amp;quot;,0,0],[&amp;quot;-2^-99&amp;quot;,0,0],[&amp;quot;-2^-100&amp;quot;,0,0],[&amp;quot;-2^-101&amp;quot;,0,0],[&amp;quot;-2^-102&amp;quot;,0,0],[&amp;quot;-2^-103&amp;quot;,0,0],[&amp;quot;-2^-104&amp;quot;,0,0],[&amp;quot;-2^-105&amp;quot;,0,0],[&amp;quot;-2^-106&amp;quot;,0,0],[&amp;quot;-2^-107&amp;quot;,0,0],[&amp;quot;-2^-108&amp;quot;,0,0],[&amp;quot;-2^-109&amp;quot;,0,0],[&amp;quot;-2^-110&amp;quot;,0,0],[&amp;quot;-2^-111&amp;quot;,0,0],[&amp;quot;-2^-112&amp;quot;,0,0],[&amp;quot;-2^-113&amp;quot;,0,0],[&amp;quot;-2^-114&amp;quot;,0,0],[&amp;quot;-2^-115&amp;quot;,0,0],[&amp;quot;-2^-116&amp;quot;,0,0],[&amp;quot;-2^-117&amp;quot;,0,0],[&amp;quot;-2^-118&amp;quot;,0,0],[&amp;quot;-2^-119&amp;quot;,0,0],[&amp;quot;-2^-120&amp;quot;,0,0],[&amp;quot;-2^-121&amp;quot;,0,0],[&amp;quot;-2^-122&amp;quot;,0,0],[&amp;quot;-2^-123&amp;quot;,0,0],[&amp;quot;-2^-124&amp;quot;,0,0],[&amp;quot;-2^-125&amp;quot;,0,0],[&amp;quot;-2^-126&amp;quot;,0,0],[&amp;quot;-SN&amp;quot;,0,0],[&amp;quot;0&amp;quot;,0,0],[&amp;quot;+SN&amp;quot;,0,0],[&amp;quot;+2^-126&amp;quot;,0,0],[&amp;quot;+2^-125&amp;quot;,0,0],[&amp;quot;+2^-124&amp;quot;,0,0],[&amp;quot;+2^-123&amp;quot;,0,0],[&amp;quot;+2^-122&amp;quot;,0,0],[&amp;quot;+2^-121&amp;quot;,0,0],[&amp;quot;+2^-120&amp;quot;,0,0],[&amp;quot;+2^-119&amp;quot;,0,0],[&amp;quot;+2^-118&amp;quot;,0,0],[&amp;quot;+2^-117&amp;quot;,0,0],[&amp;quot;+2^-116&amp;quot;,0,0],[&amp;quot;+2^-115&amp;quot;,0,0],[&amp;quot;+2^-114&amp;quot;,0,0],[&amp;quot;+2^-113&amp;quot;,0,0],[&amp;quot;+2^-112&amp;quot;,0,0],[&amp;quot;+2^-111&amp;quot;,0,0],[&amp;quot;+2^-110&amp;quot;,0,0],[&amp;quot;+2^-109&amp;quot;,0,0],[&amp;quot;+2^-108&amp;quot;,0,0],[&amp;quot;+2^-107&amp;quot;,0,0],[&amp;quot;+2^-106&amp;quot;,0,0],[&amp;quot;+2^-105&amp;quot;,0,0],[&amp;quot;+2^-104&amp;quot;,0,0],[&amp;quot;+2^-103&amp;quot;,0,0],[&amp;quot;+2^-102&amp;quot;,0,0],[&amp;quot;+2^-101&amp;quot;,0,0],[&amp;quot;+2^-100&amp;quot;,0,0],[&amp;quot;+2^-99&amp;quot;,0,0],[&amp;quot;+2^-98&amp;quot;,0,0],[&amp;quot;+2^-97&amp;quot;,0,0],[&amp;quot;+2^-96&amp;quot;,0,0],[&amp;quot;+2^-95&amp;quot;,0,0],[&amp;quot;+2^-94&amp;quot;,0,0],[&amp;quot;+2^-93&amp;quot;,0,0],[&amp;quot;+2^-92&amp;quot;,0,0],[&amp;quot;+2^-91&amp;quot;,0,0],[&amp;quot;+2^-90&amp;quot;,0,0],[&amp;quot;+2^-89&amp;quot;,0,0],[&amp;quot;+2^-88&amp;quot;,0,0],[&amp;quot;+2^-87&amp;quot;,0,0],[&amp;quot;+2^-86&amp;quot;,0,0],[&amp;quot;+2^-85&amp;quot;,0,0],[&amp;quot;+2^-84&amp;quot;,0,0],[&amp;quot;+2^-83&amp;quot;,0,0],[&amp;quot;+2^-82&amp;quot;,0,0],[&amp;quot;+2^-81&amp;quot;,0,0],[&amp;quot;+2^-80&amp;quot;,0,0],[&amp;quot;+2^-79&amp;quot;,0,0],[&amp;quot;+2^-78&amp;quot;,0,0],[&amp;quot;+2^-77&amp;quot;,0,0],[&amp;quot;+2^-76&amp;quot;,0,0],[&amp;quot;+2^-75&amp;quot;,0,0],[&amp;quot;+2^-74&amp;quot;,0,0],[&amp;quot;+2^-73&amp;quot;,0,0],[&amp;quot;+2^-72&amp;quot;,0,0],[&amp;quot;+2^-71&amp;quot;,0,0],[&amp;quot;+2^-70&amp;quot;,0,0],[&amp;quot;+2^-69&amp;quot;,0,0],[&amp;quot;+2^-68&amp;quot;,0,0],[&amp;quot;+2^-67&amp;quot;,0,0],[&amp;quot;+2^-66&amp;quot;,0,0],[&amp;quot;+2^-65&amp;quot;,0,0],[&amp;quot;+2^-64&amp;quot;,0,0],[&amp;quot;+2^-63&amp;quot;,0,0],[&amp;quot;+2^-62&amp;quot;,0,0],[&amp;quot;+2^-61&amp;quot;,0,0],[&amp;quot;+2^-60&amp;quot;,0,0],[&amp;quot;+2^-59&amp;quot;,0,0],[&amp;quot;+2^-58&amp;quot;,0,0],[&amp;quot;+2^-57&amp;quot;,0,0],[&amp;quot;+2^-56&amp;quot;,0,0],[&amp;quot;+2^-55&amp;quot;,0,0],[&amp;quot;+2^-54&amp;quot;,1.2326e-32,1.86265e-09],[&amp;quot;+2^-53&amp;quot;,2.46519e-32,1.86265e-09],[&amp;quot;+2^-52&amp;quot;,4.93038e-32,1.86265e-09],[&amp;quot;+2^-51&amp;quot;,9.86076e-32,1.86265e-09],[&amp;quot;+2^-50&amp;quot;,1.97215e-31,1.86265e-09],[&amp;quot;+2^-49&amp;quot;,3.9443e-31,1.86265e-09],[&amp;quot;+2^-48&amp;quot;,7.88861e-31,1.86265e-09],[&amp;quot;+2^-47&amp;quot;,1.57772e-30,1.86265e-09],[&amp;quot;+2^-46&amp;quot;,3.15544e-30,1.86265e-09],[&amp;quot;+2^-45&amp;quot;,6.31089e-30,1.86265e-09],[&amp;quot;+2^-44&amp;quot;,1.26218e-29,1.86265e-09],[&amp;quot;+2^-43&amp;quot;,2.52435e-29,1.86265e-09],[&amp;quot;+2^-42&amp;quot;,5.04871e-29,1.86265e-09],[&amp;quot;+2^-41&amp;quot;,1.00974e-28,1.86265e-09],[&amp;quot;+2^-40&amp;quot;,2.01948e-28,1.86265e-09],[&amp;quot;+2^-39&amp;quot;,4.03897e-28,1.86265e-09],[&amp;quot;+2^-38&amp;quot;,8.07794e-28,1.86265e-09],[&amp;quot;+2^-37&amp;quot;,1.61559e-27,1.86265e-09],[&amp;quot;+2^-36&amp;quot;,3.23117e-27,1.86265e-09],[&amp;quot;+2^-35&amp;quot;,6.46235e-27,1.86265e-09],[&amp;quot;+2^-34&amp;quot;,1.29247e-26,1.86265e-09],[&amp;quot;+2^-33&amp;quot;,2.58494e-26,1.86265e-09],[&amp;quot;+2^-32&amp;quot;,5.16988e-26,1.86265e-09],[&amp;quot;+2^-31&amp;quot;,1.03398e-25,1.86265e-09],[&amp;quot;+2^-30&amp;quot;,2.06795e-25,1.86265e-09],[&amp;quot;+2^-29&amp;quot;,4.1359e-25,1.86265e-09],[&amp;quot;+2^-28&amp;quot;,8.27181e-25,1.86265e-09],[&amp;quot;+2^-27&amp;quot;,1.65436e-24,1.86265e-09],[&amp;quot;+2^-26&amp;quot;,9.92617e-24,5.58794e-09],[&amp;quot;+2^-25&amp;quot;,7.27919e-23,2.04891e-08],[&amp;quot;+2^-24&amp;quot;,5.691e-22,8.00937e-08],[&amp;quot;+2^-23&amp;quot;,4.52633e-21,3.18512e-07],[&amp;quot;+2^-22&amp;quot;,3.61577e-20,1.27219e-06],[&amp;quot;+2^-21&amp;quot;,2.89156e-19,5.08688e-06],[&amp;quot;+2^-20&amp;quot;,2.31304e-18,2.03457e-05],[&amp;quot;+2^-19&amp;quot;,1.85039e-17,8.13808e-05],[&amp;quot;+2^-18&amp;quot;,1.48029e-16,0.00032552],[&amp;quot;+2^-17&amp;quot;,1.18424e-15,0.00130208],[&amp;quot;+2^-16&amp;quot;,9.47387e-15,0.00520832],[&amp;quot;+2^-15&amp;quot;,7.5791e-14,0.0208333],[&amp;quot;+2^-14&amp;quot;,6.06328e-13,0.0833331],[&amp;quot;+2^-13&amp;quot;,4.85062e-12,0.333332],[&amp;quot;+2^-12&amp;quot;,1.45519e-11,0.5],[&amp;quot;+2^-11&amp;quot;,2.91037e-11,0.499998],[&amp;quot;+2^-10&amp;quot;,5.82082e-11,0.500004],[&amp;quot;+2^-9&amp;quot;,1.16423e-10,0.500033],[&amp;quot;+2^-8&amp;quot;,2.32917e-10,0.500185],[&amp;quot;+2^-7&amp;quot;,4.66402e-10,0.500795],[&amp;quot;+2^-6&amp;quot;,9.51472e-10,0.510818],[&amp;quot;+2^-5&amp;quot;,3.89213e-09,1.04478],[&amp;quot;+2^-4&amp;quot;,2.57737e-07,34.5929],[&amp;quot;+2^-3&amp;quot;,3.11475e-05,2090.28],[&amp;quot;+2^-2&amp;quot;,0.00318735,106950],[&amp;quot;+2^-1&amp;quot;,0.127635,2.14135e+06],[&amp;quot;+2^0&amp;quot;,0.940715,1.57826e+07],[&amp;quot;+2^1&amp;quot;,2.94633,4.94312e+07],[&amp;quot;+2^2&amp;quot;,6.96672,1.16882e+08],[&amp;quot;+2^3&amp;quot;,14.9718,1.33874e+08],[&amp;quot;+2^4&amp;quot;,30.9629,2.59736e+08],[&amp;quot;+2^5&amp;quot;,62.9354,5.27941e+08],[&amp;quot;+2^6&amp;quot;,126.876,1.06431e+09],[&amp;quot;+2^7&amp;quot;,254.754,2.13703e+09],[&amp;quot;+2^8&amp;quot;,510.509,4.28246e+09],[&amp;quot;+2^9&amp;quot;,1022.02,8.57331e+09],[&amp;quot;+2^10&amp;quot;,2045.04,1.7155e+10],[&amp;quot;+2^11&amp;quot;,4091.07,3.43184e+10],[&amp;quot;+2^12&amp;quot;,8183.15,6.86452e+10],[&amp;quot;+2^13&amp;quot;,16367.3,1.37299e+11],[&amp;quot;+2^14&amp;quot;,32735.6,2.74606e+11],[&amp;quot;+2^15&amp;quot;,65472.2,5.4922e+11],[&amp;quot;+2^16&amp;quot;,130945,1.09845e+12],[&amp;quot;+2^17&amp;quot;,261892,2.19691e+12],[&amp;quot;+2^18&amp;quot;,523784,4.39382e+12],[&amp;quot;+2^19&amp;quot;,1.04757e+06,8.78765e+12],[&amp;quot;+2^20&amp;quot;,2.09514e+06,1.75753e+13],[&amp;quot;+2^21&amp;quot;,4.19028e+06,3.51506e+13],[&amp;quot;+2^22&amp;quot;,8.38056e+06,7.03013e+13],[&amp;quot;+2^23&amp;quot;,1.67611e+07,1.40603e+14],[&amp;quot;+2^24&amp;quot;,3.35223e+07,2.81205e+14],[&amp;quot;+2^25&amp;quot;,6.70445e+07,5.6241e+14],[&amp;quot;+2^26&amp;quot;,1.34089e+08,1.12482e+15],[&amp;quot;+2^27&amp;quot;,2.68178e+08,2.24964e+15],[&amp;quot;+2^28&amp;quot;,5.36356e+08,4.49928e+15],[&amp;quot;+2^29&amp;quot;,1.07271e+09,8.99856e+15],[&amp;quot;+2^30&amp;quot;,2.14542e+09,1.79971e+16],[&amp;quot;+2^31&amp;quot;,4.29085e+09,3.59943e+16],[&amp;quot;+2^32&amp;quot;,8.58993e+09,7.20575e+16],[&amp;quot;+2^33&amp;quot;,1.71799e+10,1.44115e+17],[&amp;quot;+2^34&amp;quot;,3.43597e+10,2.8823e+17],[&amp;quot;+2^35&amp;quot;,6.87194e+10,5.7646e+17],[&amp;quot;+2^36&amp;quot;,1.37439e+11,1.15292e+18],[&amp;quot;+2^37&amp;quot;,2.74878e+11,2.30584e+18],[&amp;quot;+2^38&amp;quot;,5.49755e+11,4.61168e+18],[&amp;quot;+2^39&amp;quot;,1.09951e+12,9.22336e+18],[&amp;quot;+2^40&amp;quot;,2.19902e+12,1.84467e+19],[&amp;quot;+2^41&amp;quot;,4.39804e+12,3.68935e+19],[&amp;quot;+2^42&amp;quot;,8.79608e+12,7.37869e+19],[&amp;quot;+2^43&amp;quot;,1.75922e+13,1.47574e+20],[&amp;quot;+2^44&amp;quot;,3.51843e+13,2.95148e+20],[&amp;quot;+2^45&amp;quot;,7.03687e+13,5.90295e+20],[&amp;quot;+2^46&amp;quot;,1.40737e+14,1.18059e+21],[&amp;quot;+2^47&amp;quot;,2.81475e+14,2.36118e+21],[&amp;quot;+2^48&amp;quot;,5.62949e+14,4.72236e+21],[&amp;quot;+2^49&amp;quot;,1.1259e+15,9.44472e+21],[&amp;quot;+2^50&amp;quot;,2.2518e+15,1.88894e+22],[&amp;quot;+2^51&amp;quot;,4.5036e+15,3.77789e+22],[&amp;quot;+2^52&amp;quot;,9.00719e+15,7.55578e+22],[&amp;quot;+2^53&amp;quot;,1.80144e+16,1.51116e+23],[&amp;quot;+2^54&amp;quot;,3.60288e+16,3.02231e+23],[&amp;quot;+2^55&amp;quot;,7.20575e+16,6.04462e+23],[&amp;quot;+2^56&amp;quot;,1.44115e+17,1.20892e+24],[&amp;quot;+2^57&amp;quot;,2.8823e+17,2.41785e+24],[&amp;quot;+2^58&amp;quot;,5.7646e+17,4.8357e+24],[&amp;quot;+2^59&amp;quot;,1.15292e+18,9.6714e+24],[&amp;quot;+2^60&amp;quot;,2.30584e+18,1.93428e+25],[&amp;quot;+2^61&amp;quot;,4.61168e+18,3.86856e+25],[&amp;quot;+2^62&amp;quot;,9.22336e+18,7.73712e+25],[&amp;quot;+2^63&amp;quot;,1.84467e+19,1.54742e+26],[&amp;quot;+2^64&amp;quot;,0,0],[&amp;quot;+2^65&amp;quot;,0,0],[&amp;quot;+2^66&amp;quot;,0,0],[&amp;quot;+2^67&amp;quot;,0,0],[&amp;quot;+2^68&amp;quot;,0,0],[&amp;quot;+2^69&amp;quot;,0,0],[&amp;quot;+2^70&amp;quot;,0,0],[&amp;quot;+2^71&amp;quot;,0,0],[&amp;quot;+2^72&amp;quot;,0,0],[&amp;quot;+2^73&amp;quot;,0,0],[&amp;quot;+2^74&amp;quot;,0,0],[&amp;quot;+2^75&amp;quot;,0,0],[&amp;quot;+2^76&amp;quot;,0,0],[&amp;quot;+2^77&amp;quot;,0,0],[&amp;quot;+2^78&amp;quot;,0,0],[&amp;quot;+2^79&amp;quot;,0,0],[&amp;quot;+2^80&amp;quot;,0,0],[&amp;quot;+2^81&amp;quot;,0,0],[&amp;quot;+2^82&amp;quot;,0,0],[&amp;quot;+2^83&amp;quot;,0,0],[&amp;quot;+2^84&amp;quot;,0,0],[&amp;quot;+2^85&amp;quot;,0,0],[&amp;quot;+2^86&amp;quot;,0,0],[&amp;quot;+2^87&amp;quot;,0,0],[&amp;quot;+2^88&amp;quot;,0,0],[&amp;quot;+2^89&amp;quot;,0,0],[&amp;quot;+2^90&amp;quot;,0,0],[&amp;quot;+2^91&amp;quot;,0,0],[&amp;quot;+2^92&amp;quot;,0,0],[&amp;quot;+2^93&amp;quot;,0,0],[&amp;quot;+2^94&amp;quot;,0,0],[&amp;quot;+2^95&amp;quot;,0,0],[&amp;quot;+2^96&amp;quot;,0,0],[&amp;quot;+2^97&amp;quot;,0,0],[&amp;quot;+2^98&amp;quot;,0,0],[&amp;quot;+2^99&amp;quot;,0,0],[&amp;quot;+2^100&amp;quot;,0,0],[&amp;quot;+2^101&amp;quot;,0,0],[&amp;quot;+2^102&amp;quot;,0,0],[&amp;quot;+2^103&amp;quot;,0,0],[&amp;quot;+2^104&amp;quot;,0,0],[&amp;quot;+2^105&amp;quot;,0,0],[&amp;quot;+2^106&amp;quot;,0,0],[&amp;quot;+2^107&amp;quot;,0,0],[&amp;quot;+2^108&amp;quot;,0,0],[&amp;quot;+2^109&amp;quot;,0,0],[&amp;quot;+2^110&amp;quot;,0,0],[&amp;quot;+2^111&amp;quot;,0,0],[&amp;quot;+2^112&amp;quot;,0,0],[&amp;quot;+2^113&amp;quot;,0,0],[&amp;quot;+2^114&amp;quot;,0,0],[&amp;quot;+2^115&amp;quot;,0,0],[&amp;quot;+2^116&amp;quot;,0,0],[&amp;quot;+2^117&amp;quot;,0,0],[&amp;quot;+2^118&amp;quot;,0,0],[&amp;quot;+2^119&amp;quot;,0,0],[&amp;quot;+2^120&amp;quot;,0,0],[&amp;quot;+2^121&amp;quot;,0,0],[&amp;quot;+2^122&amp;quot;,0,0],[&amp;quot;+2^123&amp;quot;,0,0],[&amp;quot;+2^124&amp;quot;,0,0],[&amp;quot;+2^125&amp;quot;,0,0],[&amp;quot;+2^126&amp;quot;,0,0],[&amp;quot;+2^127&amp;quot;,0,0] ]&lt;br /&gt;
 }}&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Key observation:&#039;&#039;&#039;  &lt;br /&gt;
The Taylor approximation is highly accurate near zero but diverges extremely fast outside its convergence region.  &lt;br /&gt;
This makes it unsuitable as a standalone &amp;lt;code&amp;gt;tanh&amp;lt;/code&amp;gt; implementation, but very effective as a local approximation when combined with range-based dispatching.&lt;br /&gt;
&lt;br /&gt;
== Localized Approximation Functions ==&lt;br /&gt;
&lt;br /&gt;
Localized approximation functions are designed to provide highly accurate and computationally efficient evaluations of &amp;lt;code&amp;gt;tanh(x)&amp;lt;/code&amp;gt; within a restricted input range. &lt;br /&gt;
They are typically used as building blocks in range-based dispatch strategies, where different approximations are selected depending on the magnitude of the input value.&lt;br /&gt;
&lt;br /&gt;
By limiting the valid input range, these approximations can achieve very low ULP error with simple polynomial or rational expressions, avoiding expensive transcendental functions.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
!Id&lt;br /&gt;
!Formula&lt;br /&gt;
!ULP &amp;lt;= 2.0&lt;br /&gt;
!NaN&lt;br /&gt;
|-&lt;br /&gt;
|signum&lt;br /&gt;
|&amp;lt;math&amp;gt;\sgn{x}&amp;lt;/math&amp;gt;&lt;br /&gt;
| -inf &amp;lt; x &amp;lt; -8.31763&lt;br /&gt;
8.31763 &amp;lt; x &amp;lt; inf&lt;br /&gt;
| -&lt;br /&gt;
|-&lt;br /&gt;
|poly1&lt;br /&gt;
|&amp;lt;math&amp;gt;x&amp;lt;/math&amp;gt;&lt;br /&gt;
| -0.000704229 &amp;lt; x &amp;lt; 0.000704229&lt;br /&gt;
| -&lt;br /&gt;
|-&lt;br /&gt;
|poly3&lt;br /&gt;
|&amp;lt;math&amp;gt;x - \frac{1}{3} x^3&amp;lt;/math&amp;gt;&lt;br /&gt;
| -0.0291781 &amp;lt; x &amp;lt; 0.0291781 &lt;br /&gt;
| -&lt;br /&gt;
|-&lt;br /&gt;
|Lambert7&lt;br /&gt;
|&amp;lt;math&amp;gt;\frac{x \cdot (135135 + x^2 \cdot (17325 + x^2 \cdot (378 + x^2)))}{135135 + x^2 \cdot (62370 + x^2 \cdot (3150 + 28 \cdot x^2))}&lt;br /&gt;
&amp;lt;/math&amp;gt;&lt;br /&gt;
| -0.000947416 &amp;lt; x &amp;lt; 0.000947207&lt;br /&gt;
| -inf &amp;lt; x &amp;lt; -1.51629e+06&lt;br /&gt;
1.51629e+06 &amp;lt; x &amp;lt; inf&lt;br /&gt;
|-&lt;br /&gt;
|pade&lt;br /&gt;
|&amp;lt;math&amp;gt;x + x \cdot \frac{x^2 \cdot \left( p_1 \cdot x^2 + p_0 \right)}{\left( x^2 + q_1 \right) \cdot x^2 + q_0} &lt;br /&gt;
&lt;br /&gt;
&amp;lt;/math&amp;gt; &amp;lt;math&amp;gt;\begin{align}&lt;br /&gt;
p_0 &amp;amp;= -0.2059432032 \\&lt;br /&gt;
p_1 &amp;amp;= -0.0009577527 \\&lt;br /&gt;
q_0 &amp;amp;= 0.6178299136 \\ &lt;br /&gt;
q_1 &amp;amp;= 0.25&lt;br /&gt;
\end{align}&lt;br /&gt;
&amp;lt;/math&amp;gt;&lt;br /&gt;
| -0.0016973 &amp;lt; x &amp;lt; 0.0016973 &lt;br /&gt;
| -inf &amp;lt; x &amp;lt; -6.14896e+18&lt;br /&gt;
6.14896e+18 &amp;lt; x &amp;lt; inf&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== External Links ==&lt;br /&gt;
&lt;br /&gt;
* https://yaikhom.com/2020-04-28-localised-approximation-of-hyperbolic-tangents.html#mjx-eqn-eqn7-th%20degree%20lambert%20approximant&lt;br /&gt;
*https://github.com/llvm/llvm-project/blob/main/libc/src/math/generic/tanhf.cpp&lt;br /&gt;
*https://forums.developer.nvidia.com/t/hardware-accelerated-tanh-on-turing/173291&lt;br /&gt;
[[Category:Numerical Precision]]&lt;/div&gt;</summary>
		<author><name>Timo.stripf</name></author>
	</entry>
	<entry>
		<id>https://www.emmtrix.com/w139/index.php?title=tanh_Software_Implementation&amp;diff=2933</id>
		<title>tanh Software Implementation</title>
		<link rel="alternate" type="text/html" href="https://www.emmtrix.com/w139/index.php?title=tanh_Software_Implementation&amp;diff=2933"/>
		<updated>2026-02-04T12:05:47Z</updated>

		<summary type="html">&lt;p&gt;Timo.stripf: /* Taylor */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;[[File:Hyperbolic Tangent.svg|thumb]]&lt;br /&gt;
The hyperbolic tangent function, commonly referred to as &amp;lt;code&amp;gt;tanh&amp;lt;/code&amp;gt;, is a mathematical function that maps real numbers to the range &amp;lt;math&amp;gt;(-1, 1)&amp;lt;/math&amp;gt;. It is defined as:&lt;br /&gt;
&lt;br /&gt;
:&amp;lt;math&amp;gt;\tanh(x) = \frac{e^x - e^{-x}}{e^x + e^{-x}}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
From a software implementation perspective, &amp;lt;code&amp;gt;tanh&amp;lt;/code&amp;gt; is a numerically challenging function due to the use of exponential functions, which can easily overflow for large inputs and suffer from catastrophic cancellation for very small values of &amp;lt;math&amp;gt;x&amp;lt;/math&amp;gt;. These issues are particularly relevant in single-precision floating-point (&amp;lt;code&amp;gt;float32&amp;lt;/code&amp;gt;) implementations, where both accuracy and performance are critical.&lt;br /&gt;
&lt;br /&gt;
In machine learning and deep learning, &amp;lt;code&amp;gt;tanh&amp;lt;/code&amp;gt; is widely used as an activation function because it introduces non-linearity and produces outputs centered around zero, which can improve gradient-based optimization. As a result, &amp;lt;code&amp;gt;tanh&amp;lt;/code&amp;gt; is often evaluated billions of times during training and inference, making efficient and numerically stable implementations essential.&lt;br /&gt;
&lt;br /&gt;
This article focuses on practical software implementations of &amp;lt;code&amp;gt;tanh&amp;lt;/code&amp;gt;, analyzing different computational formulations with respect to floating-point accuracy, ULP error, overflow behavior, and NaN generation. The goal is to provide a foundation for designing robust &amp;lt;code&amp;gt;tanh&amp;lt;/code&amp;gt; implementations suitable for performance-critical environments such as numerical libraries, embedded systems, and AI workloads.&lt;br /&gt;
&lt;br /&gt;
== Problem Overview ==&lt;br /&gt;
The hyperbolic tangent function poses several challenges when implemented in finite-precision floating-point arithmetic. While the mathematical definition is simple, direct translations into software often suffer from numerical instability and performance issues.&lt;br /&gt;
&lt;br /&gt;
A primary challenge is the use of exponential functions. For large input values, &amp;lt;code&amp;gt;exp(x)&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;exp(-x)&amp;lt;/code&amp;gt; can overflow in single-precision floating-point arithmetic, leading to infinities and, in some formulations, NaN results. Avoiding overflow therefore requires careful reformulation or explicit range handling.&lt;br /&gt;
&lt;br /&gt;
For very small input values, catastrophic cancellation becomes the dominant issue. Expressions such as &amp;lt;math&amp;gt;e^x - e^{-x}&amp;lt;/math&amp;gt; involve the subtraction of nearly equal numbers, causing a severe loss of significant bits and large ULP errors. In these regions, naive implementations may return zero or highly inaccurate results even though &amp;lt;math&amp;gt;\tanh(x) \approx x&amp;lt;/math&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
In addition to accuracy concerns, performance is a critical factor. In many applications, especially machine learning and numerical simulations, &amp;lt;code&amp;gt;tanh&amp;lt;/code&amp;gt; is evaluated extremely frequently. This makes it necessary to balance numerical robustness with the cost of transcendental function calls, branch complexity, and instruction-level efficiency.&lt;br /&gt;
&lt;br /&gt;
==Direct Exponential Formulations==&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+&lt;br /&gt;
!Id&lt;br /&gt;
!Formula&lt;br /&gt;
!Highest ULP Error&lt;br /&gt;
!ULP &amp;lt;= 2.0&lt;br /&gt;
!NaN&lt;br /&gt;
|-&lt;br /&gt;
|exp_v1&lt;br /&gt;
|&amp;lt;math&amp;gt;\frac{e^x - e^{-x}}{e^x + e^{-x}}&amp;lt;/math&amp;gt;&lt;br /&gt;
|x &amp;lt; 0: 8.55638e+08 at -2.98023e-08 with 0x0p+0 instead of -0x1p-25&lt;br /&gt;
&lt;br /&gt;
x &amp;gt; 0: 8.55638e+08 at 2.98023e-08 with 0x0p+0 instead of 0x1p-25&lt;br /&gt;
| -88.7246 &amp;lt; x &amp;lt; -7.14197&lt;br /&gt;
7.14197 &amp;lt; x &amp;lt; 88.7246&lt;br /&gt;
| -inf &amp;lt; x &amp;lt; -88.7246&lt;br /&gt;
88.7246 &amp;lt; x &amp;lt; inf&lt;br /&gt;
|-&lt;br /&gt;
|exp_v2&lt;br /&gt;
|&amp;lt;math&amp;gt;\frac{e^{2x} - 1}{e^{2x} + 1}&amp;lt;/math&amp;gt;&lt;br /&gt;
|x &amp;lt; 0: 8.47249e+08 at -1.49012e-08 with 0x0p+0 instead of -0x1p-26&lt;br /&gt;
x &amp;gt; 0: 8.55638e+08 at 2.98023e-08 with 0x0p+0 instead of 0x1.fffffep-26&lt;br /&gt;
| -inf &amp;lt; x &amp;lt; -0.547821&lt;br /&gt;
0.254822 &amp;lt; x &amp;lt; 8.31787&lt;br /&gt;
&lt;br /&gt;
8.38379 &amp;lt; x &amp;lt; 44.3623&lt;br /&gt;
|44.3623 &amp;lt; x &amp;lt; inf&lt;br /&gt;
|-&lt;br /&gt;
|exp_v3&lt;br /&gt;
|&amp;lt;math&amp;gt;1 - \frac{2}{e^{2x} + 1}&amp;lt;/math&amp;gt;&lt;br /&gt;
|x &amp;lt; 0: 8.59832e+08 at -4.47035e-08 with 0x0p+0 instead of -0x1.8p-25&lt;br /&gt;
x &amp;gt; 0: 8.68221e+08 at 8.9407e-08 with 0x0p+0 instead of 0x1.7ffffcp-24&lt;br /&gt;
| -inf &amp;lt; x &amp;lt; -6.26233&lt;br /&gt;
0.346382 &amp;lt; x &amp;lt; inf&lt;br /&gt;
| -&lt;br /&gt;
|-&lt;br /&gt;
|expm1&lt;br /&gt;
|&amp;lt;math&amp;gt;\frac{expm1(2x)}{expm1(2x) + 2}&amp;lt;/math&amp;gt; &lt;br /&gt;
|x &amp;lt; 0: 2.49585 at -3.95627 with -0x1.ffa00ap-1 instead of -0x1.ffa00ep-1&lt;br /&gt;
x &amp;gt; 0: 2.41657 at 0.0155837 with 0x1.fe9b66p-7 instead of 0x1.fe9b62p-7&lt;br /&gt;
| -inf &amp;lt; x &amp;lt; -5.57227&lt;br /&gt;
-0.42981 &amp;lt; x &amp;lt; 0.00777948&lt;br /&gt;
&lt;br /&gt;
0.125351 &amp;lt; x &amp;lt; 44.3623&lt;br /&gt;
|44.3623 &amp;lt; x &amp;lt; inf&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
The first equation is the definition of the hyperbolic tangent function. This equation requires two exponentials with different values. Since exponentials are expensive to compute, we can use the second equation to reduce the number of exponentials to one.&lt;br /&gt;
&lt;br /&gt;
The second equation has the drawback that both the numerator and the denominator can become infinity for large values of x which results in NaN. That happens for &amp;lt;math&amp;gt;|x| &amp;gt; \log(FLT\_MAX)/2 \approx 44&amp;lt;/math&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
The third equation solves this problem but has a slightly higher error for &amp;lt;math&amp;gt;x \approx 2^{-25}&amp;lt;/math&amp;gt;. In the following graph you can see the error chart for equation 3. The error charts for the other two equations are very similar. All three equations have an error of up to 16 mio ULPs for &amp;lt;math&amp;gt;x &amp;lt; 2^{-5}&amp;lt;/math&amp;gt; which means that up to 24 bits could be wrong.&lt;br /&gt;
&lt;br /&gt;
=== exp_v2 ===&lt;br /&gt;
&lt;br /&gt;
The following error graph shows the ULP error of the &amp;lt;code&amp;gt;exp_v2&amp;lt;/code&amp;gt; formulation across the full &amp;lt;code&amp;gt;float32&amp;lt;/code&amp;gt; input range. &lt;br /&gt;
This formulation reduces the number of exponential evaluations to one, but is particularly sensitive to catastrophic cancellation for very small input values and to overflow for large &amp;lt;math&amp;gt;|x|&amp;lt;/math&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
{{#widget:FloatErrorGraph|id=tanhf_exp_v2&lt;br /&gt;
  |data=[ [&amp;quot;-2^127&amp;quot;,0,1.17549e-38],[&amp;quot;-2^126&amp;quot;,0,1.17549e-38],[&amp;quot;-2^125&amp;quot;,0,1.17549e-38],[&amp;quot;-2^124&amp;quot;,0,1.17549e-38],[&amp;quot;-2^123&amp;quot;,0,1.17549e-38],[&amp;quot;-2^122&amp;quot;,0,1.17549e-38],[&amp;quot;-2^121&amp;quot;,0,1.17549e-38],[&amp;quot;-2^120&amp;quot;,0,1.17549e-38],[&amp;quot;-2^119&amp;quot;,0,1.17549e-38],[&amp;quot;-2^118&amp;quot;,0,1.17549e-38],[&amp;quot;-2^117&amp;quot;,0,1.17549e-38],[&amp;quot;-2^116&amp;quot;,0,1.17549e-38],[&amp;quot;-2^115&amp;quot;,0,1.17549e-38],[&amp;quot;-2^114&amp;quot;,0,1.17549e-38],[&amp;quot;-2^113&amp;quot;,0,1.17549e-38],[&amp;quot;-2^112&amp;quot;,0,1.17549e-38],[&amp;quot;-2^111&amp;quot;,0,1.17549e-38],[&amp;quot;-2^110&amp;quot;,0,1.17549e-38],[&amp;quot;-2^109&amp;quot;,0,1.17549e-38],[&amp;quot;-2^108&amp;quot;,0,1.17549e-38],[&amp;quot;-2^107&amp;quot;,0,1.17549e-38],[&amp;quot;-2^106&amp;quot;,0,1.17549e-38],[&amp;quot;-2^105&amp;quot;,0,1.17549e-38],[&amp;quot;-2^104&amp;quot;,0,1.17549e-38],[&amp;quot;-2^103&amp;quot;,0,1.17549e-38],[&amp;quot;-2^102&amp;quot;,0,1.17549e-38],[&amp;quot;-2^101&amp;quot;,0,1.17549e-38],[&amp;quot;-2^100&amp;quot;,0,1.17549e-38],[&amp;quot;-2^99&amp;quot;,0,1.17549e-38],[&amp;quot;-2^98&amp;quot;,0,1.17549e-38],[&amp;quot;-2^97&amp;quot;,0,1.17549e-38],[&amp;quot;-2^96&amp;quot;,0,1.17549e-38],[&amp;quot;-2^95&amp;quot;,0,1.17549e-38],[&amp;quot;-2^94&amp;quot;,0,1.17549e-38],[&amp;quot;-2^93&amp;quot;,0,1.17549e-38],[&amp;quot;-2^92&amp;quot;,0,1.17549e-38],[&amp;quot;-2^91&amp;quot;,0,1.17549e-38],[&amp;quot;-2^90&amp;quot;,0,1.17549e-38],[&amp;quot;-2^89&amp;quot;,0,1.17549e-38],[&amp;quot;-2^88&amp;quot;,0,1.17549e-38],[&amp;quot;-2^87&amp;quot;,0,1.17549e-38],[&amp;quot;-2^86&amp;quot;,0,1.17549e-38],[&amp;quot;-2^85&amp;quot;,0,1.17549e-38],[&amp;quot;-2^84&amp;quot;,0,1.17549e-38],[&amp;quot;-2^83&amp;quot;,0,1.17549e-38],[&amp;quot;-2^82&amp;quot;,0,1.17549e-38],[&amp;quot;-2^81&amp;quot;,0,1.17549e-38],[&amp;quot;-2^80&amp;quot;,0,1.17549e-38],[&amp;quot;-2^79&amp;quot;,0,1.17549e-38],[&amp;quot;-2^78&amp;quot;,0,1.17549e-38],[&amp;quot;-2^77&amp;quot;,0,1.17549e-38],[&amp;quot;-2^76&amp;quot;,0,1.17549e-38],[&amp;quot;-2^75&amp;quot;,0,1.17549e-38],[&amp;quot;-2^74&amp;quot;,0,1.17549e-38],[&amp;quot;-2^73&amp;quot;,0,1.17549e-38],[&amp;quot;-2^72&amp;quot;,0,1.17549e-38],[&amp;quot;-2^71&amp;quot;,0,1.17549e-38],[&amp;quot;-2^70&amp;quot;,0,1.17549e-38],[&amp;quot;-2^69&amp;quot;,0,1.17549e-38],[&amp;quot;-2^68&amp;quot;,0,1.17549e-38],[&amp;quot;-2^67&amp;quot;,0,1.17549e-38],[&amp;quot;-2^66&amp;quot;,0,1.17549e-38],[&amp;quot;-2^65&amp;quot;,0,1.17549e-38],[&amp;quot;-2^64&amp;quot;,0,1.17549e-38],[&amp;quot;-2^63&amp;quot;,0,1.17549e-38],[&amp;quot;-2^62&amp;quot;,0,1.17549e-38],[&amp;quot;-2^61&amp;quot;,0,1.17549e-38],[&amp;quot;-2^60&amp;quot;,0,1.17549e-38],[&amp;quot;-2^59&amp;quot;,0,1.17549e-38],[&amp;quot;-2^58&amp;quot;,0,1.17549e-38],[&amp;quot;-2^57&amp;quot;,0,1.17549e-38],[&amp;quot;-2^56&amp;quot;,0,1.17549e-38],[&amp;quot;-2^55&amp;quot;,0,1.17549e-38],[&amp;quot;-2^54&amp;quot;,0,1.17549e-38],[&amp;quot;-2^53&amp;quot;,0,1.17549e-38],[&amp;quot;-2^52&amp;quot;,0,1.17549e-38],[&amp;quot;-2^51&amp;quot;,0,1.17549e-38],[&amp;quot;-2^50&amp;quot;,0,1.17549e-38],[&amp;quot;-2^49&amp;quot;,0,1.17549e-38],[&amp;quot;-2^48&amp;quot;,0,1.17549e-38],[&amp;quot;-2^47&amp;quot;,0,1.17549e-38],[&amp;quot;-2^46&amp;quot;,0,1.17549e-38],[&amp;quot;-2^45&amp;quot;,0,1.17549e-38],[&amp;quot;-2^44&amp;quot;,0,1.17549e-38],[&amp;quot;-2^43&amp;quot;,0,1.17549e-38],[&amp;quot;-2^42&amp;quot;,0,1.17549e-38],[&amp;quot;-2^41&amp;quot;,0,1.17549e-38],[&amp;quot;-2^40&amp;quot;,0,1.17549e-38],[&amp;quot;-2^39&amp;quot;,0,1.17549e-38],[&amp;quot;-2^38&amp;quot;,0,1.17549e-38],[&amp;quot;-2^37&amp;quot;,0,1.17549e-38],[&amp;quot;-2^36&amp;quot;,0,1.17549e-38],[&amp;quot;-2^35&amp;quot;,0,1.17549e-38],[&amp;quot;-2^34&amp;quot;,0,1.17549e-38],[&amp;quot;-2^33&amp;quot;,0,1.17549e-38],[&amp;quot;-2^32&amp;quot;,0,1.17549e-38],[&amp;quot;-2^31&amp;quot;,0,1.17549e-38],[&amp;quot;-2^30&amp;quot;,0,1.17549e-38],[&amp;quot;-2^29&amp;quot;,0,1.17549e-38],[&amp;quot;-2^28&amp;quot;,0,1.17549e-38],[&amp;quot;-2^27&amp;quot;,0,1.17549e-38],[&amp;quot;-2^26&amp;quot;,0,1.17549e-38],[&amp;quot;-2^25&amp;quot;,0,1.17549e-38],[&amp;quot;-2^24&amp;quot;,0,1.17549e-38],[&amp;quot;-2^23&amp;quot;,0,1.17549e-38],[&amp;quot;-2^22&amp;quot;,0,1.17549e-38],[&amp;quot;-2^21&amp;quot;,0,1.17549e-38],[&amp;quot;-2^20&amp;quot;,0,1.17549e-38],[&amp;quot;-2^19&amp;quot;,0,1.17549e-38],[&amp;quot;-2^18&amp;quot;,0,1.17549e-38],[&amp;quot;-2^17&amp;quot;,0,1.17549e-38],[&amp;quot;-2^16&amp;quot;,0,1.17549e-38],[&amp;quot;-2^15&amp;quot;,0,1.17549e-38],[&amp;quot;-2^14&amp;quot;,0,1.17549e-38],[&amp;quot;-2^13&amp;quot;,0,1.17549e-38],[&amp;quot;-2^12&amp;quot;,0,1.17549e-38],[&amp;quot;-2^11&amp;quot;,0,1.17549e-38],[&amp;quot;-2^10&amp;quot;,0,1.17549e-38],[&amp;quot;-2^9&amp;quot;,0,1.17549e-38],[&amp;quot;-2^8&amp;quot;,0,1.17549e-38],[&amp;quot;-2^7&amp;quot;,0,1.17549e-38],[&amp;quot;-2^6&amp;quot;,0,1.17549e-38],[&amp;quot;-2^5&amp;quot;,0,1.17549e-38],[&amp;quot;-2^4&amp;quot;,2.53131e-14,4.24683e-07],[&amp;quot;-2^3&amp;quot;,5.95888e-08,0.999734],[&amp;quot;-2^2&amp;quot;,8.86696e-08,1.48763],[&amp;quot;-2^1&amp;quot;,8.91609e-08,1.49587],[&amp;quot;-2^0&amp;quot;,8.73743e-08,1.4659],[&amp;quot;-2^-1&amp;quot;,8.65382e-08,2.13222],[&amp;quot;-2^-2&amp;quot;,6.18481e-08,2.61428],[&amp;quot;-2^-3&amp;quot;,3.88071e-08,3.08594],[&amp;quot;-2^-4&amp;quot;,2.6234e-08,4.23594],[&amp;quot;-2^-5&amp;quot;,2.02799e-08,8.19726],[&amp;quot;-2^-6&amp;quot;,1.75183e-08,12.4495],[&amp;quot;-2^-7&amp;quot;,1.61937e-08,17.3878],[&amp;quot;-2^-8&amp;quot;,1.5566e-08,33.4278],[&amp;quot;-2^-9&amp;quot;,1.51688e-08,65.1495],[&amp;quot;-2^-10&amp;quot;,1.50281e-08,129.09],[&amp;quot;-2^-11&amp;quot;,1.49613e-08,257.033],[&amp;quot;-2^-12&amp;quot;,1.49296e-08,512.977],[&amp;quot;-2^-13&amp;quot;,1.49174e-08,1025.12],[&amp;quot;-2^-14&amp;quot;,1.4909e-08,2049.08],[&amp;quot;-2^-15&amp;quot;,1.49049e-08,4097.02],[&amp;quot;-2^-16&amp;quot;,1.4903e-08,8193],[&amp;quot;-2^-17&amp;quot;,1.49021e-08,16385],[&amp;quot;-2^-18&amp;quot;,1.48862e-08,32735],[&amp;quot;-2^-19&amp;quot;,1.48973e-08,65519],[&amp;quot;-2^-20&amp;quot;,1.49001e-08,131063],[&amp;quot;-2^-21&amp;quot;,1.49009e-08,262139],[&amp;quot;-2^-22&amp;quot;,1.49011e-08,524285],[&amp;quot;-2^-23&amp;quot;,1.49011e-08,1.04857e+06],[&amp;quot;-2^-24&amp;quot;,1.49011e-08,2.09715e+06],[&amp;quot;-2^-25&amp;quot;,1.49012e-08,4.1943e+06],[&amp;quot;-2^-26&amp;quot;,1.49007e-08,8.38835e+06],[&amp;quot;-2^-27&amp;quot;,1.49012e-08,8.47249e+08],[&amp;quot;-2^-28&amp;quot;,7.45058e-09,8.38861e+08],[&amp;quot;-2^-29&amp;quot;,3.72529e-09,8.30472e+08],[&amp;quot;-2^-30&amp;quot;,1.86265e-09,8.22084e+08],[&amp;quot;-2^-31&amp;quot;,9.31323e-10,8.13695e+08],[&amp;quot;-2^-32&amp;quot;,4.65661e-10,8.05306e+08],[&amp;quot;-2^-33&amp;quot;,2.32831e-10,7.96918e+08],[&amp;quot;-2^-34&amp;quot;,1.16415e-10,7.88529e+08],[&amp;quot;-2^-35&amp;quot;,5.82077e-11,7.80141e+08],[&amp;quot;-2^-36&amp;quot;,2.91038e-11,7.71752e+08],[&amp;quot;-2^-37&amp;quot;,1.45519e-11,7.63363e+08],[&amp;quot;-2^-38&amp;quot;,7.27596e-12,7.54975e+08],[&amp;quot;-2^-39&amp;quot;,3.63798e-12,7.46586e+08],[&amp;quot;-2^-40&amp;quot;,1.81899e-12,7.38198e+08],[&amp;quot;-2^-41&amp;quot;,9.09495e-13,7.29809e+08],[&amp;quot;-2^-42&amp;quot;,4.54747e-13,7.2142e+08],[&amp;quot;-2^-43&amp;quot;,2.27374e-13,7.13032e+08],[&amp;quot;-2^-44&amp;quot;,1.13687e-13,7.04643e+08],[&amp;quot;-2^-45&amp;quot;,5.68434e-14,6.96254e+08],[&amp;quot;-2^-46&amp;quot;,2.84217e-14,6.87866e+08],[&amp;quot;-2^-47&amp;quot;,1.42109e-14,6.79477e+08],[&amp;quot;-2^-48&amp;quot;,7.10543e-15,6.71089e+08],[&amp;quot;-2^-49&amp;quot;,3.55271e-15,6.627e+08],[&amp;quot;-2^-50&amp;quot;,1.77636e-15,6.54311e+08],[&amp;quot;-2^-51&amp;quot;,8.88178e-16,6.45923e+08],[&amp;quot;-2^-52&amp;quot;,4.44089e-16,6.37534e+08],[&amp;quot;-2^-53&amp;quot;,2.22045e-16,6.29146e+08],[&amp;quot;-2^-54&amp;quot;,1.11022e-16,6.20757e+08],[&amp;quot;-2^-55&amp;quot;,5.55111e-17,6.12368e+08],[&amp;quot;-2^-56&amp;quot;,2.77556e-17,6.0398e+08],[&amp;quot;-2^-57&amp;quot;,1.38778e-17,5.95591e+08],[&amp;quot;-2^-58&amp;quot;,6.93889e-18,5.87203e+08],[&amp;quot;-2^-59&amp;quot;,3.46945e-18,5.78814e+08],[&amp;quot;-2^-60&amp;quot;,1.73472e-18,5.70425e+08],[&amp;quot;-2^-61&amp;quot;,8.67362e-19,5.62037e+08],[&amp;quot;-2^-62&amp;quot;,4.33681e-19,5.53648e+08],[&amp;quot;-2^-63&amp;quot;,2.1684e-19,5.4526e+08],[&amp;quot;-2^-64&amp;quot;,1.0842e-19,5.36871e+08],[&amp;quot;-2^-65&amp;quot;,5.42101e-20,5.28482e+08],[&amp;quot;-2^-66&amp;quot;,2.71051e-20,5.20094e+08],[&amp;quot;-2^-67&amp;quot;,1.35525e-20,5.11705e+08],[&amp;quot;-2^-68&amp;quot;,6.77626e-21,5.03316e+08],[&amp;quot;-2^-69&amp;quot;,3.38813e-21,4.94928e+08],[&amp;quot;-2^-70&amp;quot;,1.69407e-21,4.86539e+08],[&amp;quot;-2^-71&amp;quot;,8.47033e-22,4.78151e+08],[&amp;quot;-2^-72&amp;quot;,4.23516e-22,4.69762e+08],[&amp;quot;-2^-73&amp;quot;,2.11758e-22,4.61373e+08],[&amp;quot;-2^-74&amp;quot;,1.05879e-22,4.52985e+08],[&amp;quot;-2^-75&amp;quot;,5.29396e-23,4.44596e+08],[&amp;quot;-2^-76&amp;quot;,2.64698e-23,4.36208e+08],[&amp;quot;-2^-77&amp;quot;,1.32349e-23,4.27819e+08],[&amp;quot;-2^-78&amp;quot;,6.61744e-24,4.1943e+08],[&amp;quot;-2^-79&amp;quot;,3.30872e-24,4.11042e+08],[&amp;quot;-2^-80&amp;quot;,1.65436e-24,4.02653e+08],[&amp;quot;-2^-81&amp;quot;,8.27181e-25,3.94265e+08],[&amp;quot;-2^-82&amp;quot;,4.1359e-25,3.85876e+08],[&amp;quot;-2^-83&amp;quot;,2.06795e-25,3.77487e+08],[&amp;quot;-2^-84&amp;quot;,1.03398e-25,3.69099e+08],[&amp;quot;-2^-85&amp;quot;,5.16988e-26,3.6071e+08],[&amp;quot;-2^-86&amp;quot;,2.58494e-26,3.52322e+08],[&amp;quot;-2^-87&amp;quot;,1.29247e-26,3.43933e+08],[&amp;quot;-2^-88&amp;quot;,6.46235e-27,3.35544e+08],[&amp;quot;-2^-89&amp;quot;,3.23117e-27,3.27156e+08],[&amp;quot;-2^-90&amp;quot;,1.61559e-27,3.18767e+08],[&amp;quot;-2^-91&amp;quot;,8.07794e-28,3.10378e+08],[&amp;quot;-2^-92&amp;quot;,4.03897e-28,3.0199e+08],[&amp;quot;-2^-93&amp;quot;,2.01948e-28,2.93601e+08],[&amp;quot;-2^-94&amp;quot;,1.00974e-28,2.85213e+08],[&amp;quot;-2^-95&amp;quot;,5.04871e-29,2.76824e+08],[&amp;quot;-2^-96&amp;quot;,2.52435e-29,2.68435e+08],[&amp;quot;-2^-97&amp;quot;,1.26218e-29,2.60047e+08],[&amp;quot;-2^-98&amp;quot;,6.31089e-30,2.51658e+08],[&amp;quot;-2^-99&amp;quot;,3.15544e-30,2.4327e+08],[&amp;quot;-2^-100&amp;quot;,1.57772e-30,2.34881e+08],[&amp;quot;-2^-101&amp;quot;,7.88861e-31,2.26492e+08],[&amp;quot;-2^-102&amp;quot;,3.9443e-31,2.18104e+08],[&amp;quot;-2^-103&amp;quot;,1.97215e-31,2.09715e+08],[&amp;quot;-2^-104&amp;quot;,9.86076e-32,2.01327e+08],[&amp;quot;-2^-105&amp;quot;,4.93038e-32,1.92938e+08],[&amp;quot;-2^-106&amp;quot;,2.46519e-32,1.84549e+08],[&amp;quot;-2^-107&amp;quot;,1.2326e-32,1.76161e+08],[&amp;quot;-2^-108&amp;quot;,6.16298e-33,1.67772e+08],[&amp;quot;-2^-109&amp;quot;,3.08149e-33,1.59384e+08],[&amp;quot;-2^-110&amp;quot;,1.54074e-33,1.50995e+08],[&amp;quot;-2^-111&amp;quot;,7.70372e-34,1.42606e+08],[&amp;quot;-2^-112&amp;quot;,3.85186e-34,1.34218e+08],[&amp;quot;-2^-113&amp;quot;,1.92593e-34,1.25829e+08],[&amp;quot;-2^-114&amp;quot;,9.62965e-35,1.17441e+08],[&amp;quot;-2^-115&amp;quot;,4.81482e-35,1.09052e+08],[&amp;quot;-2^-116&amp;quot;,2.40741e-35,1.00663e+08],[&amp;quot;-2^-117&amp;quot;,1.20371e-35,9.22747e+07],[&amp;quot;-2^-118&amp;quot;,6.01853e-36,8.38861e+07],[&amp;quot;-2^-119&amp;quot;,3.00927e-36,7.54975e+07],[&amp;quot;-2^-120&amp;quot;,1.50463e-36,6.71089e+07],[&amp;quot;-2^-121&amp;quot;,7.52316e-37,5.87203e+07],[&amp;quot;-2^-122&amp;quot;,3.76158e-37,5.03316e+07],[&amp;quot;-2^-123&amp;quot;,1.88079e-37,4.1943e+07],[&amp;quot;-2^-124&amp;quot;,9.40395e-38,3.35544e+07],[&amp;quot;-2^-125&amp;quot;,4.70198e-38,2.51658e+07],[&amp;quot;-2^-126&amp;quot;,2.35099e-38,1.67772e+07],[&amp;quot;-SN&amp;quot;,1.17546e-38,8.38835e+06],[&amp;quot;0&amp;quot;,0,1.17549e-38],[&amp;quot;+SN&amp;quot;,1.17546e-38,8.38835e+06],[&amp;quot;+2^-126&amp;quot;,2.35095e-38,1.6777e+07],[&amp;quot;+2^-125&amp;quot;,4.70191e-38,2.51656e+07],[&amp;quot;+2^-124&amp;quot;,9.40381e-38,3.35542e+07],[&amp;quot;+2^-123&amp;quot;,1.88076e-37,4.19428e+07],[&amp;quot;+2^-122&amp;quot;,3.76152e-37,5.03314e+07],[&amp;quot;+2^-121&amp;quot;,7.52305e-37,5.872e+07],[&amp;quot;+2^-120&amp;quot;,1.50461e-36,6.71086e+07],[&amp;quot;+2^-119&amp;quot;,3.00922e-36,7.54972e+07],[&amp;quot;+2^-118&amp;quot;,6.01844e-36,8.38858e+07],[&amp;quot;+2^-117&amp;quot;,1.20369e-35,9.22744e+07],[&amp;quot;+2^-116&amp;quot;,2.40738e-35,1.00663e+08],[&amp;quot;+2^-115&amp;quot;,4.81475e-35,1.09052e+08],[&amp;quot;+2^-114&amp;quot;,9.6295e-35,1.1744e+08],[&amp;quot;+2^-113&amp;quot;,1.9259e-34,1.25829e+08],[&amp;quot;+2^-112&amp;quot;,3.8518e-34,1.34217e+08],[&amp;quot;+2^-111&amp;quot;,7.7036e-34,1.42606e+08],[&amp;quot;+2^-110&amp;quot;,1.54072e-33,1.50995e+08],[&amp;quot;+2^-109&amp;quot;,3.08144e-33,1.59383e+08],[&amp;quot;+2^-108&amp;quot;,6.16288e-33,1.67772e+08],[&amp;quot;+2^-107&amp;quot;,1.23258e-32,1.76161e+08],[&amp;quot;+2^-106&amp;quot;,2.46515e-32,1.84549e+08],[&amp;quot;+2^-105&amp;quot;,4.93031e-32,1.92938e+08],[&amp;quot;+2^-104&amp;quot;,9.86061e-32,2.01326e+08],[&amp;quot;+2^-103&amp;quot;,1.97212e-31,2.09715e+08],[&amp;quot;+2^-102&amp;quot;,3.94424e-31,2.18104e+08],[&amp;quot;+2^-101&amp;quot;,7.88849e-31,2.26492e+08],[&amp;quot;+2^-100&amp;quot;,1.5777e-30,2.34881e+08],[&amp;quot;+2^-99&amp;quot;,3.1554e-30,2.43269e+08],[&amp;quot;+2^-98&amp;quot;,6.31079e-30,2.51658e+08],[&amp;quot;+2^-97&amp;quot;,1.26216e-29,2.60047e+08],[&amp;quot;+2^-96&amp;quot;,2.52432e-29,2.68435e+08],[&amp;quot;+2^-95&amp;quot;,5.04863e-29,2.76824e+08],[&amp;quot;+2^-94&amp;quot;,1.00973e-28,2.85212e+08],[&amp;quot;+2^-93&amp;quot;,2.01945e-28,2.93601e+08],[&amp;quot;+2^-92&amp;quot;,4.03891e-28,3.0199e+08],[&amp;quot;+2^-91&amp;quot;,8.07781e-28,3.10378e+08],[&amp;quot;+2^-90&amp;quot;,1.61556e-27,3.18767e+08],[&amp;quot;+2^-89&amp;quot;,3.23112e-27,3.27155e+08],[&amp;quot;+2^-88&amp;quot;,6.46225e-27,3.35544e+08],[&amp;quot;+2^-87&amp;quot;,1.29245e-26,3.43933e+08],[&amp;quot;+2^-86&amp;quot;,2.5849e-26,3.52321e+08],[&amp;quot;+2^-85&amp;quot;,5.1698e-26,3.6071e+08],[&amp;quot;+2^-84&amp;quot;,1.03396e-25,3.69098e+08],[&amp;quot;+2^-83&amp;quot;,2.06792e-25,3.77487e+08],[&amp;quot;+2^-82&amp;quot;,4.13584e-25,3.85876e+08],[&amp;quot;+2^-81&amp;quot;,8.27168e-25,3.94264e+08],[&amp;quot;+2^-80&amp;quot;,1.65434e-24,4.02653e+08],[&amp;quot;+2^-79&amp;quot;,3.30867e-24,4.11042e+08],[&amp;quot;+2^-78&amp;quot;,6.61734e-24,4.1943e+08],[&amp;quot;+2^-77&amp;quot;,1.32347e-23,4.27819e+08],[&amp;quot;+2^-76&amp;quot;,2.64694e-23,4.36207e+08],[&amp;quot;+2^-75&amp;quot;,5.29388e-23,4.44596e+08],[&amp;quot;+2^-74&amp;quot;,1.05878e-22,4.52985e+08],[&amp;quot;+2^-73&amp;quot;,2.11755e-22,4.61373e+08],[&amp;quot;+2^-72&amp;quot;,4.2351e-22,4.69762e+08],[&amp;quot;+2^-71&amp;quot;,8.4702e-22,4.7815e+08],[&amp;quot;+2^-70&amp;quot;,1.69404e-21,4.86539e+08],[&amp;quot;+2^-69&amp;quot;,3.38808e-21,4.94928e+08],[&amp;quot;+2^-68&amp;quot;,6.77616e-21,5.03316e+08],[&amp;quot;+2^-67&amp;quot;,1.35523e-20,5.11705e+08],[&amp;quot;+2^-66&amp;quot;,2.71046e-20,5.20093e+08],[&amp;quot;+2^-65&amp;quot;,5.42093e-20,5.28482e+08],[&amp;quot;+2^-64&amp;quot;,1.08419e-19,5.36871e+08],[&amp;quot;+2^-63&amp;quot;,2.16837e-19,5.45259e+08],[&amp;quot;+2^-62&amp;quot;,4.33674e-19,5.53648e+08],[&amp;quot;+2^-61&amp;quot;,8.67349e-19,5.62036e+08],[&amp;quot;+2^-60&amp;quot;,1.7347e-18,5.70425e+08],[&amp;quot;+2^-59&amp;quot;,3.46939e-18,5.78814e+08],[&amp;quot;+2^-58&amp;quot;,6.93879e-18,5.87202e+08],[&amp;quot;+2^-57&amp;quot;,1.38776e-17,5.95591e+08],[&amp;quot;+2^-56&amp;quot;,2.77552e-17,6.0398e+08],[&amp;quot;+2^-55&amp;quot;,5.55103e-17,6.12368e+08],[&amp;quot;+2^-54&amp;quot;,1.11021e-16,6.20757e+08],[&amp;quot;+2^-53&amp;quot;,2.22041e-16,6.29145e+08],[&amp;quot;+2^-52&amp;quot;,4.44082e-16,6.37534e+08],[&amp;quot;+2^-51&amp;quot;,8.88165e-16,6.45923e+08],[&amp;quot;+2^-50&amp;quot;,1.77633e-15,6.54311e+08],[&amp;quot;+2^-49&amp;quot;,3.55266e-15,6.627e+08],[&amp;quot;+2^-48&amp;quot;,7.10532e-15,6.71088e+08],[&amp;quot;+2^-47&amp;quot;,1.42106e-14,6.79477e+08],[&amp;quot;+2^-46&amp;quot;,2.84213e-14,6.87866e+08],[&amp;quot;+2^-45&amp;quot;,5.68426e-14,6.96254e+08],[&amp;quot;+2^-44&amp;quot;,1.13685e-13,7.04643e+08],[&amp;quot;+2^-43&amp;quot;,2.2737e-13,7.13031e+08],[&amp;quot;+2^-42&amp;quot;,4.5474e-13,7.2142e+08],[&amp;quot;+2^-41&amp;quot;,9.09481e-13,7.29809e+08],[&amp;quot;+2^-40&amp;quot;,1.81896e-12,7.38197e+08],[&amp;quot;+2^-39&amp;quot;,3.63792e-12,7.46586e+08],[&amp;quot;+2^-38&amp;quot;,7.27585e-12,7.54974e+08],[&amp;quot;+2^-37&amp;quot;,1.45517e-11,7.63363e+08],[&amp;quot;+2^-36&amp;quot;,2.91034e-11,7.71752e+08],[&amp;quot;+2^-35&amp;quot;,5.82068e-11,7.8014e+08],[&amp;quot;+2^-34&amp;quot;,1.16414e-10,7.88529e+08],[&amp;quot;+2^-33&amp;quot;,2.32827e-10,7.96918e+08],[&amp;quot;+2^-32&amp;quot;,4.65654e-10,8.05306e+08],[&amp;quot;+2^-31&amp;quot;,9.31308e-10,8.13695e+08],[&amp;quot;+2^-30&amp;quot;,1.86262e-09,8.22083e+08],[&amp;quot;+2^-29&amp;quot;,3.72523e-09,8.30472e+08],[&amp;quot;+2^-28&amp;quot;,7.45047e-09,8.38861e+08],[&amp;quot;+2^-27&amp;quot;,1.49009e-08,8.47249e+08],[&amp;quot;+2^-26&amp;quot;,2.98019e-08,8.55638e+08],[&amp;quot;+2^-25&amp;quot;,2.98023e-08,8.38861e+06],[&amp;quot;+2^-24&amp;quot;,2.98023e-08,4.1943e+06],[&amp;quot;+2^-23&amp;quot;,2.98023e-08,2.09715e+06],[&amp;quot;+2^-22&amp;quot;,2.98023e-08,1.04857e+06],[&amp;quot;+2^-21&amp;quot;,2.98021e-08,524284],[&amp;quot;+2^-20&amp;quot;,2.98014e-08,262136],[&amp;quot;+2^-19&amp;quot;,2.97987e-08,131056],[&amp;quot;+2^-18&amp;quot;,2.97878e-08,65504],[&amp;quot;+2^-17&amp;quot;,2.98014e-08,32767],[&amp;quot;+2^-16&amp;quot;,2.98005e-08,16384],[&amp;quot;+2^-15&amp;quot;,2.98023e-08,8192],[&amp;quot;+2^-14&amp;quot;,2.98025e-08,4351.99],[&amp;quot;+2^-13&amp;quot;,2.98044e-08,2815.96],[&amp;quot;+2^-12&amp;quot;,2.98091e-08,1024.23],[&amp;quot;+2^-11&amp;quot;,2.9812e-08,512.167],[&amp;quot;+2^-10&amp;quot;,2.98154e-08,256.113],[&amp;quot;+2^-9&amp;quot;,2.98591e-08,128.244],[&amp;quot;+2^-8&amp;quot;,3.00204e-08,85.3328],[&amp;quot;+2^-7&amp;quot;,3.01947e-08,43.675],[&amp;quot;+2^-6&amp;quot;,3.05713e-08,30.45],[&amp;quot;+2^-5&amp;quot;,3.14319e-08,8.43743],[&amp;quot;+2^-4&amp;quot;,3.31291e-08,7.77058],[&amp;quot;+2^-3&amp;quot;,3.58597e-08,3.99659],[&amp;quot;+2^-2&amp;quot;,4.14639e-08,2.29246],[&amp;quot;+2^-1&amp;quot;,8.02409e-08,1.34622],[&amp;quot;+2^0&amp;quot;,8.66201e-08,1.45324],[&amp;quot;+2^1&amp;quot;,8.8654e-08,1.48737],[&amp;quot;+2^2&amp;quot;,6.7976e-08,1.14045],[&amp;quot;+2^3&amp;quot;,1.33955e-07,2.2474],[&amp;quot;+2^4&amp;quot;,2.53131e-14,4.24683e-07],[&amp;quot;+2^5&amp;quot;,0,null],[&amp;quot;+2^6&amp;quot;,0,null],[&amp;quot;+2^7&amp;quot;,0,null],[&amp;quot;+2^8&amp;quot;,0,null],[&amp;quot;+2^9&amp;quot;,0,null],[&amp;quot;+2^10&amp;quot;,0,null],[&amp;quot;+2^11&amp;quot;,0,null],[&amp;quot;+2^12&amp;quot;,0,null],[&amp;quot;+2^13&amp;quot;,0,null],[&amp;quot;+2^14&amp;quot;,0,null],[&amp;quot;+2^15&amp;quot;,0,null],[&amp;quot;+2^16&amp;quot;,0,null],[&amp;quot;+2^17&amp;quot;,0,null],[&amp;quot;+2^18&amp;quot;,0,null],[&amp;quot;+2^19&amp;quot;,0,null],[&amp;quot;+2^20&amp;quot;,0,null],[&amp;quot;+2^21&amp;quot;,0,null],[&amp;quot;+2^22&amp;quot;,0,null],[&amp;quot;+2^23&amp;quot;,0,null],[&amp;quot;+2^24&amp;quot;,0,null],[&amp;quot;+2^25&amp;quot;,0,null],[&amp;quot;+2^26&amp;quot;,0,null],[&amp;quot;+2^27&amp;quot;,0,null],[&amp;quot;+2^28&amp;quot;,0,null],[&amp;quot;+2^29&amp;quot;,0,null],[&amp;quot;+2^30&amp;quot;,0,null],[&amp;quot;+2^31&amp;quot;,0,null],[&amp;quot;+2^32&amp;quot;,0,null],[&amp;quot;+2^33&amp;quot;,0,null],[&amp;quot;+2^34&amp;quot;,0,null],[&amp;quot;+2^35&amp;quot;,0,null],[&amp;quot;+2^36&amp;quot;,0,null],[&amp;quot;+2^37&amp;quot;,0,null],[&amp;quot;+2^38&amp;quot;,0,null],[&amp;quot;+2^39&amp;quot;,0,null],[&amp;quot;+2^40&amp;quot;,0,null],[&amp;quot;+2^41&amp;quot;,0,null],[&amp;quot;+2^42&amp;quot;,0,null],[&amp;quot;+2^43&amp;quot;,0,null],[&amp;quot;+2^44&amp;quot;,0,null],[&amp;quot;+2^45&amp;quot;,0,null],[&amp;quot;+2^46&amp;quot;,0,null],[&amp;quot;+2^47&amp;quot;,0,null],[&amp;quot;+2^48&amp;quot;,0,null],[&amp;quot;+2^49&amp;quot;,0,null],[&amp;quot;+2^50&amp;quot;,0,null],[&amp;quot;+2^51&amp;quot;,0,null],[&amp;quot;+2^52&amp;quot;,0,null],[&amp;quot;+2^53&amp;quot;,0,null],[&amp;quot;+2^54&amp;quot;,0,null],[&amp;quot;+2^55&amp;quot;,0,null],[&amp;quot;+2^56&amp;quot;,0,null],[&amp;quot;+2^57&amp;quot;,0,null],[&amp;quot;+2^58&amp;quot;,0,null],[&amp;quot;+2^59&amp;quot;,0,null],[&amp;quot;+2^60&amp;quot;,0,null],[&amp;quot;+2^61&amp;quot;,0,null],[&amp;quot;+2^62&amp;quot;,0,null],[&amp;quot;+2^63&amp;quot;,0,null],[&amp;quot;+2^64&amp;quot;,0,null],[&amp;quot;+2^65&amp;quot;,0,null],[&amp;quot;+2^66&amp;quot;,0,null],[&amp;quot;+2^67&amp;quot;,0,null],[&amp;quot;+2^68&amp;quot;,0,null],[&amp;quot;+2^69&amp;quot;,0,null],[&amp;quot;+2^70&amp;quot;,0,null],[&amp;quot;+2^71&amp;quot;,0,null],[&amp;quot;+2^72&amp;quot;,0,null],[&amp;quot;+2^73&amp;quot;,0,null],[&amp;quot;+2^74&amp;quot;,0,null],[&amp;quot;+2^75&amp;quot;,0,null],[&amp;quot;+2^76&amp;quot;,0,null],[&amp;quot;+2^77&amp;quot;,0,null],[&amp;quot;+2^78&amp;quot;,0,null],[&amp;quot;+2^79&amp;quot;,0,null],[&amp;quot;+2^80&amp;quot;,0,null],[&amp;quot;+2^81&amp;quot;,0,null],[&amp;quot;+2^82&amp;quot;,0,null],[&amp;quot;+2^83&amp;quot;,0,null],[&amp;quot;+2^84&amp;quot;,0,null],[&amp;quot;+2^85&amp;quot;,0,null],[&amp;quot;+2^86&amp;quot;,0,null],[&amp;quot;+2^87&amp;quot;,0,null],[&amp;quot;+2^88&amp;quot;,0,null],[&amp;quot;+2^89&amp;quot;,0,null],[&amp;quot;+2^90&amp;quot;,0,null],[&amp;quot;+2^91&amp;quot;,0,null],[&amp;quot;+2^92&amp;quot;,0,null],[&amp;quot;+2^93&amp;quot;,0,null],[&amp;quot;+2^94&amp;quot;,0,null],[&amp;quot;+2^95&amp;quot;,0,null],[&amp;quot;+2^96&amp;quot;,0,null],[&amp;quot;+2^97&amp;quot;,0,null],[&amp;quot;+2^98&amp;quot;,0,null],[&amp;quot;+2^99&amp;quot;,0,null],[&amp;quot;+2^100&amp;quot;,0,null],[&amp;quot;+2^101&amp;quot;,0,null],[&amp;quot;+2^102&amp;quot;,0,null],[&amp;quot;+2^103&amp;quot;,0,null],[&amp;quot;+2^104&amp;quot;,0,null],[&amp;quot;+2^105&amp;quot;,0,null],[&amp;quot;+2^106&amp;quot;,0,null],[&amp;quot;+2^107&amp;quot;,0,null],[&amp;quot;+2^108&amp;quot;,0,null],[&amp;quot;+2^109&amp;quot;,0,null],[&amp;quot;+2^110&amp;quot;,0,null],[&amp;quot;+2^111&amp;quot;,0,null],[&amp;quot;+2^112&amp;quot;,0,null],[&amp;quot;+2^113&amp;quot;,0,null],[&amp;quot;+2^114&amp;quot;,0,null],[&amp;quot;+2^115&amp;quot;,0,null],[&amp;quot;+2^116&amp;quot;,0,null],[&amp;quot;+2^117&amp;quot;,0,null],[&amp;quot;+2^118&amp;quot;,0,null],[&amp;quot;+2^119&amp;quot;,0,null],[&amp;quot;+2^120&amp;quot;,0,null],[&amp;quot;+2^121&amp;quot;,0,null],[&amp;quot;+2^122&amp;quot;,0,null],[&amp;quot;+2^123&amp;quot;,0,null],[&amp;quot;+2^124&amp;quot;,0,null],[&amp;quot;+2^125&amp;quot;,0,null],[&amp;quot;+2^126&amp;quot;,0,null],[&amp;quot;+2^127&amp;quot;,0,null] ]&lt;br /&gt;
 }}&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Key observation:&#039;&#039;&#039;  &lt;br /&gt;
For &amp;lt;math&amp;gt;|x| \ll 1&amp;lt;/math&amp;gt;, the ULP error grows rapidly and reaches extremely large values due to loss of significance. &lt;br /&gt;
For sufficiently large &amp;lt;math&amp;gt;x&amp;lt;/math&amp;gt;, both numerator and denominator overflow, leading to NaN results.&lt;br /&gt;
&lt;br /&gt;
=== expm1 ===&lt;br /&gt;
&lt;br /&gt;
This graph visualizes the ULP error of the &amp;lt;code&amp;gt;expm1&amp;lt;/code&amp;gt;-based formulation.&lt;br /&gt;
Using &amp;lt;code&amp;gt;expm1&amp;lt;/code&amp;gt; significantly improves numerical accuracy for small input values by avoiding catastrophic cancellation in the expression &amp;lt;math&amp;gt;e^{2x} - 1&amp;lt;/math&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
{{#widget:FloatErrorGraph|id=tanhf_expm1&lt;br /&gt;
  |data=[ [&amp;quot;-2^127&amp;quot;,0,1.17549e-38],[&amp;quot;-2^126&amp;quot;,0,1.17549e-38],[&amp;quot;-2^125&amp;quot;,0,1.17549e-38],[&amp;quot;-2^124&amp;quot;,0,1.17549e-38],[&amp;quot;-2^123&amp;quot;,0,1.17549e-38],[&amp;quot;-2^122&amp;quot;,0,1.17549e-38],[&amp;quot;-2^121&amp;quot;,0,1.17549e-38],[&amp;quot;-2^120&amp;quot;,0,1.17549e-38],[&amp;quot;-2^119&amp;quot;,0,1.17549e-38],[&amp;quot;-2^118&amp;quot;,0,1.17549e-38],[&amp;quot;-2^117&amp;quot;,0,1.17549e-38],[&amp;quot;-2^116&amp;quot;,0,1.17549e-38],[&amp;quot;-2^115&amp;quot;,0,1.17549e-38],[&amp;quot;-2^114&amp;quot;,0,1.17549e-38],[&amp;quot;-2^113&amp;quot;,0,1.17549e-38],[&amp;quot;-2^112&amp;quot;,0,1.17549e-38],[&amp;quot;-2^111&amp;quot;,0,1.17549e-38],[&amp;quot;-2^110&amp;quot;,0,1.17549e-38],[&amp;quot;-2^109&amp;quot;,0,1.17549e-38],[&amp;quot;-2^108&amp;quot;,0,1.17549e-38],[&amp;quot;-2^107&amp;quot;,0,1.17549e-38],[&amp;quot;-2^106&amp;quot;,0,1.17549e-38],[&amp;quot;-2^105&amp;quot;,0,1.17549e-38],[&amp;quot;-2^104&amp;quot;,0,1.17549e-38],[&amp;quot;-2^103&amp;quot;,0,1.17549e-38],[&amp;quot;-2^102&amp;quot;,0,1.17549e-38],[&amp;quot;-2^101&amp;quot;,0,1.17549e-38],[&amp;quot;-2^100&amp;quot;,0,1.17549e-38],[&amp;quot;-2^99&amp;quot;,0,1.17549e-38],[&amp;quot;-2^98&amp;quot;,0,1.17549e-38],[&amp;quot;-2^97&amp;quot;,0,1.17549e-38],[&amp;quot;-2^96&amp;quot;,0,1.17549e-38],[&amp;quot;-2^95&amp;quot;,0,1.17549e-38],[&amp;quot;-2^94&amp;quot;,0,1.17549e-38],[&amp;quot;-2^93&amp;quot;,0,1.17549e-38],[&amp;quot;-2^92&amp;quot;,0,1.17549e-38],[&amp;quot;-2^91&amp;quot;,0,1.17549e-38],[&amp;quot;-2^90&amp;quot;,0,1.17549e-38],[&amp;quot;-2^89&amp;quot;,0,1.17549e-38],[&amp;quot;-2^88&amp;quot;,0,1.17549e-38],[&amp;quot;-2^87&amp;quot;,0,1.17549e-38],[&amp;quot;-2^86&amp;quot;,0,1.17549e-38],[&amp;quot;-2^85&amp;quot;,0,1.17549e-38],[&amp;quot;-2^84&amp;quot;,0,1.17549e-38],[&amp;quot;-2^83&amp;quot;,0,1.17549e-38],[&amp;quot;-2^82&amp;quot;,0,1.17549e-38],[&amp;quot;-2^81&amp;quot;,0,1.17549e-38],[&amp;quot;-2^80&amp;quot;,0,1.17549e-38],[&amp;quot;-2^79&amp;quot;,0,1.17549e-38],[&amp;quot;-2^78&amp;quot;,0,1.17549e-38],[&amp;quot;-2^77&amp;quot;,0,1.17549e-38],[&amp;quot;-2^76&amp;quot;,0,1.17549e-38],[&amp;quot;-2^75&amp;quot;,0,1.17549e-38],[&amp;quot;-2^74&amp;quot;,0,1.17549e-38],[&amp;quot;-2^73&amp;quot;,0,1.17549e-38],[&amp;quot;-2^72&amp;quot;,0,1.17549e-38],[&amp;quot;-2^71&amp;quot;,0,1.17549e-38],[&amp;quot;-2^70&amp;quot;,0,1.17549e-38],[&amp;quot;-2^69&amp;quot;,0,1.17549e-38],[&amp;quot;-2^68&amp;quot;,0,1.17549e-38],[&amp;quot;-2^67&amp;quot;,0,1.17549e-38],[&amp;quot;-2^66&amp;quot;,0,1.17549e-38],[&amp;quot;-2^65&amp;quot;,0,1.17549e-38],[&amp;quot;-2^64&amp;quot;,0,1.17549e-38],[&amp;quot;-2^63&amp;quot;,0,1.17549e-38],[&amp;quot;-2^62&amp;quot;,0,1.17549e-38],[&amp;quot;-2^61&amp;quot;,0,1.17549e-38],[&amp;quot;-2^60&amp;quot;,0,1.17549e-38],[&amp;quot;-2^59&amp;quot;,0,1.17549e-38],[&amp;quot;-2^58&amp;quot;,0,1.17549e-38],[&amp;quot;-2^57&amp;quot;,0,1.17549e-38],[&amp;quot;-2^56&amp;quot;,0,1.17549e-38],[&amp;quot;-2^55&amp;quot;,0,1.17549e-38],[&amp;quot;-2^54&amp;quot;,0,1.17549e-38],[&amp;quot;-2^53&amp;quot;,0,1.17549e-38],[&amp;quot;-2^52&amp;quot;,0,1.17549e-38],[&amp;quot;-2^51&amp;quot;,0,1.17549e-38],[&amp;quot;-2^50&amp;quot;,0,1.17549e-38],[&amp;quot;-2^49&amp;quot;,0,1.17549e-38],[&amp;quot;-2^48&amp;quot;,0,1.17549e-38],[&amp;quot;-2^47&amp;quot;,0,1.17549e-38],[&amp;quot;-2^46&amp;quot;,0,1.17549e-38],[&amp;quot;-2^45&amp;quot;,0,1.17549e-38],[&amp;quot;-2^44&amp;quot;,0,1.17549e-38],[&amp;quot;-2^43&amp;quot;,0,1.17549e-38],[&amp;quot;-2^42&amp;quot;,0,1.17549e-38],[&amp;quot;-2^41&amp;quot;,0,1.17549e-38],[&amp;quot;-2^40&amp;quot;,0,1.17549e-38],[&amp;quot;-2^39&amp;quot;,0,1.17549e-38],[&amp;quot;-2^38&amp;quot;,0,1.17549e-38],[&amp;quot;-2^37&amp;quot;,0,1.17549e-38],[&amp;quot;-2^36&amp;quot;,0,1.17549e-38],[&amp;quot;-2^35&amp;quot;,0,1.17549e-38],[&amp;quot;-2^34&amp;quot;,0,1.17549e-38],[&amp;quot;-2^33&amp;quot;,0,1.17549e-38],[&amp;quot;-2^32&amp;quot;,0,1.17549e-38],[&amp;quot;-2^31&amp;quot;,0,1.17549e-38],[&amp;quot;-2^30&amp;quot;,0,1.17549e-38],[&amp;quot;-2^29&amp;quot;,0,1.17549e-38],[&amp;quot;-2^28&amp;quot;,0,1.17549e-38],[&amp;quot;-2^27&amp;quot;,0,1.17549e-38],[&amp;quot;-2^26&amp;quot;,0,1.17549e-38],[&amp;quot;-2^25&amp;quot;,0,1.17549e-38],[&amp;quot;-2^24&amp;quot;,0,1.17549e-38],[&amp;quot;-2^23&amp;quot;,0,1.17549e-38],[&amp;quot;-2^22&amp;quot;,0,1.17549e-38],[&amp;quot;-2^21&amp;quot;,0,1.17549e-38],[&amp;quot;-2^20&amp;quot;,0,1.17549e-38],[&amp;quot;-2^19&amp;quot;,0,1.17549e-38],[&amp;quot;-2^18&amp;quot;,0,1.17549e-38],[&amp;quot;-2^17&amp;quot;,0,1.17549e-38],[&amp;quot;-2^16&amp;quot;,0,1.17549e-38],[&amp;quot;-2^15&amp;quot;,0,1.17549e-38],[&amp;quot;-2^14&amp;quot;,0,1.17549e-38],[&amp;quot;-2^13&amp;quot;,0,1.17549e-38],[&amp;quot;-2^12&amp;quot;,0,1.17549e-38],[&amp;quot;-2^11&amp;quot;,0,1.17549e-38],[&amp;quot;-2^10&amp;quot;,0,1.17549e-38],[&amp;quot;-2^9&amp;quot;,0,1.17549e-38],[&amp;quot;-2^8&amp;quot;,0,1.17549e-38],[&amp;quot;-2^7&amp;quot;,0,1.17549e-38],[&amp;quot;-2^6&amp;quot;,0,1.17549e-38],[&amp;quot;-2^5&amp;quot;,0,1.17549e-38],[&amp;quot;-2^4&amp;quot;,2.53131e-14,4.24683e-07],[&amp;quot;-2^3&amp;quot;,1.19138e-07,1.9988],[&amp;quot;-2^2&amp;quot;,1.46791e-07,2.46274],[&amp;quot;-2^1&amp;quot;,1.47723e-07,2.47839],[&amp;quot;-2^0&amp;quot;,1.40758e-07,2.36153],[&amp;quot;-2^-1&amp;quot;,1.13422e-07,2.23445],[&amp;quot;-2^-2&amp;quot;,6.38012e-08,2.14081],[&amp;quot;-2^-3&amp;quot;,2.69538e-08,1.80884],[&amp;quot;-2^-4&amp;quot;,1.21308e-08,1.62817],[&amp;quot;-2^-5&amp;quot;,5.59535e-09,1.50199],[&amp;quot;-2^-6&amp;quot;,2.69177e-09,1.44513],[&amp;quot;-2^-7&amp;quot;,1.31289e-09,1.4097],[&amp;quot;-2^-8&amp;quot;,6.49688e-10,1.3952],[&amp;quot;-2^-9&amp;quot;,3.32043e-10,1.42611],[&amp;quot;-2^-10&amp;quot;,1.66275e-10,1.4283],[&amp;quot;-2^-11&amp;quot;,8.08595e-11,1.38916],[&amp;quot;-2^-12&amp;quot;,3.8939e-11,1.33794],[&amp;quot;-2^-13&amp;quot;,1.9401e-11,1.33323],[&amp;quot;-2^-14&amp;quot;,7.88212e-12,1.08331],[&amp;quot;-2^-15&amp;quot;,3.71372e-12,1.02082],[&amp;quot;-2^-16&amp;quot;,1.82845e-12,1.0052],[&amp;quot;-2^-17&amp;quot;,9.10676e-13,1.0013],[&amp;quot;-2^-18&amp;quot;,4.54895e-13,1.00032],[&amp;quot;-2^-19&amp;quot;,2.27392e-13,1.00008],[&amp;quot;-2^-20&amp;quot;,1.13689e-13,1.00002],[&amp;quot;-2^-21&amp;quot;,5.68437e-14,1],[&amp;quot;-2^-22&amp;quot;,2.84217e-14,1],[&amp;quot;-2^-23&amp;quot;,1.42109e-14,1],[&amp;quot;-2^-24&amp;quot;,7.10543e-15,1],[&amp;quot;-2^-25&amp;quot;,3.55271e-15,1],[&amp;quot;-2^-26&amp;quot;,9.92617e-24,5.58794e-09],[&amp;quot;-2^-27&amp;quot;,1.65436e-24,1.86265e-09],[&amp;quot;-2^-28&amp;quot;,8.27181e-25,1.86265e-09],[&amp;quot;-2^-29&amp;quot;,4.1359e-25,1.86265e-09],[&amp;quot;-2^-30&amp;quot;,2.06795e-25,1.86265e-09],[&amp;quot;-2^-31&amp;quot;,1.03398e-25,1.86265e-09],[&amp;quot;-2^-32&amp;quot;,5.16988e-26,1.86265e-09],[&amp;quot;-2^-33&amp;quot;,2.58494e-26,1.86265e-09],[&amp;quot;-2^-34&amp;quot;,1.29247e-26,1.86265e-09],[&amp;quot;-2^-35&amp;quot;,6.46235e-27,1.86265e-09],[&amp;quot;-2^-36&amp;quot;,3.23117e-27,1.86265e-09],[&amp;quot;-2^-37&amp;quot;,1.61559e-27,1.86265e-09],[&amp;quot;-2^-38&amp;quot;,8.07794e-28,1.86265e-09],[&amp;quot;-2^-39&amp;quot;,4.03897e-28,1.86265e-09],[&amp;quot;-2^-40&amp;quot;,2.01948e-28,1.86265e-09],[&amp;quot;-2^-41&amp;quot;,1.00974e-28,1.86265e-09],[&amp;quot;-2^-42&amp;quot;,5.04871e-29,1.86265e-09],[&amp;quot;-2^-43&amp;quot;,2.52435e-29,1.86265e-09],[&amp;quot;-2^-44&amp;quot;,1.26218e-29,1.86265e-09],[&amp;quot;-2^-45&amp;quot;,6.31089e-30,1.86265e-09],[&amp;quot;-2^-46&amp;quot;,3.15544e-30,1.86265e-09],[&amp;quot;-2^-47&amp;quot;,1.57772e-30,1.86265e-09],[&amp;quot;-2^-48&amp;quot;,7.88861e-31,1.86265e-09],[&amp;quot;-2^-49&amp;quot;,3.9443e-31,1.86265e-09],[&amp;quot;-2^-50&amp;quot;,1.97215e-31,1.86265e-09],[&amp;quot;-2^-51&amp;quot;,9.86076e-32,1.86265e-09],[&amp;quot;-2^-52&amp;quot;,4.93038e-32,1.86265e-09],[&amp;quot;-2^-53&amp;quot;,2.46519e-32,1.86265e-09],[&amp;quot;-2^-54&amp;quot;,1.2326e-32,1.86265e-09],[&amp;quot;-2^-55&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-56&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-57&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-58&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-59&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-60&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-61&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-62&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-63&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-64&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-65&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-66&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-67&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-68&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-69&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-70&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-71&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-72&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-73&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-74&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-75&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-76&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-77&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-78&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-79&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-80&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-81&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-82&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-83&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-84&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-85&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-86&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-87&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-88&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-89&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-90&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-91&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-92&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-93&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-94&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-95&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-96&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-97&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-98&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-99&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-100&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-101&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-102&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-103&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-104&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-105&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-106&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-107&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-108&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-109&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-110&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-111&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-112&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-113&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-114&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-115&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-116&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-117&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-118&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-119&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-120&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-121&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-122&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-123&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-124&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-125&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-126&amp;quot;,0,1.17549e-38],[&amp;quot;-SN&amp;quot;,0,1.17549e-38],[&amp;quot;0&amp;quot;,0,1.17549e-38],[&amp;quot;+SN&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-126&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-125&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-124&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-123&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-122&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-121&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-120&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-119&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-118&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-117&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-116&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-115&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-114&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-113&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-112&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-111&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-110&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-109&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-108&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-107&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-106&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-105&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-104&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-103&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-102&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-101&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-100&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-99&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-98&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-97&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-96&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-95&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-94&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-93&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-92&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-91&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-90&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-89&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-88&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-87&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-86&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-85&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-84&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-83&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-82&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-81&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-80&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-79&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-78&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-77&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-76&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-75&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-74&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-73&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-72&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-71&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-70&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-69&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-68&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-67&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-66&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-65&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-64&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-63&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-62&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-61&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-60&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-59&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-58&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-57&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-56&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-55&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-54&amp;quot;,1.2326e-32,1.86265e-09],[&amp;quot;+2^-53&amp;quot;,2.46519e-32,1.86265e-09],[&amp;quot;+2^-52&amp;quot;,4.93038e-32,1.86265e-09],[&amp;quot;+2^-51&amp;quot;,9.86076e-32,1.86265e-09],[&amp;quot;+2^-50&amp;quot;,1.97215e-31,1.86265e-09],[&amp;quot;+2^-49&amp;quot;,3.9443e-31,1.86265e-09],[&amp;quot;+2^-48&amp;quot;,7.88861e-31,1.86265e-09],[&amp;quot;+2^-47&amp;quot;,1.57772e-30,1.86265e-09],[&amp;quot;+2^-46&amp;quot;,3.15544e-30,1.86265e-09],[&amp;quot;+2^-45&amp;quot;,6.31089e-30,1.86265e-09],[&amp;quot;+2^-44&amp;quot;,1.26218e-29,1.86265e-09],[&amp;quot;+2^-43&amp;quot;,2.52435e-29,1.86265e-09],[&amp;quot;+2^-42&amp;quot;,5.04871e-29,1.86265e-09],[&amp;quot;+2^-41&amp;quot;,1.00974e-28,1.86265e-09],[&amp;quot;+2^-40&amp;quot;,2.01948e-28,1.86265e-09],[&amp;quot;+2^-39&amp;quot;,4.03897e-28,1.86265e-09],[&amp;quot;+2^-38&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-37&amp;quot;,1.61559e-27,1.86265e-09],[&amp;quot;+2^-36&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-35&amp;quot;,6.46235e-27,1.86265e-09],[&amp;quot;+2^-34&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-33&amp;quot;,2.58494e-26,1.86265e-09],[&amp;quot;+2^-32&amp;quot;,5.16988e-26,1.86265e-09],[&amp;quot;+2^-31&amp;quot;,1.03398e-25,1.86265e-09],[&amp;quot;+2^-30&amp;quot;,2.06795e-25,1.86265e-09],[&amp;quot;+2^-29&amp;quot;,4.1359e-25,1.86265e-09],[&amp;quot;+2^-28&amp;quot;,8.27181e-25,1.86265e-09],[&amp;quot;+2^-27&amp;quot;,1.65436e-24,1.86265e-09],[&amp;quot;+2^-26&amp;quot;,9.92617e-24,5.58794e-09],[&amp;quot;+2^-25&amp;quot;,3.55271e-15,1],[&amp;quot;+2^-24&amp;quot;,7.10543e-15,1],[&amp;quot;+2^-23&amp;quot;,1.42109e-14,1],[&amp;quot;+2^-22&amp;quot;,2.84217e-14,1],[&amp;quot;+2^-21&amp;quot;,5.68437e-14,1],[&amp;quot;+2^-20&amp;quot;,1.13689e-13,1.00002],[&amp;quot;+2^-19&amp;quot;,2.27391e-13,1.00008],[&amp;quot;+2^-18&amp;quot;,4.54892e-13,1.00032],[&amp;quot;+2^-17&amp;quot;,9.10665e-13,1.00129],[&amp;quot;+2^-16&amp;quot;,1.82841e-12,1.00518],[&amp;quot;+2^-15&amp;quot;,3.71353e-12,1.02077],[&amp;quot;+2^-14&amp;quot;,1.39798e-11,1.92136],[&amp;quot;+2^-13&amp;quot;,2.63239e-11,1.80896],[&amp;quot;+2^-12&amp;quot;,5.37712e-11,1.84757],[&amp;quot;+2^-11&amp;quot;,1.09932e-10,1.88861],[&amp;quot;+2^-10&amp;quot;,2.24787e-10,1.93091],[&amp;quot;+2^-9&amp;quot;,4.3452e-10,1.86625],[&amp;quot;+2^-8&amp;quot;,1.01783e-09,2.18576],[&amp;quot;+2^-7&amp;quot;,2.10562e-09,2.26089],[&amp;quot;+2^-6&amp;quot;,4.03954e-09,2.16871],[&amp;quot;+2^-5&amp;quot;,8.15746e-09,2.18975],[&amp;quot;+2^-4&amp;quot;,1.5545e-08,2.08641],[&amp;quot;+2^-3&amp;quot;,2.62153e-08,2.05864],[&amp;quot;+2^-2&amp;quot;,4.09295e-08,1.73804],[&amp;quot;+2^-1&amp;quot;,8.02409e-08,1.34622],[&amp;quot;+2^0&amp;quot;,8.66201e-08,1.45324],[&amp;quot;+2^1&amp;quot;,8.8654e-08,1.48737],[&amp;quot;+2^2&amp;quot;,6.7976e-08,1.14045],[&amp;quot;+2^3&amp;quot;,7.44117e-08,1.24842],[&amp;quot;+2^4&amp;quot;,2.53131e-14,4.24683e-07],[&amp;quot;+2^5&amp;quot;,0,null],[&amp;quot;+2^6&amp;quot;,0,null],[&amp;quot;+2^7&amp;quot;,0,null],[&amp;quot;+2^8&amp;quot;,0,null],[&amp;quot;+2^9&amp;quot;,0,null],[&amp;quot;+2^10&amp;quot;,0,null],[&amp;quot;+2^11&amp;quot;,0,null],[&amp;quot;+2^12&amp;quot;,0,null],[&amp;quot;+2^13&amp;quot;,0,null],[&amp;quot;+2^14&amp;quot;,0,null],[&amp;quot;+2^15&amp;quot;,0,null],[&amp;quot;+2^16&amp;quot;,0,null],[&amp;quot;+2^17&amp;quot;,0,null],[&amp;quot;+2^18&amp;quot;,0,null],[&amp;quot;+2^19&amp;quot;,0,null],[&amp;quot;+2^20&amp;quot;,0,null],[&amp;quot;+2^21&amp;quot;,0,null],[&amp;quot;+2^22&amp;quot;,0,null],[&amp;quot;+2^23&amp;quot;,0,null],[&amp;quot;+2^24&amp;quot;,0,null],[&amp;quot;+2^25&amp;quot;,0,null],[&amp;quot;+2^26&amp;quot;,0,null],[&amp;quot;+2^27&amp;quot;,0,null],[&amp;quot;+2^28&amp;quot;,0,null],[&amp;quot;+2^29&amp;quot;,0,null],[&amp;quot;+2^30&amp;quot;,0,null],[&amp;quot;+2^31&amp;quot;,0,null],[&amp;quot;+2^32&amp;quot;,0,null],[&amp;quot;+2^33&amp;quot;,0,null],[&amp;quot;+2^34&amp;quot;,0,null],[&amp;quot;+2^35&amp;quot;,0,null],[&amp;quot;+2^36&amp;quot;,0,null],[&amp;quot;+2^37&amp;quot;,0,null],[&amp;quot;+2^38&amp;quot;,0,null],[&amp;quot;+2^39&amp;quot;,0,null],[&amp;quot;+2^40&amp;quot;,0,null],[&amp;quot;+2^41&amp;quot;,0,null],[&amp;quot;+2^42&amp;quot;,0,null],[&amp;quot;+2^43&amp;quot;,0,null],[&amp;quot;+2^44&amp;quot;,0,null],[&amp;quot;+2^45&amp;quot;,0,null],[&amp;quot;+2^46&amp;quot;,0,null],[&amp;quot;+2^47&amp;quot;,0,null],[&amp;quot;+2^48&amp;quot;,0,null],[&amp;quot;+2^49&amp;quot;,0,null],[&amp;quot;+2^50&amp;quot;,0,null],[&amp;quot;+2^51&amp;quot;,0,null],[&amp;quot;+2^52&amp;quot;,0,null],[&amp;quot;+2^53&amp;quot;,0,null],[&amp;quot;+2^54&amp;quot;,0,null],[&amp;quot;+2^55&amp;quot;,0,null],[&amp;quot;+2^56&amp;quot;,0,null],[&amp;quot;+2^57&amp;quot;,0,null],[&amp;quot;+2^58&amp;quot;,0,null],[&amp;quot;+2^59&amp;quot;,0,null],[&amp;quot;+2^60&amp;quot;,0,null],[&amp;quot;+2^61&amp;quot;,0,null],[&amp;quot;+2^62&amp;quot;,0,null],[&amp;quot;+2^63&amp;quot;,0,null],[&amp;quot;+2^64&amp;quot;,0,null],[&amp;quot;+2^65&amp;quot;,0,null],[&amp;quot;+2^66&amp;quot;,0,null],[&amp;quot;+2^67&amp;quot;,0,null],[&amp;quot;+2^68&amp;quot;,0,null],[&amp;quot;+2^69&amp;quot;,0,null],[&amp;quot;+2^70&amp;quot;,0,null],[&amp;quot;+2^71&amp;quot;,0,null],[&amp;quot;+2^72&amp;quot;,0,null],[&amp;quot;+2^73&amp;quot;,0,null],[&amp;quot;+2^74&amp;quot;,0,null],[&amp;quot;+2^75&amp;quot;,0,null],[&amp;quot;+2^76&amp;quot;,0,null],[&amp;quot;+2^77&amp;quot;,0,null],[&amp;quot;+2^78&amp;quot;,0,null],[&amp;quot;+2^79&amp;quot;,0,null],[&amp;quot;+2^80&amp;quot;,0,null],[&amp;quot;+2^81&amp;quot;,0,null],[&amp;quot;+2^82&amp;quot;,0,null],[&amp;quot;+2^83&amp;quot;,0,null],[&amp;quot;+2^84&amp;quot;,0,null],[&amp;quot;+2^85&amp;quot;,0,null],[&amp;quot;+2^86&amp;quot;,0,null],[&amp;quot;+2^87&amp;quot;,0,null],[&amp;quot;+2^88&amp;quot;,0,null],[&amp;quot;+2^89&amp;quot;,0,null],[&amp;quot;+2^90&amp;quot;,0,null],[&amp;quot;+2^91&amp;quot;,0,null],[&amp;quot;+2^92&amp;quot;,0,null],[&amp;quot;+2^93&amp;quot;,0,null],[&amp;quot;+2^94&amp;quot;,0,null],[&amp;quot;+2^95&amp;quot;,0,null],[&amp;quot;+2^96&amp;quot;,0,null],[&amp;quot;+2^97&amp;quot;,0,null],[&amp;quot;+2^98&amp;quot;,0,null],[&amp;quot;+2^99&amp;quot;,0,null],[&amp;quot;+2^100&amp;quot;,0,null],[&amp;quot;+2^101&amp;quot;,0,null],[&amp;quot;+2^102&amp;quot;,0,null],[&amp;quot;+2^103&amp;quot;,0,null],[&amp;quot;+2^104&amp;quot;,0,null],[&amp;quot;+2^105&amp;quot;,0,null],[&amp;quot;+2^106&amp;quot;,0,null],[&amp;quot;+2^107&amp;quot;,0,null],[&amp;quot;+2^108&amp;quot;,0,null],[&amp;quot;+2^109&amp;quot;,0,null],[&amp;quot;+2^110&amp;quot;,0,null],[&amp;quot;+2^111&amp;quot;,0,null],[&amp;quot;+2^112&amp;quot;,0,null],[&amp;quot;+2^113&amp;quot;,0,null],[&amp;quot;+2^114&amp;quot;,0,null],[&amp;quot;+2^115&amp;quot;,0,null],[&amp;quot;+2^116&amp;quot;,0,null],[&amp;quot;+2^117&amp;quot;,0,null],[&amp;quot;+2^118&amp;quot;,0,null],[&amp;quot;+2^119&amp;quot;,0,null],[&amp;quot;+2^120&amp;quot;,0,null],[&amp;quot;+2^121&amp;quot;,0,null],[&amp;quot;+2^122&amp;quot;,0,null],[&amp;quot;+2^123&amp;quot;,0,null],[&amp;quot;+2^124&amp;quot;,0,null],[&amp;quot;+2^125&amp;quot;,0,null],[&amp;quot;+2^126&amp;quot;,0,null],[&amp;quot;+2^127&amp;quot;,0,null] ]&lt;br /&gt;
 }}&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Key observation:&#039;&#039;&#039;  &lt;br /&gt;
Compared to direct exponential formulations, the &amp;lt;code&amp;gt;expm1&amp;lt;/code&amp;gt; approach maintains low ULP error for small and medium values of &amp;lt;math&amp;gt;x&amp;lt;/math&amp;gt;. &lt;br /&gt;
Overflow is still present for very large inputs, but the overall error behavior is substantially more stable.&lt;br /&gt;
&lt;br /&gt;
== Numerical Issues and Error Metrics ==&lt;br /&gt;
&lt;br /&gt;
To evaluate and compare different software implementations of &amp;lt;code&amp;gt;tanh&amp;lt;/code&amp;gt;, several numerical error metrics are used throughout this article. These metrics make it possible to reason about accuracy, robustness, and failure modes in floating-point arithmetic.&lt;br /&gt;
&lt;br /&gt;
=== ULP Error ===&lt;br /&gt;
ULP (Unit in the Last Place) measures the distance between two adjacent representable floating-point numbers. The ULP error of a computed result is defined as the difference, measured in ULPs, between the computed value and the correctly rounded reference value.&lt;br /&gt;
&lt;br /&gt;
An error of &amp;lt;code&amp;gt;ULP ≤ 2&amp;lt;/code&amp;gt; is commonly considered acceptable for single-precision transcendental functions, as it indicates that the result is very close to the correctly rounded value.&lt;br /&gt;
&lt;br /&gt;
Large ULP errors typically indicate catastrophic cancellation, loss of significant bits, or severe rounding effects.&lt;br /&gt;
&lt;br /&gt;
=== NaN and Overflow ===&lt;br /&gt;
NaN (Not a Number) results occur when undefined floating-point operations are performed, such as &amp;lt;code&amp;gt;inf / inf&amp;lt;/code&amp;gt;. In &amp;lt;code&amp;gt;tanh&amp;lt;/code&amp;gt; implementations, this often happens when both the numerator and denominator of a formulation overflow to infinity.&lt;br /&gt;
&lt;br /&gt;
Tracking NaN generation is critical, as NaNs can silently propagate through numerical pipelines and invalidate downstream results.&lt;br /&gt;
&lt;br /&gt;
=== Error Graphs ===&lt;br /&gt;
The error graphs shown in the following sections visualize ULP error across the full &amp;lt;code&amp;gt;float32&amp;lt;/code&amp;gt; input domain. They make it possible to identify:&lt;br /&gt;
* regions with catastrophic cancellation,&lt;br /&gt;
* input ranges that trigger overflow or NaNs,&lt;br /&gt;
* and numerical stability differences between formulations.&lt;br /&gt;
&lt;br /&gt;
These graphs are a key tool for understanding not only the maximum error, but also how errors are distributed across different input magnitudes.&lt;br /&gt;
&lt;br /&gt;
== Taylor Expansion (Local Approximation) ==&lt;br /&gt;
&lt;br /&gt;
Taylor series approximations of &amp;lt;code&amp;gt;tanh(x)&amp;lt;/code&amp;gt; expand the function around &amp;lt;math&amp;gt;x = 0&amp;lt;/math&amp;gt; and therefore provide a very accurate approximation only for sufficiently small input magnitudes.&lt;br /&gt;
&lt;br /&gt;
For small &amp;lt;math&amp;gt;|x|&amp;lt;/math&amp;gt;, the hyperbolic tangent behaves approximately linear (&amp;lt;math&amp;gt;\tanh(x) \approx x&amp;lt;/math&amp;gt;), and low-order Taylor polynomials can achieve excellent accuracy with minimal computational cost. This makes Taylor-based approximations attractive as a fast path in range-reduced implementations.&lt;br /&gt;
&lt;br /&gt;
However, Taylor expansions have a strictly limited radius of convergence. Outside a narrow neighborhood around zero, the approximation error grows extremely rapidly and quickly becomes unusable. As a result, Taylor polynomials are not suitable as standalone implementations of &amp;lt;code&amp;gt;tanh&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
{{#widget:FloatErrorGraph|id=taylor&lt;br /&gt;
  |data=[ [&amp;quot;-2^127&amp;quot;,0,0],[&amp;quot;-2^126&amp;quot;,0,0],[&amp;quot;-2^125&amp;quot;,0,0],[&amp;quot;-2^124&amp;quot;,0,0],[&amp;quot;-2^123&amp;quot;,0,0],[&amp;quot;-2^122&amp;quot;,0,0],[&amp;quot;-2^121&amp;quot;,0,0],[&amp;quot;-2^120&amp;quot;,0,0],[&amp;quot;-2^119&amp;quot;,0,0],[&amp;quot;-2^118&amp;quot;,0,0],[&amp;quot;-2^117&amp;quot;,0,0],[&amp;quot;-2^116&amp;quot;,0,0],[&amp;quot;-2^115&amp;quot;,0,0],[&amp;quot;-2^114&amp;quot;,0,0],[&amp;quot;-2^113&amp;quot;,0,0],[&amp;quot;-2^112&amp;quot;,0,0],[&amp;quot;-2^111&amp;quot;,0,0],[&amp;quot;-2^110&amp;quot;,0,0],[&amp;quot;-2^109&amp;quot;,0,0],[&amp;quot;-2^108&amp;quot;,0,0],[&amp;quot;-2^107&amp;quot;,0,0],[&amp;quot;-2^106&amp;quot;,0,0],[&amp;quot;-2^105&amp;quot;,0,0],[&amp;quot;-2^104&amp;quot;,0,0],[&amp;quot;-2^103&amp;quot;,0,0],[&amp;quot;-2^102&amp;quot;,0,0],[&amp;quot;-2^101&amp;quot;,0,0],[&amp;quot;-2^100&amp;quot;,0,0],[&amp;quot;-2^99&amp;quot;,0,0],[&amp;quot;-2^98&amp;quot;,0,0],[&amp;quot;-2^97&amp;quot;,0,0],[&amp;quot;-2^96&amp;quot;,0,0],[&amp;quot;-2^95&amp;quot;,0,0],[&amp;quot;-2^94&amp;quot;,0,0],[&amp;quot;-2^93&amp;quot;,0,0],[&amp;quot;-2^92&amp;quot;,0,0],[&amp;quot;-2^91&amp;quot;,0,0],[&amp;quot;-2^90&amp;quot;,0,0],[&amp;quot;-2^89&amp;quot;,0,0],[&amp;quot;-2^88&amp;quot;,0,0],[&amp;quot;-2^87&amp;quot;,0,0],[&amp;quot;-2^86&amp;quot;,0,0],[&amp;quot;-2^85&amp;quot;,0,0],[&amp;quot;-2^84&amp;quot;,0,0],[&amp;quot;-2^83&amp;quot;,0,0],[&amp;quot;-2^82&amp;quot;,0,0],[&amp;quot;-2^81&amp;quot;,0,0],[&amp;quot;-2^80&amp;quot;,0,0],[&amp;quot;-2^79&amp;quot;,0,0],[&amp;quot;-2^78&amp;quot;,0,0],[&amp;quot;-2^77&amp;quot;,0,0],[&amp;quot;-2^76&amp;quot;,0,0],[&amp;quot;-2^75&amp;quot;,0,0],[&amp;quot;-2^74&amp;quot;,0,0],[&amp;quot;-2^73&amp;quot;,0,0],[&amp;quot;-2^72&amp;quot;,0,0],[&amp;quot;-2^71&amp;quot;,0,0],[&amp;quot;-2^70&amp;quot;,0,0],[&amp;quot;-2^69&amp;quot;,0,0],[&amp;quot;-2^68&amp;quot;,0,0],[&amp;quot;-2^67&amp;quot;,0,0],[&amp;quot;-2^66&amp;quot;,0,0],[&amp;quot;-2^65&amp;quot;,0,0],[&amp;quot;-2^64&amp;quot;,0,0],[&amp;quot;-2^63&amp;quot;,1.84467e+19,3.09485e+26],[&amp;quot;-2^62&amp;quot;,9.22336e+18,1.54742e+26],[&amp;quot;-2^61&amp;quot;,4.61168e+18,7.73712e+25],[&amp;quot;-2^60&amp;quot;,2.30584e+18,3.86856e+25],[&amp;quot;-2^59&amp;quot;,1.15292e+18,1.93428e+25],[&amp;quot;-2^58&amp;quot;,5.7646e+17,9.6714e+24],[&amp;quot;-2^57&amp;quot;,2.8823e+17,4.8357e+24],[&amp;quot;-2^56&amp;quot;,1.44115e+17,2.41785e+24],[&amp;quot;-2^55&amp;quot;,7.20575e+16,1.20892e+24],[&amp;quot;-2^54&amp;quot;,3.60288e+16,6.04462e+23],[&amp;quot;-2^53&amp;quot;,1.80144e+16,3.02231e+23],[&amp;quot;-2^52&amp;quot;,9.00719e+15,1.51116e+23],[&amp;quot;-2^51&amp;quot;,4.5036e+15,7.55578e+22],[&amp;quot;-2^50&amp;quot;,2.2518e+15,3.77789e+22],[&amp;quot;-2^49&amp;quot;,1.1259e+15,1.88894e+22],[&amp;quot;-2^48&amp;quot;,5.62949e+14,9.44472e+21],[&amp;quot;-2^47&amp;quot;,2.81475e+14,4.72236e+21],[&amp;quot;-2^46&amp;quot;,1.40737e+14,2.36118e+21],[&amp;quot;-2^45&amp;quot;,7.03687e+13,1.18059e+21],[&amp;quot;-2^44&amp;quot;,3.51843e+13,5.90295e+20],[&amp;quot;-2^43&amp;quot;,1.75922e+13,2.95148e+20],[&amp;quot;-2^42&amp;quot;,8.79608e+12,1.47574e+20],[&amp;quot;-2^41&amp;quot;,4.39804e+12,7.37869e+19],[&amp;quot;-2^40&amp;quot;,2.19902e+12,3.68935e+19],[&amp;quot;-2^39&amp;quot;,1.09951e+12,1.84467e+19],[&amp;quot;-2^38&amp;quot;,5.49755e+11,9.22336e+18],[&amp;quot;-2^37&amp;quot;,2.74878e+11,4.61168e+18],[&amp;quot;-2^36&amp;quot;,1.37439e+11,2.30584e+18],[&amp;quot;-2^35&amp;quot;,6.87194e+10,1.15292e+18],[&amp;quot;-2^34&amp;quot;,3.43597e+10,5.7646e+17],[&amp;quot;-2^33&amp;quot;,1.71799e+10,2.8823e+17],[&amp;quot;-2^32&amp;quot;,8.58993e+09,1.44115e+17],[&amp;quot;-2^31&amp;quot;,4.29085e+09,7.19885e+16],[&amp;quot;-2^30&amp;quot;,2.14542e+09,3.59943e+16],[&amp;quot;-2^29&amp;quot;,1.07271e+09,1.79971e+16],[&amp;quot;-2^28&amp;quot;,5.36356e+08,8.99856e+15],[&amp;quot;-2^27&amp;quot;,2.68178e+08,4.49928e+15],[&amp;quot;-2^26&amp;quot;,1.34089e+08,2.24964e+15],[&amp;quot;-2^25&amp;quot;,6.70445e+07,1.12482e+15],[&amp;quot;-2^24&amp;quot;,3.35223e+07,5.6241e+14],[&amp;quot;-2^23&amp;quot;,1.67611e+07,2.81205e+14],[&amp;quot;-2^22&amp;quot;,8.38056e+06,1.40603e+14],[&amp;quot;-2^21&amp;quot;,4.19028e+06,7.03013e+13],[&amp;quot;-2^20&amp;quot;,2.09514e+06,3.51506e+13],[&amp;quot;-2^19&amp;quot;,1.04757e+06,1.75753e+13],[&amp;quot;-2^18&amp;quot;,523784,8.78764e+12],[&amp;quot;-2^17&amp;quot;,261892,4.39381e+12],[&amp;quot;-2^16&amp;quot;,130945,2.1969e+12],[&amp;quot;-2^15&amp;quot;,65472.2,1.09844e+12],[&amp;quot;-2^14&amp;quot;,32735.6,5.49212e+11],[&amp;quot;-2^13&amp;quot;,16367.3,2.74598e+11],[&amp;quot;-2^12&amp;quot;,8183.15,1.3729e+11],[&amp;quot;-2^11&amp;quot;,4091.07,6.86368e+10],[&amp;quot;-2^10&amp;quot;,2045.04,3.431e+10],[&amp;quot;-2^9&amp;quot;,1022.02,1.71466e+10],[&amp;quot;-2^8&amp;quot;,510.509,8.56492e+09],[&amp;quot;-2^7&amp;quot;,254.754,4.27406e+09],[&amp;quot;-2^6&amp;quot;,126.876,2.12862e+09],[&amp;quot;-2^5&amp;quot;,62.9354,1.05588e+09],[&amp;quot;-2^4&amp;quot;,30.9629,5.19471e+08],[&amp;quot;-2^3&amp;quot;,14.9718,2.51185e+08],[&amp;quot;-2^2&amp;quot;,6.96672,1.16882e+08],[&amp;quot;-2^1&amp;quot;,2.94633,4.94312e+07],[&amp;quot;-2^0&amp;quot;,0.940715,1.57826e+07],[&amp;quot;-2^-1&amp;quot;,0.127635,2.14135e+06],[&amp;quot;-2^-2&amp;quot;,0.00318735,106950],[&amp;quot;-2^-3&amp;quot;,3.11475e-05,2090.28],[&amp;quot;-2^-4&amp;quot;,2.57737e-07,34.5929],[&amp;quot;-2^-5&amp;quot;,3.89213e-09,1.04478],[&amp;quot;-2^-6&amp;quot;,9.51472e-10,0.510818],[&amp;quot;-2^-7&amp;quot;,4.66402e-10,0.500795],[&amp;quot;-2^-8&amp;quot;,2.32917e-10,0.500185],[&amp;quot;-2^-9&amp;quot;,1.16423e-10,0.500033],[&amp;quot;-2^-10&amp;quot;,5.82082e-11,0.500004],[&amp;quot;-2^-11&amp;quot;,2.91037e-11,0.499998],[&amp;quot;-2^-12&amp;quot;,1.45519e-11,0.5],[&amp;quot;-2^-13&amp;quot;,4.85062e-12,0.333332],[&amp;quot;-2^-14&amp;quot;,6.06328e-13,0.0833331],[&amp;quot;-2^-15&amp;quot;,7.5791e-14,0.0208333],[&amp;quot;-2^-16&amp;quot;,9.47387e-15,0.00520832],[&amp;quot;-2^-17&amp;quot;,1.18424e-15,0.00130208],[&amp;quot;-2^-18&amp;quot;,1.48029e-16,0.00032552],[&amp;quot;-2^-19&amp;quot;,1.85039e-17,8.13808e-05],[&amp;quot;-2^-20&amp;quot;,2.31304e-18,2.03457e-05],[&amp;quot;-2^-21&amp;quot;,2.89156e-19,5.08688e-06],[&amp;quot;-2^-22&amp;quot;,3.61577e-20,1.27219e-06],[&amp;quot;-2^-23&amp;quot;,4.52633e-21,3.18512e-07],[&amp;quot;-2^-24&amp;quot;,5.691e-22,8.00937e-08],[&amp;quot;-2^-25&amp;quot;,7.27919e-23,2.04891e-08],[&amp;quot;-2^-26&amp;quot;,9.92617e-24,5.58794e-09],[&amp;quot;-2^-27&amp;quot;,1.65436e-24,1.86265e-09],[&amp;quot;-2^-28&amp;quot;,8.27181e-25,1.86265e-09],[&amp;quot;-2^-29&amp;quot;,4.1359e-25,1.86265e-09],[&amp;quot;-2^-30&amp;quot;,2.06795e-25,1.86265e-09],[&amp;quot;-2^-31&amp;quot;,1.03398e-25,1.86265e-09],[&amp;quot;-2^-32&amp;quot;,5.16988e-26,1.86265e-09],[&amp;quot;-2^-33&amp;quot;,2.58494e-26,1.86265e-09],[&amp;quot;-2^-34&amp;quot;,1.29247e-26,1.86265e-09],[&amp;quot;-2^-35&amp;quot;,6.46235e-27,1.86265e-09],[&amp;quot;-2^-36&amp;quot;,3.23117e-27,1.86265e-09],[&amp;quot;-2^-37&amp;quot;,1.61559e-27,1.86265e-09],[&amp;quot;-2^-38&amp;quot;,8.07794e-28,1.86265e-09],[&amp;quot;-2^-39&amp;quot;,4.03897e-28,1.86265e-09],[&amp;quot;-2^-40&amp;quot;,2.01948e-28,1.86265e-09],[&amp;quot;-2^-41&amp;quot;,1.00974e-28,1.86265e-09],[&amp;quot;-2^-42&amp;quot;,5.04871e-29,1.86265e-09],[&amp;quot;-2^-43&amp;quot;,2.52435e-29,1.86265e-09],[&amp;quot;-2^-44&amp;quot;,1.26218e-29,1.86265e-09],[&amp;quot;-2^-45&amp;quot;,6.31089e-30,1.86265e-09],[&amp;quot;-2^-46&amp;quot;,3.15544e-30,1.86265e-09],[&amp;quot;-2^-47&amp;quot;,1.57772e-30,1.86265e-09],[&amp;quot;-2^-48&amp;quot;,7.88861e-31,1.86265e-09],[&amp;quot;-2^-49&amp;quot;,3.9443e-31,1.86265e-09],[&amp;quot;-2^-50&amp;quot;,1.97215e-31,1.86265e-09],[&amp;quot;-2^-51&amp;quot;,9.86076e-32,1.86265e-09],[&amp;quot;-2^-52&amp;quot;,4.93038e-32,1.86265e-09],[&amp;quot;-2^-53&amp;quot;,2.46519e-32,1.86265e-09],[&amp;quot;-2^-54&amp;quot;,1.2326e-32,1.86265e-09],[&amp;quot;-2^-55&amp;quot;,0,0],[&amp;quot;-2^-56&amp;quot;,0,0],[&amp;quot;-2^-57&amp;quot;,0,0],[&amp;quot;-2^-58&amp;quot;,0,0],[&amp;quot;-2^-59&amp;quot;,0,0],[&amp;quot;-2^-60&amp;quot;,0,0],[&amp;quot;-2^-61&amp;quot;,0,0],[&amp;quot;-2^-62&amp;quot;,0,0],[&amp;quot;-2^-63&amp;quot;,0,0],[&amp;quot;-2^-64&amp;quot;,0,0],[&amp;quot;-2^-65&amp;quot;,0,0],[&amp;quot;-2^-66&amp;quot;,0,0],[&amp;quot;-2^-67&amp;quot;,0,0],[&amp;quot;-2^-68&amp;quot;,0,0],[&amp;quot;-2^-69&amp;quot;,0,0],[&amp;quot;-2^-70&amp;quot;,0,0],[&amp;quot;-2^-71&amp;quot;,0,0],[&amp;quot;-2^-72&amp;quot;,0,0],[&amp;quot;-2^-73&amp;quot;,0,0],[&amp;quot;-2^-74&amp;quot;,0,0],[&amp;quot;-2^-75&amp;quot;,0,0],[&amp;quot;-2^-76&amp;quot;,0,0],[&amp;quot;-2^-77&amp;quot;,0,0],[&amp;quot;-2^-78&amp;quot;,0,0],[&amp;quot;-2^-79&amp;quot;,0,0],[&amp;quot;-2^-80&amp;quot;,0,0],[&amp;quot;-2^-81&amp;quot;,0,0],[&amp;quot;-2^-82&amp;quot;,0,0],[&amp;quot;-2^-83&amp;quot;,0,0],[&amp;quot;-2^-84&amp;quot;,0,0],[&amp;quot;-2^-85&amp;quot;,0,0],[&amp;quot;-2^-86&amp;quot;,0,0],[&amp;quot;-2^-87&amp;quot;,0,0],[&amp;quot;-2^-88&amp;quot;,0,0],[&amp;quot;-2^-89&amp;quot;,0,0],[&amp;quot;-2^-90&amp;quot;,0,0],[&amp;quot;-2^-91&amp;quot;,0,0],[&amp;quot;-2^-92&amp;quot;,0,0],[&amp;quot;-2^-93&amp;quot;,0,0],[&amp;quot;-2^-94&amp;quot;,0,0],[&amp;quot;-2^-95&amp;quot;,0,0],[&amp;quot;-2^-96&amp;quot;,0,0],[&amp;quot;-2^-97&amp;quot;,0,0],[&amp;quot;-2^-98&amp;quot;,0,0],[&amp;quot;-2^-99&amp;quot;,0,0],[&amp;quot;-2^-100&amp;quot;,0,0],[&amp;quot;-2^-101&amp;quot;,0,0],[&amp;quot;-2^-102&amp;quot;,0,0],[&amp;quot;-2^-103&amp;quot;,0,0],[&amp;quot;-2^-104&amp;quot;,0,0],[&amp;quot;-2^-105&amp;quot;,0,0],[&amp;quot;-2^-106&amp;quot;,0,0],[&amp;quot;-2^-107&amp;quot;,0,0],[&amp;quot;-2^-108&amp;quot;,0,0],[&amp;quot;-2^-109&amp;quot;,0,0],[&amp;quot;-2^-110&amp;quot;,0,0],[&amp;quot;-2^-111&amp;quot;,0,0],[&amp;quot;-2^-112&amp;quot;,0,0],[&amp;quot;-2^-113&amp;quot;,0,0],[&amp;quot;-2^-114&amp;quot;,0,0],[&amp;quot;-2^-115&amp;quot;,0,0],[&amp;quot;-2^-116&amp;quot;,0,0],[&amp;quot;-2^-117&amp;quot;,0,0],[&amp;quot;-2^-118&amp;quot;,0,0],[&amp;quot;-2^-119&amp;quot;,0,0],[&amp;quot;-2^-120&amp;quot;,0,0],[&amp;quot;-2^-121&amp;quot;,0,0],[&amp;quot;-2^-122&amp;quot;,0,0],[&amp;quot;-2^-123&amp;quot;,0,0],[&amp;quot;-2^-124&amp;quot;,0,0],[&amp;quot;-2^-125&amp;quot;,0,0],[&amp;quot;-2^-126&amp;quot;,0,0],[&amp;quot;-SN&amp;quot;,0,0],[&amp;quot;0&amp;quot;,0,0],[&amp;quot;+SN&amp;quot;,0,0],[&amp;quot;+2^-126&amp;quot;,0,0],[&amp;quot;+2^-125&amp;quot;,0,0],[&amp;quot;+2^-124&amp;quot;,0,0],[&amp;quot;+2^-123&amp;quot;,0,0],[&amp;quot;+2^-122&amp;quot;,0,0],[&amp;quot;+2^-121&amp;quot;,0,0],[&amp;quot;+2^-120&amp;quot;,0,0],[&amp;quot;+2^-119&amp;quot;,0,0],[&amp;quot;+2^-118&amp;quot;,0,0],[&amp;quot;+2^-117&amp;quot;,0,0],[&amp;quot;+2^-116&amp;quot;,0,0],[&amp;quot;+2^-115&amp;quot;,0,0],[&amp;quot;+2^-114&amp;quot;,0,0],[&amp;quot;+2^-113&amp;quot;,0,0],[&amp;quot;+2^-112&amp;quot;,0,0],[&amp;quot;+2^-111&amp;quot;,0,0],[&amp;quot;+2^-110&amp;quot;,0,0],[&amp;quot;+2^-109&amp;quot;,0,0],[&amp;quot;+2^-108&amp;quot;,0,0],[&amp;quot;+2^-107&amp;quot;,0,0],[&amp;quot;+2^-106&amp;quot;,0,0],[&amp;quot;+2^-105&amp;quot;,0,0],[&amp;quot;+2^-104&amp;quot;,0,0],[&amp;quot;+2^-103&amp;quot;,0,0],[&amp;quot;+2^-102&amp;quot;,0,0],[&amp;quot;+2^-101&amp;quot;,0,0],[&amp;quot;+2^-100&amp;quot;,0,0],[&amp;quot;+2^-99&amp;quot;,0,0],[&amp;quot;+2^-98&amp;quot;,0,0],[&amp;quot;+2^-97&amp;quot;,0,0],[&amp;quot;+2^-96&amp;quot;,0,0],[&amp;quot;+2^-95&amp;quot;,0,0],[&amp;quot;+2^-94&amp;quot;,0,0],[&amp;quot;+2^-93&amp;quot;,0,0],[&amp;quot;+2^-92&amp;quot;,0,0],[&amp;quot;+2^-91&amp;quot;,0,0],[&amp;quot;+2^-90&amp;quot;,0,0],[&amp;quot;+2^-89&amp;quot;,0,0],[&amp;quot;+2^-88&amp;quot;,0,0],[&amp;quot;+2^-87&amp;quot;,0,0],[&amp;quot;+2^-86&amp;quot;,0,0],[&amp;quot;+2^-85&amp;quot;,0,0],[&amp;quot;+2^-84&amp;quot;,0,0],[&amp;quot;+2^-83&amp;quot;,0,0],[&amp;quot;+2^-82&amp;quot;,0,0],[&amp;quot;+2^-81&amp;quot;,0,0],[&amp;quot;+2^-80&amp;quot;,0,0],[&amp;quot;+2^-79&amp;quot;,0,0],[&amp;quot;+2^-78&amp;quot;,0,0],[&amp;quot;+2^-77&amp;quot;,0,0],[&amp;quot;+2^-76&amp;quot;,0,0],[&amp;quot;+2^-75&amp;quot;,0,0],[&amp;quot;+2^-74&amp;quot;,0,0],[&amp;quot;+2^-73&amp;quot;,0,0],[&amp;quot;+2^-72&amp;quot;,0,0],[&amp;quot;+2^-71&amp;quot;,0,0],[&amp;quot;+2^-70&amp;quot;,0,0],[&amp;quot;+2^-69&amp;quot;,0,0],[&amp;quot;+2^-68&amp;quot;,0,0],[&amp;quot;+2^-67&amp;quot;,0,0],[&amp;quot;+2^-66&amp;quot;,0,0],[&amp;quot;+2^-65&amp;quot;,0,0],[&amp;quot;+2^-64&amp;quot;,0,0],[&amp;quot;+2^-63&amp;quot;,0,0],[&amp;quot;+2^-62&amp;quot;,0,0],[&amp;quot;+2^-61&amp;quot;,0,0],[&amp;quot;+2^-60&amp;quot;,0,0],[&amp;quot;+2^-59&amp;quot;,0,0],[&amp;quot;+2^-58&amp;quot;,0,0],[&amp;quot;+2^-57&amp;quot;,0,0],[&amp;quot;+2^-56&amp;quot;,0,0],[&amp;quot;+2^-55&amp;quot;,0,0],[&amp;quot;+2^-54&amp;quot;,1.2326e-32,1.86265e-09],[&amp;quot;+2^-53&amp;quot;,2.46519e-32,1.86265e-09],[&amp;quot;+2^-52&amp;quot;,4.93038e-32,1.86265e-09],[&amp;quot;+2^-51&amp;quot;,9.86076e-32,1.86265e-09],[&amp;quot;+2^-50&amp;quot;,1.97215e-31,1.86265e-09],[&amp;quot;+2^-49&amp;quot;,3.9443e-31,1.86265e-09],[&amp;quot;+2^-48&amp;quot;,7.88861e-31,1.86265e-09],[&amp;quot;+2^-47&amp;quot;,1.57772e-30,1.86265e-09],[&amp;quot;+2^-46&amp;quot;,3.15544e-30,1.86265e-09],[&amp;quot;+2^-45&amp;quot;,6.31089e-30,1.86265e-09],[&amp;quot;+2^-44&amp;quot;,1.26218e-29,1.86265e-09],[&amp;quot;+2^-43&amp;quot;,2.52435e-29,1.86265e-09],[&amp;quot;+2^-42&amp;quot;,5.04871e-29,1.86265e-09],[&amp;quot;+2^-41&amp;quot;,1.00974e-28,1.86265e-09],[&amp;quot;+2^-40&amp;quot;,2.01948e-28,1.86265e-09],[&amp;quot;+2^-39&amp;quot;,4.03897e-28,1.86265e-09],[&amp;quot;+2^-38&amp;quot;,8.07794e-28,1.86265e-09],[&amp;quot;+2^-37&amp;quot;,1.61559e-27,1.86265e-09],[&amp;quot;+2^-36&amp;quot;,3.23117e-27,1.86265e-09],[&amp;quot;+2^-35&amp;quot;,6.46235e-27,1.86265e-09],[&amp;quot;+2^-34&amp;quot;,1.29247e-26,1.86265e-09],[&amp;quot;+2^-33&amp;quot;,2.58494e-26,1.86265e-09],[&amp;quot;+2^-32&amp;quot;,5.16988e-26,1.86265e-09],[&amp;quot;+2^-31&amp;quot;,1.03398e-25,1.86265e-09],[&amp;quot;+2^-30&amp;quot;,2.06795e-25,1.86265e-09],[&amp;quot;+2^-29&amp;quot;,4.1359e-25,1.86265e-09],[&amp;quot;+2^-28&amp;quot;,8.27181e-25,1.86265e-09],[&amp;quot;+2^-27&amp;quot;,1.65436e-24,1.86265e-09],[&amp;quot;+2^-26&amp;quot;,9.92617e-24,5.58794e-09],[&amp;quot;+2^-25&amp;quot;,7.27919e-23,2.04891e-08],[&amp;quot;+2^-24&amp;quot;,5.691e-22,8.00937e-08],[&amp;quot;+2^-23&amp;quot;,4.52633e-21,3.18512e-07],[&amp;quot;+2^-22&amp;quot;,3.61577e-20,1.27219e-06],[&amp;quot;+2^-21&amp;quot;,2.89156e-19,5.08688e-06],[&amp;quot;+2^-20&amp;quot;,2.31304e-18,2.03457e-05],[&amp;quot;+2^-19&amp;quot;,1.85039e-17,8.13808e-05],[&amp;quot;+2^-18&amp;quot;,1.48029e-16,0.00032552],[&amp;quot;+2^-17&amp;quot;,1.18424e-15,0.00130208],[&amp;quot;+2^-16&amp;quot;,9.47387e-15,0.00520832],[&amp;quot;+2^-15&amp;quot;,7.5791e-14,0.0208333],[&amp;quot;+2^-14&amp;quot;,6.06328e-13,0.0833331],[&amp;quot;+2^-13&amp;quot;,4.85062e-12,0.333332],[&amp;quot;+2^-12&amp;quot;,1.45519e-11,0.5],[&amp;quot;+2^-11&amp;quot;,2.91037e-11,0.499998],[&amp;quot;+2^-10&amp;quot;,5.82082e-11,0.500004],[&amp;quot;+2^-9&amp;quot;,1.16423e-10,0.500033],[&amp;quot;+2^-8&amp;quot;,2.32917e-10,0.500185],[&amp;quot;+2^-7&amp;quot;,4.66402e-10,0.500795],[&amp;quot;+2^-6&amp;quot;,9.51472e-10,0.510818],[&amp;quot;+2^-5&amp;quot;,3.89213e-09,1.04478],[&amp;quot;+2^-4&amp;quot;,2.57737e-07,34.5929],[&amp;quot;+2^-3&amp;quot;,3.11475e-05,2090.28],[&amp;quot;+2^-2&amp;quot;,0.00318735,106950],[&amp;quot;+2^-1&amp;quot;,0.127635,2.14135e+06],[&amp;quot;+2^0&amp;quot;,0.940715,1.57826e+07],[&amp;quot;+2^1&amp;quot;,2.94633,4.94312e+07],[&amp;quot;+2^2&amp;quot;,6.96672,1.16882e+08],[&amp;quot;+2^3&amp;quot;,14.9718,1.33874e+08],[&amp;quot;+2^4&amp;quot;,30.9629,2.59736e+08],[&amp;quot;+2^5&amp;quot;,62.9354,5.27941e+08],[&amp;quot;+2^6&amp;quot;,126.876,1.06431e+09],[&amp;quot;+2^7&amp;quot;,254.754,2.13703e+09],[&amp;quot;+2^8&amp;quot;,510.509,4.28246e+09],[&amp;quot;+2^9&amp;quot;,1022.02,8.57331e+09],[&amp;quot;+2^10&amp;quot;,2045.04,1.7155e+10],[&amp;quot;+2^11&amp;quot;,4091.07,3.43184e+10],[&amp;quot;+2^12&amp;quot;,8183.15,6.86452e+10],[&amp;quot;+2^13&amp;quot;,16367.3,1.37299e+11],[&amp;quot;+2^14&amp;quot;,32735.6,2.74606e+11],[&amp;quot;+2^15&amp;quot;,65472.2,5.4922e+11],[&amp;quot;+2^16&amp;quot;,130945,1.09845e+12],[&amp;quot;+2^17&amp;quot;,261892,2.19691e+12],[&amp;quot;+2^18&amp;quot;,523784,4.39382e+12],[&amp;quot;+2^19&amp;quot;,1.04757e+06,8.78765e+12],[&amp;quot;+2^20&amp;quot;,2.09514e+06,1.75753e+13],[&amp;quot;+2^21&amp;quot;,4.19028e+06,3.51506e+13],[&amp;quot;+2^22&amp;quot;,8.38056e+06,7.03013e+13],[&amp;quot;+2^23&amp;quot;,1.67611e+07,1.40603e+14],[&amp;quot;+2^24&amp;quot;,3.35223e+07,2.81205e+14],[&amp;quot;+2^25&amp;quot;,6.70445e+07,5.6241e+14],[&amp;quot;+2^26&amp;quot;,1.34089e+08,1.12482e+15],[&amp;quot;+2^27&amp;quot;,2.68178e+08,2.24964e+15],[&amp;quot;+2^28&amp;quot;,5.36356e+08,4.49928e+15],[&amp;quot;+2^29&amp;quot;,1.07271e+09,8.99856e+15],[&amp;quot;+2^30&amp;quot;,2.14542e+09,1.79971e+16],[&amp;quot;+2^31&amp;quot;,4.29085e+09,3.59943e+16],[&amp;quot;+2^32&amp;quot;,8.58993e+09,7.20575e+16],[&amp;quot;+2^33&amp;quot;,1.71799e+10,1.44115e+17],[&amp;quot;+2^34&amp;quot;,3.43597e+10,2.8823e+17],[&amp;quot;+2^35&amp;quot;,6.87194e+10,5.7646e+17],[&amp;quot;+2^36&amp;quot;,1.37439e+11,1.15292e+18],[&amp;quot;+2^37&amp;quot;,2.74878e+11,2.30584e+18],[&amp;quot;+2^38&amp;quot;,5.49755e+11,4.61168e+18],[&amp;quot;+2^39&amp;quot;,1.09951e+12,9.22336e+18],[&amp;quot;+2^40&amp;quot;,2.19902e+12,1.84467e+19],[&amp;quot;+2^41&amp;quot;,4.39804e+12,3.68935e+19],[&amp;quot;+2^42&amp;quot;,8.79608e+12,7.37869e+19],[&amp;quot;+2^43&amp;quot;,1.75922e+13,1.47574e+20],[&amp;quot;+2^44&amp;quot;,3.51843e+13,2.95148e+20],[&amp;quot;+2^45&amp;quot;,7.03687e+13,5.90295e+20],[&amp;quot;+2^46&amp;quot;,1.40737e+14,1.18059e+21],[&amp;quot;+2^47&amp;quot;,2.81475e+14,2.36118e+21],[&amp;quot;+2^48&amp;quot;,5.62949e+14,4.72236e+21],[&amp;quot;+2^49&amp;quot;,1.1259e+15,9.44472e+21],[&amp;quot;+2^50&amp;quot;,2.2518e+15,1.88894e+22],[&amp;quot;+2^51&amp;quot;,4.5036e+15,3.77789e+22],[&amp;quot;+2^52&amp;quot;,9.00719e+15,7.55578e+22],[&amp;quot;+2^53&amp;quot;,1.80144e+16,1.51116e+23],[&amp;quot;+2^54&amp;quot;,3.60288e+16,3.02231e+23],[&amp;quot;+2^55&amp;quot;,7.20575e+16,6.04462e+23],[&amp;quot;+2^56&amp;quot;,1.44115e+17,1.20892e+24],[&amp;quot;+2^57&amp;quot;,2.8823e+17,2.41785e+24],[&amp;quot;+2^58&amp;quot;,5.7646e+17,4.8357e+24],[&amp;quot;+2^59&amp;quot;,1.15292e+18,9.6714e+24],[&amp;quot;+2^60&amp;quot;,2.30584e+18,1.93428e+25],[&amp;quot;+2^61&amp;quot;,4.61168e+18,3.86856e+25],[&amp;quot;+2^62&amp;quot;,9.22336e+18,7.73712e+25],[&amp;quot;+2^63&amp;quot;,1.84467e+19,1.54742e+26],[&amp;quot;+2^64&amp;quot;,0,0],[&amp;quot;+2^65&amp;quot;,0,0],[&amp;quot;+2^66&amp;quot;,0,0],[&amp;quot;+2^67&amp;quot;,0,0],[&amp;quot;+2^68&amp;quot;,0,0],[&amp;quot;+2^69&amp;quot;,0,0],[&amp;quot;+2^70&amp;quot;,0,0],[&amp;quot;+2^71&amp;quot;,0,0],[&amp;quot;+2^72&amp;quot;,0,0],[&amp;quot;+2^73&amp;quot;,0,0],[&amp;quot;+2^74&amp;quot;,0,0],[&amp;quot;+2^75&amp;quot;,0,0],[&amp;quot;+2^76&amp;quot;,0,0],[&amp;quot;+2^77&amp;quot;,0,0],[&amp;quot;+2^78&amp;quot;,0,0],[&amp;quot;+2^79&amp;quot;,0,0],[&amp;quot;+2^80&amp;quot;,0,0],[&amp;quot;+2^81&amp;quot;,0,0],[&amp;quot;+2^82&amp;quot;,0,0],[&amp;quot;+2^83&amp;quot;,0,0],[&amp;quot;+2^84&amp;quot;,0,0],[&amp;quot;+2^85&amp;quot;,0,0],[&amp;quot;+2^86&amp;quot;,0,0],[&amp;quot;+2^87&amp;quot;,0,0],[&amp;quot;+2^88&amp;quot;,0,0],[&amp;quot;+2^89&amp;quot;,0,0],[&amp;quot;+2^90&amp;quot;,0,0],[&amp;quot;+2^91&amp;quot;,0,0],[&amp;quot;+2^92&amp;quot;,0,0],[&amp;quot;+2^93&amp;quot;,0,0],[&amp;quot;+2^94&amp;quot;,0,0],[&amp;quot;+2^95&amp;quot;,0,0],[&amp;quot;+2^96&amp;quot;,0,0],[&amp;quot;+2^97&amp;quot;,0,0],[&amp;quot;+2^98&amp;quot;,0,0],[&amp;quot;+2^99&amp;quot;,0,0],[&amp;quot;+2^100&amp;quot;,0,0],[&amp;quot;+2^101&amp;quot;,0,0],[&amp;quot;+2^102&amp;quot;,0,0],[&amp;quot;+2^103&amp;quot;,0,0],[&amp;quot;+2^104&amp;quot;,0,0],[&amp;quot;+2^105&amp;quot;,0,0],[&amp;quot;+2^106&amp;quot;,0,0],[&amp;quot;+2^107&amp;quot;,0,0],[&amp;quot;+2^108&amp;quot;,0,0],[&amp;quot;+2^109&amp;quot;,0,0],[&amp;quot;+2^110&amp;quot;,0,0],[&amp;quot;+2^111&amp;quot;,0,0],[&amp;quot;+2^112&amp;quot;,0,0],[&amp;quot;+2^113&amp;quot;,0,0],[&amp;quot;+2^114&amp;quot;,0,0],[&amp;quot;+2^115&amp;quot;,0,0],[&amp;quot;+2^116&amp;quot;,0,0],[&amp;quot;+2^117&amp;quot;,0,0],[&amp;quot;+2^118&amp;quot;,0,0],[&amp;quot;+2^119&amp;quot;,0,0],[&amp;quot;+2^120&amp;quot;,0,0],[&amp;quot;+2^121&amp;quot;,0,0],[&amp;quot;+2^122&amp;quot;,0,0],[&amp;quot;+2^123&amp;quot;,0,0],[&amp;quot;+2^124&amp;quot;,0,0],[&amp;quot;+2^125&amp;quot;,0,0],[&amp;quot;+2^126&amp;quot;,0,0],[&amp;quot;+2^127&amp;quot;,0,0] ]&lt;br /&gt;
 }}&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Key observation:&#039;&#039;&#039;  &lt;br /&gt;
The Taylor approximation is highly accurate near zero but diverges extremely fast outside its convergence region.  &lt;br /&gt;
This makes it unsuitable as a standalone &amp;lt;code&amp;gt;tanh&amp;lt;/code&amp;gt; implementation, but very effective as a local approximation when combined with range-based dispatching.&lt;br /&gt;
&lt;br /&gt;
== Localized Approximation Functions ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
!Id&lt;br /&gt;
!Formula&lt;br /&gt;
!ULP &amp;lt;= 2.0&lt;br /&gt;
!NAN&lt;br /&gt;
|-&lt;br /&gt;
|signum&lt;br /&gt;
|&amp;lt;math&amp;gt;\sgn{x}&amp;lt;/math&amp;gt;&lt;br /&gt;
| -inf &amp;lt; x &amp;lt; -8.31763&lt;br /&gt;
8.31763 &amp;lt; x &amp;lt; inf&lt;br /&gt;
| -&lt;br /&gt;
|-&lt;br /&gt;
|poly1&lt;br /&gt;
|&amp;lt;math&amp;gt;x&amp;lt;/math&amp;gt;&lt;br /&gt;
| -0.000704229 &amp;lt; x &amp;lt; 0.000704229&lt;br /&gt;
| -&lt;br /&gt;
|-&lt;br /&gt;
|poly3&lt;br /&gt;
|&amp;lt;math&amp;gt;x - \frac{1}{3} x^3&amp;lt;/math&amp;gt;&lt;br /&gt;
| -0.0291781 &amp;lt; x &amp;lt; 0.0291781 &lt;br /&gt;
| -&lt;br /&gt;
|-&lt;br /&gt;
|lampert7&lt;br /&gt;
|&amp;lt;math&amp;gt;\frac{x \cdot (135135 + x^2 \cdot (17325 + x^2 \cdot (378 + x^2)))}{135135 + x^2 \cdot (62370 + x^2 \cdot (3150 + 28 \cdot x^2))}&lt;br /&gt;
&amp;lt;/math&amp;gt;&lt;br /&gt;
| -0.000947416 &amp;lt; x &amp;lt; 0.000947207&lt;br /&gt;
| -inf &amp;lt; x &amp;lt; -1.51629e+06&lt;br /&gt;
1.51629e+06 &amp;lt; x &amp;lt; inf&lt;br /&gt;
|-&lt;br /&gt;
|pade&lt;br /&gt;
|&amp;lt;math&amp;gt;x + x \cdot \frac{x^2 \cdot \left( p_1 \cdot x^2 + p_0 \right)}{\left( x^2 + q_1 \right) \cdot x^2 + q_0} &lt;br /&gt;
&lt;br /&gt;
&amp;lt;/math&amp;gt; &amp;lt;math&amp;gt;\begin{align}&lt;br /&gt;
p_0 &amp;amp;= -0.2059432032 \\&lt;br /&gt;
p_1 &amp;amp;= -0.0009577527 \\&lt;br /&gt;
q_0 &amp;amp;= 0.6178299136 \\ &lt;br /&gt;
q_1 &amp;amp;= 0.25&lt;br /&gt;
\end{align}&lt;br /&gt;
&amp;lt;/math&amp;gt;&lt;br /&gt;
| -0.0016973 &amp;lt; x &amp;lt; 0.0016973 &lt;br /&gt;
| -inf &amp;lt; x &amp;lt; -6.14896e+18&lt;br /&gt;
6.14896e+18 &amp;lt; x &amp;lt; inf&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== External Links ==&lt;br /&gt;
&lt;br /&gt;
* https://yaikhom.com/2020-04-28-localised-approximation-of-hyperbolic-tangents.html#mjx-eqn-eqn7-th%20degree%20lambert%20approximant&lt;br /&gt;
*https://github.com/llvm/llvm-project/blob/main/libc/src/math/generic/tanhf.cpp&lt;br /&gt;
*https://forums.developer.nvidia.com/t/hardware-accelerated-tanh-on-turing/173291&lt;br /&gt;
[[Category:Numerical Precision]]&lt;/div&gt;</summary>
		<author><name>Timo.stripf</name></author>
	</entry>
	<entry>
		<id>https://www.emmtrix.com/w139/index.php?title=tanh_Software_Implementation&amp;diff=2932</id>
		<title>tanh Software Implementation</title>
		<link rel="alternate" type="text/html" href="https://www.emmtrix.com/w139/index.php?title=tanh_Software_Implementation&amp;diff=2932"/>
		<updated>2026-02-04T12:03:01Z</updated>

		<summary type="html">&lt;p&gt;Timo.stripf: /* Taylor */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;[[File:Hyperbolic Tangent.svg|thumb]]&lt;br /&gt;
The hyperbolic tangent function, commonly referred to as &amp;lt;code&amp;gt;tanh&amp;lt;/code&amp;gt;, is a mathematical function that maps real numbers to the range &amp;lt;math&amp;gt;(-1, 1)&amp;lt;/math&amp;gt;. It is defined as:&lt;br /&gt;
&lt;br /&gt;
:&amp;lt;math&amp;gt;\tanh(x) = \frac{e^x - e^{-x}}{e^x + e^{-x}}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
From a software implementation perspective, &amp;lt;code&amp;gt;tanh&amp;lt;/code&amp;gt; is a numerically challenging function due to the use of exponential functions, which can easily overflow for large inputs and suffer from catastrophic cancellation for very small values of &amp;lt;math&amp;gt;x&amp;lt;/math&amp;gt;. These issues are particularly relevant in single-precision floating-point (&amp;lt;code&amp;gt;float32&amp;lt;/code&amp;gt;) implementations, where both accuracy and performance are critical.&lt;br /&gt;
&lt;br /&gt;
In machine learning and deep learning, &amp;lt;code&amp;gt;tanh&amp;lt;/code&amp;gt; is widely used as an activation function because it introduces non-linearity and produces outputs centered around zero, which can improve gradient-based optimization. As a result, &amp;lt;code&amp;gt;tanh&amp;lt;/code&amp;gt; is often evaluated billions of times during training and inference, making efficient and numerically stable implementations essential.&lt;br /&gt;
&lt;br /&gt;
This article focuses on practical software implementations of &amp;lt;code&amp;gt;tanh&amp;lt;/code&amp;gt;, analyzing different computational formulations with respect to floating-point accuracy, ULP error, overflow behavior, and NaN generation. The goal is to provide a foundation for designing robust &amp;lt;code&amp;gt;tanh&amp;lt;/code&amp;gt; implementations suitable for performance-critical environments such as numerical libraries, embedded systems, and AI workloads.&lt;br /&gt;
&lt;br /&gt;
== Problem Overview ==&lt;br /&gt;
The hyperbolic tangent function poses several challenges when implemented in finite-precision floating-point arithmetic. While the mathematical definition is simple, direct translations into software often suffer from numerical instability and performance issues.&lt;br /&gt;
&lt;br /&gt;
A primary challenge is the use of exponential functions. For large input values, &amp;lt;code&amp;gt;exp(x)&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;exp(-x)&amp;lt;/code&amp;gt; can overflow in single-precision floating-point arithmetic, leading to infinities and, in some formulations, NaN results. Avoiding overflow therefore requires careful reformulation or explicit range handling.&lt;br /&gt;
&lt;br /&gt;
For very small input values, catastrophic cancellation becomes the dominant issue. Expressions such as &amp;lt;math&amp;gt;e^x - e^{-x}&amp;lt;/math&amp;gt; involve the subtraction of nearly equal numbers, causing a severe loss of significant bits and large ULP errors. In these regions, naive implementations may return zero or highly inaccurate results even though &amp;lt;math&amp;gt;\tanh(x) \approx x&amp;lt;/math&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
In addition to accuracy concerns, performance is a critical factor. In many applications, especially machine learning and numerical simulations, &amp;lt;code&amp;gt;tanh&amp;lt;/code&amp;gt; is evaluated extremely frequently. This makes it necessary to balance numerical robustness with the cost of transcendental function calls, branch complexity, and instruction-level efficiency.&lt;br /&gt;
&lt;br /&gt;
==Direct Exponential Formulations==&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+&lt;br /&gt;
!Id&lt;br /&gt;
!Formula&lt;br /&gt;
!Highest ULP Error&lt;br /&gt;
!ULP &amp;lt;= 2.0&lt;br /&gt;
!NaN&lt;br /&gt;
|-&lt;br /&gt;
|exp_v1&lt;br /&gt;
|&amp;lt;math&amp;gt;\frac{e^x - e^{-x}}{e^x + e^{-x}}&amp;lt;/math&amp;gt;&lt;br /&gt;
|x &amp;lt; 0: 8.55638e+08 at -2.98023e-08 with 0x0p+0 instead of -0x1p-25&lt;br /&gt;
&lt;br /&gt;
x &amp;gt; 0: 8.55638e+08 at 2.98023e-08 with 0x0p+0 instead of 0x1p-25&lt;br /&gt;
| -88.7246 &amp;lt; x &amp;lt; -7.14197&lt;br /&gt;
7.14197 &amp;lt; x &amp;lt; 88.7246&lt;br /&gt;
| -inf &amp;lt; x &amp;lt; -88.7246&lt;br /&gt;
88.7246 &amp;lt; x &amp;lt; inf&lt;br /&gt;
|-&lt;br /&gt;
|exp_v2&lt;br /&gt;
|&amp;lt;math&amp;gt;\frac{e^{2x} - 1}{e^{2x} + 1}&amp;lt;/math&amp;gt;&lt;br /&gt;
|x &amp;lt; 0: 8.47249e+08 at -1.49012e-08 with 0x0p+0 instead of -0x1p-26&lt;br /&gt;
x &amp;gt; 0: 8.55638e+08 at 2.98023e-08 with 0x0p+0 instead of 0x1.fffffep-26&lt;br /&gt;
| -inf &amp;lt; x &amp;lt; -0.547821&lt;br /&gt;
0.254822 &amp;lt; x &amp;lt; 8.31787&lt;br /&gt;
&lt;br /&gt;
8.38379 &amp;lt; x &amp;lt; 44.3623&lt;br /&gt;
|44.3623 &amp;lt; x &amp;lt; inf&lt;br /&gt;
|-&lt;br /&gt;
|exp_v3&lt;br /&gt;
|&amp;lt;math&amp;gt;1 - \frac{2}{e^{2x} + 1}&amp;lt;/math&amp;gt;&lt;br /&gt;
|x &amp;lt; 0: 8.59832e+08 at -4.47035e-08 with 0x0p+0 instead of -0x1.8p-25&lt;br /&gt;
x &amp;gt; 0: 8.68221e+08 at 8.9407e-08 with 0x0p+0 instead of 0x1.7ffffcp-24&lt;br /&gt;
| -inf &amp;lt; x &amp;lt; -6.26233&lt;br /&gt;
0.346382 &amp;lt; x &amp;lt; inf&lt;br /&gt;
| -&lt;br /&gt;
|-&lt;br /&gt;
|expm1&lt;br /&gt;
|&amp;lt;math&amp;gt;\frac{expm1(2x)}{expm1(2x) + 2}&amp;lt;/math&amp;gt; &lt;br /&gt;
|x &amp;lt; 0: 2.49585 at -3.95627 with -0x1.ffa00ap-1 instead of -0x1.ffa00ep-1&lt;br /&gt;
x &amp;gt; 0: 2.41657 at 0.0155837 with 0x1.fe9b66p-7 instead of 0x1.fe9b62p-7&lt;br /&gt;
| -inf &amp;lt; x &amp;lt; -5.57227&lt;br /&gt;
-0.42981 &amp;lt; x &amp;lt; 0.00777948&lt;br /&gt;
&lt;br /&gt;
0.125351 &amp;lt; x &amp;lt; 44.3623&lt;br /&gt;
|44.3623 &amp;lt; x &amp;lt; inf&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
The first equation is the definition of the hyperbolic tangent function. This equation requires two exponentials with different values. Since exponentials are expensive to compute, we can use the second equation to reduce the number of exponentials to one.&lt;br /&gt;
&lt;br /&gt;
The second equation has the drawback that both the numerator and the denominator can become infinity for large values of x which results in NaN. That happens for &amp;lt;math&amp;gt;|x| &amp;gt; \log(FLT\_MAX)/2 \approx 44&amp;lt;/math&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
The third equation solves this problem but has a slightly higher error for &amp;lt;math&amp;gt;x \approx 2^{-25}&amp;lt;/math&amp;gt;. In the following graph you can see the error chart for equation 3. The error charts for the other two equations are very similar. All three equations have an error of up to 16 mio ULPs for &amp;lt;math&amp;gt;x &amp;lt; 2^{-5}&amp;lt;/math&amp;gt; which means that up to 24 bits could be wrong.&lt;br /&gt;
&lt;br /&gt;
=== exp_v2 ===&lt;br /&gt;
&lt;br /&gt;
The following error graph shows the ULP error of the &amp;lt;code&amp;gt;exp_v2&amp;lt;/code&amp;gt; formulation across the full &amp;lt;code&amp;gt;float32&amp;lt;/code&amp;gt; input range. &lt;br /&gt;
This formulation reduces the number of exponential evaluations to one, but is particularly sensitive to catastrophic cancellation for very small input values and to overflow for large &amp;lt;math&amp;gt;|x|&amp;lt;/math&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
{{#widget:FloatErrorGraph|id=tanhf_exp_v2&lt;br /&gt;
  |data=[ [&amp;quot;-2^127&amp;quot;,0,1.17549e-38],[&amp;quot;-2^126&amp;quot;,0,1.17549e-38],[&amp;quot;-2^125&amp;quot;,0,1.17549e-38],[&amp;quot;-2^124&amp;quot;,0,1.17549e-38],[&amp;quot;-2^123&amp;quot;,0,1.17549e-38],[&amp;quot;-2^122&amp;quot;,0,1.17549e-38],[&amp;quot;-2^121&amp;quot;,0,1.17549e-38],[&amp;quot;-2^120&amp;quot;,0,1.17549e-38],[&amp;quot;-2^119&amp;quot;,0,1.17549e-38],[&amp;quot;-2^118&amp;quot;,0,1.17549e-38],[&amp;quot;-2^117&amp;quot;,0,1.17549e-38],[&amp;quot;-2^116&amp;quot;,0,1.17549e-38],[&amp;quot;-2^115&amp;quot;,0,1.17549e-38],[&amp;quot;-2^114&amp;quot;,0,1.17549e-38],[&amp;quot;-2^113&amp;quot;,0,1.17549e-38],[&amp;quot;-2^112&amp;quot;,0,1.17549e-38],[&amp;quot;-2^111&amp;quot;,0,1.17549e-38],[&amp;quot;-2^110&amp;quot;,0,1.17549e-38],[&amp;quot;-2^109&amp;quot;,0,1.17549e-38],[&amp;quot;-2^108&amp;quot;,0,1.17549e-38],[&amp;quot;-2^107&amp;quot;,0,1.17549e-38],[&amp;quot;-2^106&amp;quot;,0,1.17549e-38],[&amp;quot;-2^105&amp;quot;,0,1.17549e-38],[&amp;quot;-2^104&amp;quot;,0,1.17549e-38],[&amp;quot;-2^103&amp;quot;,0,1.17549e-38],[&amp;quot;-2^102&amp;quot;,0,1.17549e-38],[&amp;quot;-2^101&amp;quot;,0,1.17549e-38],[&amp;quot;-2^100&amp;quot;,0,1.17549e-38],[&amp;quot;-2^99&amp;quot;,0,1.17549e-38],[&amp;quot;-2^98&amp;quot;,0,1.17549e-38],[&amp;quot;-2^97&amp;quot;,0,1.17549e-38],[&amp;quot;-2^96&amp;quot;,0,1.17549e-38],[&amp;quot;-2^95&amp;quot;,0,1.17549e-38],[&amp;quot;-2^94&amp;quot;,0,1.17549e-38],[&amp;quot;-2^93&amp;quot;,0,1.17549e-38],[&amp;quot;-2^92&amp;quot;,0,1.17549e-38],[&amp;quot;-2^91&amp;quot;,0,1.17549e-38],[&amp;quot;-2^90&amp;quot;,0,1.17549e-38],[&amp;quot;-2^89&amp;quot;,0,1.17549e-38],[&amp;quot;-2^88&amp;quot;,0,1.17549e-38],[&amp;quot;-2^87&amp;quot;,0,1.17549e-38],[&amp;quot;-2^86&amp;quot;,0,1.17549e-38],[&amp;quot;-2^85&amp;quot;,0,1.17549e-38],[&amp;quot;-2^84&amp;quot;,0,1.17549e-38],[&amp;quot;-2^83&amp;quot;,0,1.17549e-38],[&amp;quot;-2^82&amp;quot;,0,1.17549e-38],[&amp;quot;-2^81&amp;quot;,0,1.17549e-38],[&amp;quot;-2^80&amp;quot;,0,1.17549e-38],[&amp;quot;-2^79&amp;quot;,0,1.17549e-38],[&amp;quot;-2^78&amp;quot;,0,1.17549e-38],[&amp;quot;-2^77&amp;quot;,0,1.17549e-38],[&amp;quot;-2^76&amp;quot;,0,1.17549e-38],[&amp;quot;-2^75&amp;quot;,0,1.17549e-38],[&amp;quot;-2^74&amp;quot;,0,1.17549e-38],[&amp;quot;-2^73&amp;quot;,0,1.17549e-38],[&amp;quot;-2^72&amp;quot;,0,1.17549e-38],[&amp;quot;-2^71&amp;quot;,0,1.17549e-38],[&amp;quot;-2^70&amp;quot;,0,1.17549e-38],[&amp;quot;-2^69&amp;quot;,0,1.17549e-38],[&amp;quot;-2^68&amp;quot;,0,1.17549e-38],[&amp;quot;-2^67&amp;quot;,0,1.17549e-38],[&amp;quot;-2^66&amp;quot;,0,1.17549e-38],[&amp;quot;-2^65&amp;quot;,0,1.17549e-38],[&amp;quot;-2^64&amp;quot;,0,1.17549e-38],[&amp;quot;-2^63&amp;quot;,0,1.17549e-38],[&amp;quot;-2^62&amp;quot;,0,1.17549e-38],[&amp;quot;-2^61&amp;quot;,0,1.17549e-38],[&amp;quot;-2^60&amp;quot;,0,1.17549e-38],[&amp;quot;-2^59&amp;quot;,0,1.17549e-38],[&amp;quot;-2^58&amp;quot;,0,1.17549e-38],[&amp;quot;-2^57&amp;quot;,0,1.17549e-38],[&amp;quot;-2^56&amp;quot;,0,1.17549e-38],[&amp;quot;-2^55&amp;quot;,0,1.17549e-38],[&amp;quot;-2^54&amp;quot;,0,1.17549e-38],[&amp;quot;-2^53&amp;quot;,0,1.17549e-38],[&amp;quot;-2^52&amp;quot;,0,1.17549e-38],[&amp;quot;-2^51&amp;quot;,0,1.17549e-38],[&amp;quot;-2^50&amp;quot;,0,1.17549e-38],[&amp;quot;-2^49&amp;quot;,0,1.17549e-38],[&amp;quot;-2^48&amp;quot;,0,1.17549e-38],[&amp;quot;-2^47&amp;quot;,0,1.17549e-38],[&amp;quot;-2^46&amp;quot;,0,1.17549e-38],[&amp;quot;-2^45&amp;quot;,0,1.17549e-38],[&amp;quot;-2^44&amp;quot;,0,1.17549e-38],[&amp;quot;-2^43&amp;quot;,0,1.17549e-38],[&amp;quot;-2^42&amp;quot;,0,1.17549e-38],[&amp;quot;-2^41&amp;quot;,0,1.17549e-38],[&amp;quot;-2^40&amp;quot;,0,1.17549e-38],[&amp;quot;-2^39&amp;quot;,0,1.17549e-38],[&amp;quot;-2^38&amp;quot;,0,1.17549e-38],[&amp;quot;-2^37&amp;quot;,0,1.17549e-38],[&amp;quot;-2^36&amp;quot;,0,1.17549e-38],[&amp;quot;-2^35&amp;quot;,0,1.17549e-38],[&amp;quot;-2^34&amp;quot;,0,1.17549e-38],[&amp;quot;-2^33&amp;quot;,0,1.17549e-38],[&amp;quot;-2^32&amp;quot;,0,1.17549e-38],[&amp;quot;-2^31&amp;quot;,0,1.17549e-38],[&amp;quot;-2^30&amp;quot;,0,1.17549e-38],[&amp;quot;-2^29&amp;quot;,0,1.17549e-38],[&amp;quot;-2^28&amp;quot;,0,1.17549e-38],[&amp;quot;-2^27&amp;quot;,0,1.17549e-38],[&amp;quot;-2^26&amp;quot;,0,1.17549e-38],[&amp;quot;-2^25&amp;quot;,0,1.17549e-38],[&amp;quot;-2^24&amp;quot;,0,1.17549e-38],[&amp;quot;-2^23&amp;quot;,0,1.17549e-38],[&amp;quot;-2^22&amp;quot;,0,1.17549e-38],[&amp;quot;-2^21&amp;quot;,0,1.17549e-38],[&amp;quot;-2^20&amp;quot;,0,1.17549e-38],[&amp;quot;-2^19&amp;quot;,0,1.17549e-38],[&amp;quot;-2^18&amp;quot;,0,1.17549e-38],[&amp;quot;-2^17&amp;quot;,0,1.17549e-38],[&amp;quot;-2^16&amp;quot;,0,1.17549e-38],[&amp;quot;-2^15&amp;quot;,0,1.17549e-38],[&amp;quot;-2^14&amp;quot;,0,1.17549e-38],[&amp;quot;-2^13&amp;quot;,0,1.17549e-38],[&amp;quot;-2^12&amp;quot;,0,1.17549e-38],[&amp;quot;-2^11&amp;quot;,0,1.17549e-38],[&amp;quot;-2^10&amp;quot;,0,1.17549e-38],[&amp;quot;-2^9&amp;quot;,0,1.17549e-38],[&amp;quot;-2^8&amp;quot;,0,1.17549e-38],[&amp;quot;-2^7&amp;quot;,0,1.17549e-38],[&amp;quot;-2^6&amp;quot;,0,1.17549e-38],[&amp;quot;-2^5&amp;quot;,0,1.17549e-38],[&amp;quot;-2^4&amp;quot;,2.53131e-14,4.24683e-07],[&amp;quot;-2^3&amp;quot;,5.95888e-08,0.999734],[&amp;quot;-2^2&amp;quot;,8.86696e-08,1.48763],[&amp;quot;-2^1&amp;quot;,8.91609e-08,1.49587],[&amp;quot;-2^0&amp;quot;,8.73743e-08,1.4659],[&amp;quot;-2^-1&amp;quot;,8.65382e-08,2.13222],[&amp;quot;-2^-2&amp;quot;,6.18481e-08,2.61428],[&amp;quot;-2^-3&amp;quot;,3.88071e-08,3.08594],[&amp;quot;-2^-4&amp;quot;,2.6234e-08,4.23594],[&amp;quot;-2^-5&amp;quot;,2.02799e-08,8.19726],[&amp;quot;-2^-6&amp;quot;,1.75183e-08,12.4495],[&amp;quot;-2^-7&amp;quot;,1.61937e-08,17.3878],[&amp;quot;-2^-8&amp;quot;,1.5566e-08,33.4278],[&amp;quot;-2^-9&amp;quot;,1.51688e-08,65.1495],[&amp;quot;-2^-10&amp;quot;,1.50281e-08,129.09],[&amp;quot;-2^-11&amp;quot;,1.49613e-08,257.033],[&amp;quot;-2^-12&amp;quot;,1.49296e-08,512.977],[&amp;quot;-2^-13&amp;quot;,1.49174e-08,1025.12],[&amp;quot;-2^-14&amp;quot;,1.4909e-08,2049.08],[&amp;quot;-2^-15&amp;quot;,1.49049e-08,4097.02],[&amp;quot;-2^-16&amp;quot;,1.4903e-08,8193],[&amp;quot;-2^-17&amp;quot;,1.49021e-08,16385],[&amp;quot;-2^-18&amp;quot;,1.48862e-08,32735],[&amp;quot;-2^-19&amp;quot;,1.48973e-08,65519],[&amp;quot;-2^-20&amp;quot;,1.49001e-08,131063],[&amp;quot;-2^-21&amp;quot;,1.49009e-08,262139],[&amp;quot;-2^-22&amp;quot;,1.49011e-08,524285],[&amp;quot;-2^-23&amp;quot;,1.49011e-08,1.04857e+06],[&amp;quot;-2^-24&amp;quot;,1.49011e-08,2.09715e+06],[&amp;quot;-2^-25&amp;quot;,1.49012e-08,4.1943e+06],[&amp;quot;-2^-26&amp;quot;,1.49007e-08,8.38835e+06],[&amp;quot;-2^-27&amp;quot;,1.49012e-08,8.47249e+08],[&amp;quot;-2^-28&amp;quot;,7.45058e-09,8.38861e+08],[&amp;quot;-2^-29&amp;quot;,3.72529e-09,8.30472e+08],[&amp;quot;-2^-30&amp;quot;,1.86265e-09,8.22084e+08],[&amp;quot;-2^-31&amp;quot;,9.31323e-10,8.13695e+08],[&amp;quot;-2^-32&amp;quot;,4.65661e-10,8.05306e+08],[&amp;quot;-2^-33&amp;quot;,2.32831e-10,7.96918e+08],[&amp;quot;-2^-34&amp;quot;,1.16415e-10,7.88529e+08],[&amp;quot;-2^-35&amp;quot;,5.82077e-11,7.80141e+08],[&amp;quot;-2^-36&amp;quot;,2.91038e-11,7.71752e+08],[&amp;quot;-2^-37&amp;quot;,1.45519e-11,7.63363e+08],[&amp;quot;-2^-38&amp;quot;,7.27596e-12,7.54975e+08],[&amp;quot;-2^-39&amp;quot;,3.63798e-12,7.46586e+08],[&amp;quot;-2^-40&amp;quot;,1.81899e-12,7.38198e+08],[&amp;quot;-2^-41&amp;quot;,9.09495e-13,7.29809e+08],[&amp;quot;-2^-42&amp;quot;,4.54747e-13,7.2142e+08],[&amp;quot;-2^-43&amp;quot;,2.27374e-13,7.13032e+08],[&amp;quot;-2^-44&amp;quot;,1.13687e-13,7.04643e+08],[&amp;quot;-2^-45&amp;quot;,5.68434e-14,6.96254e+08],[&amp;quot;-2^-46&amp;quot;,2.84217e-14,6.87866e+08],[&amp;quot;-2^-47&amp;quot;,1.42109e-14,6.79477e+08],[&amp;quot;-2^-48&amp;quot;,7.10543e-15,6.71089e+08],[&amp;quot;-2^-49&amp;quot;,3.55271e-15,6.627e+08],[&amp;quot;-2^-50&amp;quot;,1.77636e-15,6.54311e+08],[&amp;quot;-2^-51&amp;quot;,8.88178e-16,6.45923e+08],[&amp;quot;-2^-52&amp;quot;,4.44089e-16,6.37534e+08],[&amp;quot;-2^-53&amp;quot;,2.22045e-16,6.29146e+08],[&amp;quot;-2^-54&amp;quot;,1.11022e-16,6.20757e+08],[&amp;quot;-2^-55&amp;quot;,5.55111e-17,6.12368e+08],[&amp;quot;-2^-56&amp;quot;,2.77556e-17,6.0398e+08],[&amp;quot;-2^-57&amp;quot;,1.38778e-17,5.95591e+08],[&amp;quot;-2^-58&amp;quot;,6.93889e-18,5.87203e+08],[&amp;quot;-2^-59&amp;quot;,3.46945e-18,5.78814e+08],[&amp;quot;-2^-60&amp;quot;,1.73472e-18,5.70425e+08],[&amp;quot;-2^-61&amp;quot;,8.67362e-19,5.62037e+08],[&amp;quot;-2^-62&amp;quot;,4.33681e-19,5.53648e+08],[&amp;quot;-2^-63&amp;quot;,2.1684e-19,5.4526e+08],[&amp;quot;-2^-64&amp;quot;,1.0842e-19,5.36871e+08],[&amp;quot;-2^-65&amp;quot;,5.42101e-20,5.28482e+08],[&amp;quot;-2^-66&amp;quot;,2.71051e-20,5.20094e+08],[&amp;quot;-2^-67&amp;quot;,1.35525e-20,5.11705e+08],[&amp;quot;-2^-68&amp;quot;,6.77626e-21,5.03316e+08],[&amp;quot;-2^-69&amp;quot;,3.38813e-21,4.94928e+08],[&amp;quot;-2^-70&amp;quot;,1.69407e-21,4.86539e+08],[&amp;quot;-2^-71&amp;quot;,8.47033e-22,4.78151e+08],[&amp;quot;-2^-72&amp;quot;,4.23516e-22,4.69762e+08],[&amp;quot;-2^-73&amp;quot;,2.11758e-22,4.61373e+08],[&amp;quot;-2^-74&amp;quot;,1.05879e-22,4.52985e+08],[&amp;quot;-2^-75&amp;quot;,5.29396e-23,4.44596e+08],[&amp;quot;-2^-76&amp;quot;,2.64698e-23,4.36208e+08],[&amp;quot;-2^-77&amp;quot;,1.32349e-23,4.27819e+08],[&amp;quot;-2^-78&amp;quot;,6.61744e-24,4.1943e+08],[&amp;quot;-2^-79&amp;quot;,3.30872e-24,4.11042e+08],[&amp;quot;-2^-80&amp;quot;,1.65436e-24,4.02653e+08],[&amp;quot;-2^-81&amp;quot;,8.27181e-25,3.94265e+08],[&amp;quot;-2^-82&amp;quot;,4.1359e-25,3.85876e+08],[&amp;quot;-2^-83&amp;quot;,2.06795e-25,3.77487e+08],[&amp;quot;-2^-84&amp;quot;,1.03398e-25,3.69099e+08],[&amp;quot;-2^-85&amp;quot;,5.16988e-26,3.6071e+08],[&amp;quot;-2^-86&amp;quot;,2.58494e-26,3.52322e+08],[&amp;quot;-2^-87&amp;quot;,1.29247e-26,3.43933e+08],[&amp;quot;-2^-88&amp;quot;,6.46235e-27,3.35544e+08],[&amp;quot;-2^-89&amp;quot;,3.23117e-27,3.27156e+08],[&amp;quot;-2^-90&amp;quot;,1.61559e-27,3.18767e+08],[&amp;quot;-2^-91&amp;quot;,8.07794e-28,3.10378e+08],[&amp;quot;-2^-92&amp;quot;,4.03897e-28,3.0199e+08],[&amp;quot;-2^-93&amp;quot;,2.01948e-28,2.93601e+08],[&amp;quot;-2^-94&amp;quot;,1.00974e-28,2.85213e+08],[&amp;quot;-2^-95&amp;quot;,5.04871e-29,2.76824e+08],[&amp;quot;-2^-96&amp;quot;,2.52435e-29,2.68435e+08],[&amp;quot;-2^-97&amp;quot;,1.26218e-29,2.60047e+08],[&amp;quot;-2^-98&amp;quot;,6.31089e-30,2.51658e+08],[&amp;quot;-2^-99&amp;quot;,3.15544e-30,2.4327e+08],[&amp;quot;-2^-100&amp;quot;,1.57772e-30,2.34881e+08],[&amp;quot;-2^-101&amp;quot;,7.88861e-31,2.26492e+08],[&amp;quot;-2^-102&amp;quot;,3.9443e-31,2.18104e+08],[&amp;quot;-2^-103&amp;quot;,1.97215e-31,2.09715e+08],[&amp;quot;-2^-104&amp;quot;,9.86076e-32,2.01327e+08],[&amp;quot;-2^-105&amp;quot;,4.93038e-32,1.92938e+08],[&amp;quot;-2^-106&amp;quot;,2.46519e-32,1.84549e+08],[&amp;quot;-2^-107&amp;quot;,1.2326e-32,1.76161e+08],[&amp;quot;-2^-108&amp;quot;,6.16298e-33,1.67772e+08],[&amp;quot;-2^-109&amp;quot;,3.08149e-33,1.59384e+08],[&amp;quot;-2^-110&amp;quot;,1.54074e-33,1.50995e+08],[&amp;quot;-2^-111&amp;quot;,7.70372e-34,1.42606e+08],[&amp;quot;-2^-112&amp;quot;,3.85186e-34,1.34218e+08],[&amp;quot;-2^-113&amp;quot;,1.92593e-34,1.25829e+08],[&amp;quot;-2^-114&amp;quot;,9.62965e-35,1.17441e+08],[&amp;quot;-2^-115&amp;quot;,4.81482e-35,1.09052e+08],[&amp;quot;-2^-116&amp;quot;,2.40741e-35,1.00663e+08],[&amp;quot;-2^-117&amp;quot;,1.20371e-35,9.22747e+07],[&amp;quot;-2^-118&amp;quot;,6.01853e-36,8.38861e+07],[&amp;quot;-2^-119&amp;quot;,3.00927e-36,7.54975e+07],[&amp;quot;-2^-120&amp;quot;,1.50463e-36,6.71089e+07],[&amp;quot;-2^-121&amp;quot;,7.52316e-37,5.87203e+07],[&amp;quot;-2^-122&amp;quot;,3.76158e-37,5.03316e+07],[&amp;quot;-2^-123&amp;quot;,1.88079e-37,4.1943e+07],[&amp;quot;-2^-124&amp;quot;,9.40395e-38,3.35544e+07],[&amp;quot;-2^-125&amp;quot;,4.70198e-38,2.51658e+07],[&amp;quot;-2^-126&amp;quot;,2.35099e-38,1.67772e+07],[&amp;quot;-SN&amp;quot;,1.17546e-38,8.38835e+06],[&amp;quot;0&amp;quot;,0,1.17549e-38],[&amp;quot;+SN&amp;quot;,1.17546e-38,8.38835e+06],[&amp;quot;+2^-126&amp;quot;,2.35095e-38,1.6777e+07],[&amp;quot;+2^-125&amp;quot;,4.70191e-38,2.51656e+07],[&amp;quot;+2^-124&amp;quot;,9.40381e-38,3.35542e+07],[&amp;quot;+2^-123&amp;quot;,1.88076e-37,4.19428e+07],[&amp;quot;+2^-122&amp;quot;,3.76152e-37,5.03314e+07],[&amp;quot;+2^-121&amp;quot;,7.52305e-37,5.872e+07],[&amp;quot;+2^-120&amp;quot;,1.50461e-36,6.71086e+07],[&amp;quot;+2^-119&amp;quot;,3.00922e-36,7.54972e+07],[&amp;quot;+2^-118&amp;quot;,6.01844e-36,8.38858e+07],[&amp;quot;+2^-117&amp;quot;,1.20369e-35,9.22744e+07],[&amp;quot;+2^-116&amp;quot;,2.40738e-35,1.00663e+08],[&amp;quot;+2^-115&amp;quot;,4.81475e-35,1.09052e+08],[&amp;quot;+2^-114&amp;quot;,9.6295e-35,1.1744e+08],[&amp;quot;+2^-113&amp;quot;,1.9259e-34,1.25829e+08],[&amp;quot;+2^-112&amp;quot;,3.8518e-34,1.34217e+08],[&amp;quot;+2^-111&amp;quot;,7.7036e-34,1.42606e+08],[&amp;quot;+2^-110&amp;quot;,1.54072e-33,1.50995e+08],[&amp;quot;+2^-109&amp;quot;,3.08144e-33,1.59383e+08],[&amp;quot;+2^-108&amp;quot;,6.16288e-33,1.67772e+08],[&amp;quot;+2^-107&amp;quot;,1.23258e-32,1.76161e+08],[&amp;quot;+2^-106&amp;quot;,2.46515e-32,1.84549e+08],[&amp;quot;+2^-105&amp;quot;,4.93031e-32,1.92938e+08],[&amp;quot;+2^-104&amp;quot;,9.86061e-32,2.01326e+08],[&amp;quot;+2^-103&amp;quot;,1.97212e-31,2.09715e+08],[&amp;quot;+2^-102&amp;quot;,3.94424e-31,2.18104e+08],[&amp;quot;+2^-101&amp;quot;,7.88849e-31,2.26492e+08],[&amp;quot;+2^-100&amp;quot;,1.5777e-30,2.34881e+08],[&amp;quot;+2^-99&amp;quot;,3.1554e-30,2.43269e+08],[&amp;quot;+2^-98&amp;quot;,6.31079e-30,2.51658e+08],[&amp;quot;+2^-97&amp;quot;,1.26216e-29,2.60047e+08],[&amp;quot;+2^-96&amp;quot;,2.52432e-29,2.68435e+08],[&amp;quot;+2^-95&amp;quot;,5.04863e-29,2.76824e+08],[&amp;quot;+2^-94&amp;quot;,1.00973e-28,2.85212e+08],[&amp;quot;+2^-93&amp;quot;,2.01945e-28,2.93601e+08],[&amp;quot;+2^-92&amp;quot;,4.03891e-28,3.0199e+08],[&amp;quot;+2^-91&amp;quot;,8.07781e-28,3.10378e+08],[&amp;quot;+2^-90&amp;quot;,1.61556e-27,3.18767e+08],[&amp;quot;+2^-89&amp;quot;,3.23112e-27,3.27155e+08],[&amp;quot;+2^-88&amp;quot;,6.46225e-27,3.35544e+08],[&amp;quot;+2^-87&amp;quot;,1.29245e-26,3.43933e+08],[&amp;quot;+2^-86&amp;quot;,2.5849e-26,3.52321e+08],[&amp;quot;+2^-85&amp;quot;,5.1698e-26,3.6071e+08],[&amp;quot;+2^-84&amp;quot;,1.03396e-25,3.69098e+08],[&amp;quot;+2^-83&amp;quot;,2.06792e-25,3.77487e+08],[&amp;quot;+2^-82&amp;quot;,4.13584e-25,3.85876e+08],[&amp;quot;+2^-81&amp;quot;,8.27168e-25,3.94264e+08],[&amp;quot;+2^-80&amp;quot;,1.65434e-24,4.02653e+08],[&amp;quot;+2^-79&amp;quot;,3.30867e-24,4.11042e+08],[&amp;quot;+2^-78&amp;quot;,6.61734e-24,4.1943e+08],[&amp;quot;+2^-77&amp;quot;,1.32347e-23,4.27819e+08],[&amp;quot;+2^-76&amp;quot;,2.64694e-23,4.36207e+08],[&amp;quot;+2^-75&amp;quot;,5.29388e-23,4.44596e+08],[&amp;quot;+2^-74&amp;quot;,1.05878e-22,4.52985e+08],[&amp;quot;+2^-73&amp;quot;,2.11755e-22,4.61373e+08],[&amp;quot;+2^-72&amp;quot;,4.2351e-22,4.69762e+08],[&amp;quot;+2^-71&amp;quot;,8.4702e-22,4.7815e+08],[&amp;quot;+2^-70&amp;quot;,1.69404e-21,4.86539e+08],[&amp;quot;+2^-69&amp;quot;,3.38808e-21,4.94928e+08],[&amp;quot;+2^-68&amp;quot;,6.77616e-21,5.03316e+08],[&amp;quot;+2^-67&amp;quot;,1.35523e-20,5.11705e+08],[&amp;quot;+2^-66&amp;quot;,2.71046e-20,5.20093e+08],[&amp;quot;+2^-65&amp;quot;,5.42093e-20,5.28482e+08],[&amp;quot;+2^-64&amp;quot;,1.08419e-19,5.36871e+08],[&amp;quot;+2^-63&amp;quot;,2.16837e-19,5.45259e+08],[&amp;quot;+2^-62&amp;quot;,4.33674e-19,5.53648e+08],[&amp;quot;+2^-61&amp;quot;,8.67349e-19,5.62036e+08],[&amp;quot;+2^-60&amp;quot;,1.7347e-18,5.70425e+08],[&amp;quot;+2^-59&amp;quot;,3.46939e-18,5.78814e+08],[&amp;quot;+2^-58&amp;quot;,6.93879e-18,5.87202e+08],[&amp;quot;+2^-57&amp;quot;,1.38776e-17,5.95591e+08],[&amp;quot;+2^-56&amp;quot;,2.77552e-17,6.0398e+08],[&amp;quot;+2^-55&amp;quot;,5.55103e-17,6.12368e+08],[&amp;quot;+2^-54&amp;quot;,1.11021e-16,6.20757e+08],[&amp;quot;+2^-53&amp;quot;,2.22041e-16,6.29145e+08],[&amp;quot;+2^-52&amp;quot;,4.44082e-16,6.37534e+08],[&amp;quot;+2^-51&amp;quot;,8.88165e-16,6.45923e+08],[&amp;quot;+2^-50&amp;quot;,1.77633e-15,6.54311e+08],[&amp;quot;+2^-49&amp;quot;,3.55266e-15,6.627e+08],[&amp;quot;+2^-48&amp;quot;,7.10532e-15,6.71088e+08],[&amp;quot;+2^-47&amp;quot;,1.42106e-14,6.79477e+08],[&amp;quot;+2^-46&amp;quot;,2.84213e-14,6.87866e+08],[&amp;quot;+2^-45&amp;quot;,5.68426e-14,6.96254e+08],[&amp;quot;+2^-44&amp;quot;,1.13685e-13,7.04643e+08],[&amp;quot;+2^-43&amp;quot;,2.2737e-13,7.13031e+08],[&amp;quot;+2^-42&amp;quot;,4.5474e-13,7.2142e+08],[&amp;quot;+2^-41&amp;quot;,9.09481e-13,7.29809e+08],[&amp;quot;+2^-40&amp;quot;,1.81896e-12,7.38197e+08],[&amp;quot;+2^-39&amp;quot;,3.63792e-12,7.46586e+08],[&amp;quot;+2^-38&amp;quot;,7.27585e-12,7.54974e+08],[&amp;quot;+2^-37&amp;quot;,1.45517e-11,7.63363e+08],[&amp;quot;+2^-36&amp;quot;,2.91034e-11,7.71752e+08],[&amp;quot;+2^-35&amp;quot;,5.82068e-11,7.8014e+08],[&amp;quot;+2^-34&amp;quot;,1.16414e-10,7.88529e+08],[&amp;quot;+2^-33&amp;quot;,2.32827e-10,7.96918e+08],[&amp;quot;+2^-32&amp;quot;,4.65654e-10,8.05306e+08],[&amp;quot;+2^-31&amp;quot;,9.31308e-10,8.13695e+08],[&amp;quot;+2^-30&amp;quot;,1.86262e-09,8.22083e+08],[&amp;quot;+2^-29&amp;quot;,3.72523e-09,8.30472e+08],[&amp;quot;+2^-28&amp;quot;,7.45047e-09,8.38861e+08],[&amp;quot;+2^-27&amp;quot;,1.49009e-08,8.47249e+08],[&amp;quot;+2^-26&amp;quot;,2.98019e-08,8.55638e+08],[&amp;quot;+2^-25&amp;quot;,2.98023e-08,8.38861e+06],[&amp;quot;+2^-24&amp;quot;,2.98023e-08,4.1943e+06],[&amp;quot;+2^-23&amp;quot;,2.98023e-08,2.09715e+06],[&amp;quot;+2^-22&amp;quot;,2.98023e-08,1.04857e+06],[&amp;quot;+2^-21&amp;quot;,2.98021e-08,524284],[&amp;quot;+2^-20&amp;quot;,2.98014e-08,262136],[&amp;quot;+2^-19&amp;quot;,2.97987e-08,131056],[&amp;quot;+2^-18&amp;quot;,2.97878e-08,65504],[&amp;quot;+2^-17&amp;quot;,2.98014e-08,32767],[&amp;quot;+2^-16&amp;quot;,2.98005e-08,16384],[&amp;quot;+2^-15&amp;quot;,2.98023e-08,8192],[&amp;quot;+2^-14&amp;quot;,2.98025e-08,4351.99],[&amp;quot;+2^-13&amp;quot;,2.98044e-08,2815.96],[&amp;quot;+2^-12&amp;quot;,2.98091e-08,1024.23],[&amp;quot;+2^-11&amp;quot;,2.9812e-08,512.167],[&amp;quot;+2^-10&amp;quot;,2.98154e-08,256.113],[&amp;quot;+2^-9&amp;quot;,2.98591e-08,128.244],[&amp;quot;+2^-8&amp;quot;,3.00204e-08,85.3328],[&amp;quot;+2^-7&amp;quot;,3.01947e-08,43.675],[&amp;quot;+2^-6&amp;quot;,3.05713e-08,30.45],[&amp;quot;+2^-5&amp;quot;,3.14319e-08,8.43743],[&amp;quot;+2^-4&amp;quot;,3.31291e-08,7.77058],[&amp;quot;+2^-3&amp;quot;,3.58597e-08,3.99659],[&amp;quot;+2^-2&amp;quot;,4.14639e-08,2.29246],[&amp;quot;+2^-1&amp;quot;,8.02409e-08,1.34622],[&amp;quot;+2^0&amp;quot;,8.66201e-08,1.45324],[&amp;quot;+2^1&amp;quot;,8.8654e-08,1.48737],[&amp;quot;+2^2&amp;quot;,6.7976e-08,1.14045],[&amp;quot;+2^3&amp;quot;,1.33955e-07,2.2474],[&amp;quot;+2^4&amp;quot;,2.53131e-14,4.24683e-07],[&amp;quot;+2^5&amp;quot;,0,null],[&amp;quot;+2^6&amp;quot;,0,null],[&amp;quot;+2^7&amp;quot;,0,null],[&amp;quot;+2^8&amp;quot;,0,null],[&amp;quot;+2^9&amp;quot;,0,null],[&amp;quot;+2^10&amp;quot;,0,null],[&amp;quot;+2^11&amp;quot;,0,null],[&amp;quot;+2^12&amp;quot;,0,null],[&amp;quot;+2^13&amp;quot;,0,null],[&amp;quot;+2^14&amp;quot;,0,null],[&amp;quot;+2^15&amp;quot;,0,null],[&amp;quot;+2^16&amp;quot;,0,null],[&amp;quot;+2^17&amp;quot;,0,null],[&amp;quot;+2^18&amp;quot;,0,null],[&amp;quot;+2^19&amp;quot;,0,null],[&amp;quot;+2^20&amp;quot;,0,null],[&amp;quot;+2^21&amp;quot;,0,null],[&amp;quot;+2^22&amp;quot;,0,null],[&amp;quot;+2^23&amp;quot;,0,null],[&amp;quot;+2^24&amp;quot;,0,null],[&amp;quot;+2^25&amp;quot;,0,null],[&amp;quot;+2^26&amp;quot;,0,null],[&amp;quot;+2^27&amp;quot;,0,null],[&amp;quot;+2^28&amp;quot;,0,null],[&amp;quot;+2^29&amp;quot;,0,null],[&amp;quot;+2^30&amp;quot;,0,null],[&amp;quot;+2^31&amp;quot;,0,null],[&amp;quot;+2^32&amp;quot;,0,null],[&amp;quot;+2^33&amp;quot;,0,null],[&amp;quot;+2^34&amp;quot;,0,null],[&amp;quot;+2^35&amp;quot;,0,null],[&amp;quot;+2^36&amp;quot;,0,null],[&amp;quot;+2^37&amp;quot;,0,null],[&amp;quot;+2^38&amp;quot;,0,null],[&amp;quot;+2^39&amp;quot;,0,null],[&amp;quot;+2^40&amp;quot;,0,null],[&amp;quot;+2^41&amp;quot;,0,null],[&amp;quot;+2^42&amp;quot;,0,null],[&amp;quot;+2^43&amp;quot;,0,null],[&amp;quot;+2^44&amp;quot;,0,null],[&amp;quot;+2^45&amp;quot;,0,null],[&amp;quot;+2^46&amp;quot;,0,null],[&amp;quot;+2^47&amp;quot;,0,null],[&amp;quot;+2^48&amp;quot;,0,null],[&amp;quot;+2^49&amp;quot;,0,null],[&amp;quot;+2^50&amp;quot;,0,null],[&amp;quot;+2^51&amp;quot;,0,null],[&amp;quot;+2^52&amp;quot;,0,null],[&amp;quot;+2^53&amp;quot;,0,null],[&amp;quot;+2^54&amp;quot;,0,null],[&amp;quot;+2^55&amp;quot;,0,null],[&amp;quot;+2^56&amp;quot;,0,null],[&amp;quot;+2^57&amp;quot;,0,null],[&amp;quot;+2^58&amp;quot;,0,null],[&amp;quot;+2^59&amp;quot;,0,null],[&amp;quot;+2^60&amp;quot;,0,null],[&amp;quot;+2^61&amp;quot;,0,null],[&amp;quot;+2^62&amp;quot;,0,null],[&amp;quot;+2^63&amp;quot;,0,null],[&amp;quot;+2^64&amp;quot;,0,null],[&amp;quot;+2^65&amp;quot;,0,null],[&amp;quot;+2^66&amp;quot;,0,null],[&amp;quot;+2^67&amp;quot;,0,null],[&amp;quot;+2^68&amp;quot;,0,null],[&amp;quot;+2^69&amp;quot;,0,null],[&amp;quot;+2^70&amp;quot;,0,null],[&amp;quot;+2^71&amp;quot;,0,null],[&amp;quot;+2^72&amp;quot;,0,null],[&amp;quot;+2^73&amp;quot;,0,null],[&amp;quot;+2^74&amp;quot;,0,null],[&amp;quot;+2^75&amp;quot;,0,null],[&amp;quot;+2^76&amp;quot;,0,null],[&amp;quot;+2^77&amp;quot;,0,null],[&amp;quot;+2^78&amp;quot;,0,null],[&amp;quot;+2^79&amp;quot;,0,null],[&amp;quot;+2^80&amp;quot;,0,null],[&amp;quot;+2^81&amp;quot;,0,null],[&amp;quot;+2^82&amp;quot;,0,null],[&amp;quot;+2^83&amp;quot;,0,null],[&amp;quot;+2^84&amp;quot;,0,null],[&amp;quot;+2^85&amp;quot;,0,null],[&amp;quot;+2^86&amp;quot;,0,null],[&amp;quot;+2^87&amp;quot;,0,null],[&amp;quot;+2^88&amp;quot;,0,null],[&amp;quot;+2^89&amp;quot;,0,null],[&amp;quot;+2^90&amp;quot;,0,null],[&amp;quot;+2^91&amp;quot;,0,null],[&amp;quot;+2^92&amp;quot;,0,null],[&amp;quot;+2^93&amp;quot;,0,null],[&amp;quot;+2^94&amp;quot;,0,null],[&amp;quot;+2^95&amp;quot;,0,null],[&amp;quot;+2^96&amp;quot;,0,null],[&amp;quot;+2^97&amp;quot;,0,null],[&amp;quot;+2^98&amp;quot;,0,null],[&amp;quot;+2^99&amp;quot;,0,null],[&amp;quot;+2^100&amp;quot;,0,null],[&amp;quot;+2^101&amp;quot;,0,null],[&amp;quot;+2^102&amp;quot;,0,null],[&amp;quot;+2^103&amp;quot;,0,null],[&amp;quot;+2^104&amp;quot;,0,null],[&amp;quot;+2^105&amp;quot;,0,null],[&amp;quot;+2^106&amp;quot;,0,null],[&amp;quot;+2^107&amp;quot;,0,null],[&amp;quot;+2^108&amp;quot;,0,null],[&amp;quot;+2^109&amp;quot;,0,null],[&amp;quot;+2^110&amp;quot;,0,null],[&amp;quot;+2^111&amp;quot;,0,null],[&amp;quot;+2^112&amp;quot;,0,null],[&amp;quot;+2^113&amp;quot;,0,null],[&amp;quot;+2^114&amp;quot;,0,null],[&amp;quot;+2^115&amp;quot;,0,null],[&amp;quot;+2^116&amp;quot;,0,null],[&amp;quot;+2^117&amp;quot;,0,null],[&amp;quot;+2^118&amp;quot;,0,null],[&amp;quot;+2^119&amp;quot;,0,null],[&amp;quot;+2^120&amp;quot;,0,null],[&amp;quot;+2^121&amp;quot;,0,null],[&amp;quot;+2^122&amp;quot;,0,null],[&amp;quot;+2^123&amp;quot;,0,null],[&amp;quot;+2^124&amp;quot;,0,null],[&amp;quot;+2^125&amp;quot;,0,null],[&amp;quot;+2^126&amp;quot;,0,null],[&amp;quot;+2^127&amp;quot;,0,null] ]&lt;br /&gt;
 }}&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Key observation:&#039;&#039;&#039;  &lt;br /&gt;
For &amp;lt;math&amp;gt;|x| \ll 1&amp;lt;/math&amp;gt;, the ULP error grows rapidly and reaches extremely large values due to loss of significance. &lt;br /&gt;
For sufficiently large &amp;lt;math&amp;gt;x&amp;lt;/math&amp;gt;, both numerator and denominator overflow, leading to NaN results.&lt;br /&gt;
&lt;br /&gt;
=== expm1 ===&lt;br /&gt;
&lt;br /&gt;
This graph visualizes the ULP error of the &amp;lt;code&amp;gt;expm1&amp;lt;/code&amp;gt;-based formulation.&lt;br /&gt;
Using &amp;lt;code&amp;gt;expm1&amp;lt;/code&amp;gt; significantly improves numerical accuracy for small input values by avoiding catastrophic cancellation in the expression &amp;lt;math&amp;gt;e^{2x} - 1&amp;lt;/math&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
{{#widget:FloatErrorGraph|id=tanhf_expm1&lt;br /&gt;
  |data=[ [&amp;quot;-2^127&amp;quot;,0,1.17549e-38],[&amp;quot;-2^126&amp;quot;,0,1.17549e-38],[&amp;quot;-2^125&amp;quot;,0,1.17549e-38],[&amp;quot;-2^124&amp;quot;,0,1.17549e-38],[&amp;quot;-2^123&amp;quot;,0,1.17549e-38],[&amp;quot;-2^122&amp;quot;,0,1.17549e-38],[&amp;quot;-2^121&amp;quot;,0,1.17549e-38],[&amp;quot;-2^120&amp;quot;,0,1.17549e-38],[&amp;quot;-2^119&amp;quot;,0,1.17549e-38],[&amp;quot;-2^118&amp;quot;,0,1.17549e-38],[&amp;quot;-2^117&amp;quot;,0,1.17549e-38],[&amp;quot;-2^116&amp;quot;,0,1.17549e-38],[&amp;quot;-2^115&amp;quot;,0,1.17549e-38],[&amp;quot;-2^114&amp;quot;,0,1.17549e-38],[&amp;quot;-2^113&amp;quot;,0,1.17549e-38],[&amp;quot;-2^112&amp;quot;,0,1.17549e-38],[&amp;quot;-2^111&amp;quot;,0,1.17549e-38],[&amp;quot;-2^110&amp;quot;,0,1.17549e-38],[&amp;quot;-2^109&amp;quot;,0,1.17549e-38],[&amp;quot;-2^108&amp;quot;,0,1.17549e-38],[&amp;quot;-2^107&amp;quot;,0,1.17549e-38],[&amp;quot;-2^106&amp;quot;,0,1.17549e-38],[&amp;quot;-2^105&amp;quot;,0,1.17549e-38],[&amp;quot;-2^104&amp;quot;,0,1.17549e-38],[&amp;quot;-2^103&amp;quot;,0,1.17549e-38],[&amp;quot;-2^102&amp;quot;,0,1.17549e-38],[&amp;quot;-2^101&amp;quot;,0,1.17549e-38],[&amp;quot;-2^100&amp;quot;,0,1.17549e-38],[&amp;quot;-2^99&amp;quot;,0,1.17549e-38],[&amp;quot;-2^98&amp;quot;,0,1.17549e-38],[&amp;quot;-2^97&amp;quot;,0,1.17549e-38],[&amp;quot;-2^96&amp;quot;,0,1.17549e-38],[&amp;quot;-2^95&amp;quot;,0,1.17549e-38],[&amp;quot;-2^94&amp;quot;,0,1.17549e-38],[&amp;quot;-2^93&amp;quot;,0,1.17549e-38],[&amp;quot;-2^92&amp;quot;,0,1.17549e-38],[&amp;quot;-2^91&amp;quot;,0,1.17549e-38],[&amp;quot;-2^90&amp;quot;,0,1.17549e-38],[&amp;quot;-2^89&amp;quot;,0,1.17549e-38],[&amp;quot;-2^88&amp;quot;,0,1.17549e-38],[&amp;quot;-2^87&amp;quot;,0,1.17549e-38],[&amp;quot;-2^86&amp;quot;,0,1.17549e-38],[&amp;quot;-2^85&amp;quot;,0,1.17549e-38],[&amp;quot;-2^84&amp;quot;,0,1.17549e-38],[&amp;quot;-2^83&amp;quot;,0,1.17549e-38],[&amp;quot;-2^82&amp;quot;,0,1.17549e-38],[&amp;quot;-2^81&amp;quot;,0,1.17549e-38],[&amp;quot;-2^80&amp;quot;,0,1.17549e-38],[&amp;quot;-2^79&amp;quot;,0,1.17549e-38],[&amp;quot;-2^78&amp;quot;,0,1.17549e-38],[&amp;quot;-2^77&amp;quot;,0,1.17549e-38],[&amp;quot;-2^76&amp;quot;,0,1.17549e-38],[&amp;quot;-2^75&amp;quot;,0,1.17549e-38],[&amp;quot;-2^74&amp;quot;,0,1.17549e-38],[&amp;quot;-2^73&amp;quot;,0,1.17549e-38],[&amp;quot;-2^72&amp;quot;,0,1.17549e-38],[&amp;quot;-2^71&amp;quot;,0,1.17549e-38],[&amp;quot;-2^70&amp;quot;,0,1.17549e-38],[&amp;quot;-2^69&amp;quot;,0,1.17549e-38],[&amp;quot;-2^68&amp;quot;,0,1.17549e-38],[&amp;quot;-2^67&amp;quot;,0,1.17549e-38],[&amp;quot;-2^66&amp;quot;,0,1.17549e-38],[&amp;quot;-2^65&amp;quot;,0,1.17549e-38],[&amp;quot;-2^64&amp;quot;,0,1.17549e-38],[&amp;quot;-2^63&amp;quot;,0,1.17549e-38],[&amp;quot;-2^62&amp;quot;,0,1.17549e-38],[&amp;quot;-2^61&amp;quot;,0,1.17549e-38],[&amp;quot;-2^60&amp;quot;,0,1.17549e-38],[&amp;quot;-2^59&amp;quot;,0,1.17549e-38],[&amp;quot;-2^58&amp;quot;,0,1.17549e-38],[&amp;quot;-2^57&amp;quot;,0,1.17549e-38],[&amp;quot;-2^56&amp;quot;,0,1.17549e-38],[&amp;quot;-2^55&amp;quot;,0,1.17549e-38],[&amp;quot;-2^54&amp;quot;,0,1.17549e-38],[&amp;quot;-2^53&amp;quot;,0,1.17549e-38],[&amp;quot;-2^52&amp;quot;,0,1.17549e-38],[&amp;quot;-2^51&amp;quot;,0,1.17549e-38],[&amp;quot;-2^50&amp;quot;,0,1.17549e-38],[&amp;quot;-2^49&amp;quot;,0,1.17549e-38],[&amp;quot;-2^48&amp;quot;,0,1.17549e-38],[&amp;quot;-2^47&amp;quot;,0,1.17549e-38],[&amp;quot;-2^46&amp;quot;,0,1.17549e-38],[&amp;quot;-2^45&amp;quot;,0,1.17549e-38],[&amp;quot;-2^44&amp;quot;,0,1.17549e-38],[&amp;quot;-2^43&amp;quot;,0,1.17549e-38],[&amp;quot;-2^42&amp;quot;,0,1.17549e-38],[&amp;quot;-2^41&amp;quot;,0,1.17549e-38],[&amp;quot;-2^40&amp;quot;,0,1.17549e-38],[&amp;quot;-2^39&amp;quot;,0,1.17549e-38],[&amp;quot;-2^38&amp;quot;,0,1.17549e-38],[&amp;quot;-2^37&amp;quot;,0,1.17549e-38],[&amp;quot;-2^36&amp;quot;,0,1.17549e-38],[&amp;quot;-2^35&amp;quot;,0,1.17549e-38],[&amp;quot;-2^34&amp;quot;,0,1.17549e-38],[&amp;quot;-2^33&amp;quot;,0,1.17549e-38],[&amp;quot;-2^32&amp;quot;,0,1.17549e-38],[&amp;quot;-2^31&amp;quot;,0,1.17549e-38],[&amp;quot;-2^30&amp;quot;,0,1.17549e-38],[&amp;quot;-2^29&amp;quot;,0,1.17549e-38],[&amp;quot;-2^28&amp;quot;,0,1.17549e-38],[&amp;quot;-2^27&amp;quot;,0,1.17549e-38],[&amp;quot;-2^26&amp;quot;,0,1.17549e-38],[&amp;quot;-2^25&amp;quot;,0,1.17549e-38],[&amp;quot;-2^24&amp;quot;,0,1.17549e-38],[&amp;quot;-2^23&amp;quot;,0,1.17549e-38],[&amp;quot;-2^22&amp;quot;,0,1.17549e-38],[&amp;quot;-2^21&amp;quot;,0,1.17549e-38],[&amp;quot;-2^20&amp;quot;,0,1.17549e-38],[&amp;quot;-2^19&amp;quot;,0,1.17549e-38],[&amp;quot;-2^18&amp;quot;,0,1.17549e-38],[&amp;quot;-2^17&amp;quot;,0,1.17549e-38],[&amp;quot;-2^16&amp;quot;,0,1.17549e-38],[&amp;quot;-2^15&amp;quot;,0,1.17549e-38],[&amp;quot;-2^14&amp;quot;,0,1.17549e-38],[&amp;quot;-2^13&amp;quot;,0,1.17549e-38],[&amp;quot;-2^12&amp;quot;,0,1.17549e-38],[&amp;quot;-2^11&amp;quot;,0,1.17549e-38],[&amp;quot;-2^10&amp;quot;,0,1.17549e-38],[&amp;quot;-2^9&amp;quot;,0,1.17549e-38],[&amp;quot;-2^8&amp;quot;,0,1.17549e-38],[&amp;quot;-2^7&amp;quot;,0,1.17549e-38],[&amp;quot;-2^6&amp;quot;,0,1.17549e-38],[&amp;quot;-2^5&amp;quot;,0,1.17549e-38],[&amp;quot;-2^4&amp;quot;,2.53131e-14,4.24683e-07],[&amp;quot;-2^3&amp;quot;,1.19138e-07,1.9988],[&amp;quot;-2^2&amp;quot;,1.46791e-07,2.46274],[&amp;quot;-2^1&amp;quot;,1.47723e-07,2.47839],[&amp;quot;-2^0&amp;quot;,1.40758e-07,2.36153],[&amp;quot;-2^-1&amp;quot;,1.13422e-07,2.23445],[&amp;quot;-2^-2&amp;quot;,6.38012e-08,2.14081],[&amp;quot;-2^-3&amp;quot;,2.69538e-08,1.80884],[&amp;quot;-2^-4&amp;quot;,1.21308e-08,1.62817],[&amp;quot;-2^-5&amp;quot;,5.59535e-09,1.50199],[&amp;quot;-2^-6&amp;quot;,2.69177e-09,1.44513],[&amp;quot;-2^-7&amp;quot;,1.31289e-09,1.4097],[&amp;quot;-2^-8&amp;quot;,6.49688e-10,1.3952],[&amp;quot;-2^-9&amp;quot;,3.32043e-10,1.42611],[&amp;quot;-2^-10&amp;quot;,1.66275e-10,1.4283],[&amp;quot;-2^-11&amp;quot;,8.08595e-11,1.38916],[&amp;quot;-2^-12&amp;quot;,3.8939e-11,1.33794],[&amp;quot;-2^-13&amp;quot;,1.9401e-11,1.33323],[&amp;quot;-2^-14&amp;quot;,7.88212e-12,1.08331],[&amp;quot;-2^-15&amp;quot;,3.71372e-12,1.02082],[&amp;quot;-2^-16&amp;quot;,1.82845e-12,1.0052],[&amp;quot;-2^-17&amp;quot;,9.10676e-13,1.0013],[&amp;quot;-2^-18&amp;quot;,4.54895e-13,1.00032],[&amp;quot;-2^-19&amp;quot;,2.27392e-13,1.00008],[&amp;quot;-2^-20&amp;quot;,1.13689e-13,1.00002],[&amp;quot;-2^-21&amp;quot;,5.68437e-14,1],[&amp;quot;-2^-22&amp;quot;,2.84217e-14,1],[&amp;quot;-2^-23&amp;quot;,1.42109e-14,1],[&amp;quot;-2^-24&amp;quot;,7.10543e-15,1],[&amp;quot;-2^-25&amp;quot;,3.55271e-15,1],[&amp;quot;-2^-26&amp;quot;,9.92617e-24,5.58794e-09],[&amp;quot;-2^-27&amp;quot;,1.65436e-24,1.86265e-09],[&amp;quot;-2^-28&amp;quot;,8.27181e-25,1.86265e-09],[&amp;quot;-2^-29&amp;quot;,4.1359e-25,1.86265e-09],[&amp;quot;-2^-30&amp;quot;,2.06795e-25,1.86265e-09],[&amp;quot;-2^-31&amp;quot;,1.03398e-25,1.86265e-09],[&amp;quot;-2^-32&amp;quot;,5.16988e-26,1.86265e-09],[&amp;quot;-2^-33&amp;quot;,2.58494e-26,1.86265e-09],[&amp;quot;-2^-34&amp;quot;,1.29247e-26,1.86265e-09],[&amp;quot;-2^-35&amp;quot;,6.46235e-27,1.86265e-09],[&amp;quot;-2^-36&amp;quot;,3.23117e-27,1.86265e-09],[&amp;quot;-2^-37&amp;quot;,1.61559e-27,1.86265e-09],[&amp;quot;-2^-38&amp;quot;,8.07794e-28,1.86265e-09],[&amp;quot;-2^-39&amp;quot;,4.03897e-28,1.86265e-09],[&amp;quot;-2^-40&amp;quot;,2.01948e-28,1.86265e-09],[&amp;quot;-2^-41&amp;quot;,1.00974e-28,1.86265e-09],[&amp;quot;-2^-42&amp;quot;,5.04871e-29,1.86265e-09],[&amp;quot;-2^-43&amp;quot;,2.52435e-29,1.86265e-09],[&amp;quot;-2^-44&amp;quot;,1.26218e-29,1.86265e-09],[&amp;quot;-2^-45&amp;quot;,6.31089e-30,1.86265e-09],[&amp;quot;-2^-46&amp;quot;,3.15544e-30,1.86265e-09],[&amp;quot;-2^-47&amp;quot;,1.57772e-30,1.86265e-09],[&amp;quot;-2^-48&amp;quot;,7.88861e-31,1.86265e-09],[&amp;quot;-2^-49&amp;quot;,3.9443e-31,1.86265e-09],[&amp;quot;-2^-50&amp;quot;,1.97215e-31,1.86265e-09],[&amp;quot;-2^-51&amp;quot;,9.86076e-32,1.86265e-09],[&amp;quot;-2^-52&amp;quot;,4.93038e-32,1.86265e-09],[&amp;quot;-2^-53&amp;quot;,2.46519e-32,1.86265e-09],[&amp;quot;-2^-54&amp;quot;,1.2326e-32,1.86265e-09],[&amp;quot;-2^-55&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-56&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-57&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-58&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-59&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-60&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-61&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-62&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-63&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-64&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-65&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-66&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-67&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-68&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-69&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-70&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-71&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-72&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-73&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-74&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-75&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-76&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-77&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-78&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-79&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-80&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-81&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-82&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-83&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-84&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-85&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-86&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-87&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-88&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-89&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-90&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-91&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-92&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-93&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-94&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-95&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-96&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-97&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-98&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-99&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-100&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-101&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-102&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-103&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-104&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-105&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-106&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-107&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-108&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-109&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-110&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-111&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-112&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-113&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-114&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-115&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-116&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-117&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-118&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-119&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-120&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-121&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-122&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-123&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-124&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-125&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-126&amp;quot;,0,1.17549e-38],[&amp;quot;-SN&amp;quot;,0,1.17549e-38],[&amp;quot;0&amp;quot;,0,1.17549e-38],[&amp;quot;+SN&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-126&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-125&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-124&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-123&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-122&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-121&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-120&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-119&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-118&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-117&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-116&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-115&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-114&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-113&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-112&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-111&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-110&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-109&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-108&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-107&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-106&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-105&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-104&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-103&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-102&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-101&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-100&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-99&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-98&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-97&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-96&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-95&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-94&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-93&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-92&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-91&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-90&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-89&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-88&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-87&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-86&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-85&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-84&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-83&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-82&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-81&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-80&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-79&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-78&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-77&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-76&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-75&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-74&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-73&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-72&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-71&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-70&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-69&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-68&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-67&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-66&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-65&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-64&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-63&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-62&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-61&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-60&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-59&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-58&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-57&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-56&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-55&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-54&amp;quot;,1.2326e-32,1.86265e-09],[&amp;quot;+2^-53&amp;quot;,2.46519e-32,1.86265e-09],[&amp;quot;+2^-52&amp;quot;,4.93038e-32,1.86265e-09],[&amp;quot;+2^-51&amp;quot;,9.86076e-32,1.86265e-09],[&amp;quot;+2^-50&amp;quot;,1.97215e-31,1.86265e-09],[&amp;quot;+2^-49&amp;quot;,3.9443e-31,1.86265e-09],[&amp;quot;+2^-48&amp;quot;,7.88861e-31,1.86265e-09],[&amp;quot;+2^-47&amp;quot;,1.57772e-30,1.86265e-09],[&amp;quot;+2^-46&amp;quot;,3.15544e-30,1.86265e-09],[&amp;quot;+2^-45&amp;quot;,6.31089e-30,1.86265e-09],[&amp;quot;+2^-44&amp;quot;,1.26218e-29,1.86265e-09],[&amp;quot;+2^-43&amp;quot;,2.52435e-29,1.86265e-09],[&amp;quot;+2^-42&amp;quot;,5.04871e-29,1.86265e-09],[&amp;quot;+2^-41&amp;quot;,1.00974e-28,1.86265e-09],[&amp;quot;+2^-40&amp;quot;,2.01948e-28,1.86265e-09],[&amp;quot;+2^-39&amp;quot;,4.03897e-28,1.86265e-09],[&amp;quot;+2^-38&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-37&amp;quot;,1.61559e-27,1.86265e-09],[&amp;quot;+2^-36&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-35&amp;quot;,6.46235e-27,1.86265e-09],[&amp;quot;+2^-34&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-33&amp;quot;,2.58494e-26,1.86265e-09],[&amp;quot;+2^-32&amp;quot;,5.16988e-26,1.86265e-09],[&amp;quot;+2^-31&amp;quot;,1.03398e-25,1.86265e-09],[&amp;quot;+2^-30&amp;quot;,2.06795e-25,1.86265e-09],[&amp;quot;+2^-29&amp;quot;,4.1359e-25,1.86265e-09],[&amp;quot;+2^-28&amp;quot;,8.27181e-25,1.86265e-09],[&amp;quot;+2^-27&amp;quot;,1.65436e-24,1.86265e-09],[&amp;quot;+2^-26&amp;quot;,9.92617e-24,5.58794e-09],[&amp;quot;+2^-25&amp;quot;,3.55271e-15,1],[&amp;quot;+2^-24&amp;quot;,7.10543e-15,1],[&amp;quot;+2^-23&amp;quot;,1.42109e-14,1],[&amp;quot;+2^-22&amp;quot;,2.84217e-14,1],[&amp;quot;+2^-21&amp;quot;,5.68437e-14,1],[&amp;quot;+2^-20&amp;quot;,1.13689e-13,1.00002],[&amp;quot;+2^-19&amp;quot;,2.27391e-13,1.00008],[&amp;quot;+2^-18&amp;quot;,4.54892e-13,1.00032],[&amp;quot;+2^-17&amp;quot;,9.10665e-13,1.00129],[&amp;quot;+2^-16&amp;quot;,1.82841e-12,1.00518],[&amp;quot;+2^-15&amp;quot;,3.71353e-12,1.02077],[&amp;quot;+2^-14&amp;quot;,1.39798e-11,1.92136],[&amp;quot;+2^-13&amp;quot;,2.63239e-11,1.80896],[&amp;quot;+2^-12&amp;quot;,5.37712e-11,1.84757],[&amp;quot;+2^-11&amp;quot;,1.09932e-10,1.88861],[&amp;quot;+2^-10&amp;quot;,2.24787e-10,1.93091],[&amp;quot;+2^-9&amp;quot;,4.3452e-10,1.86625],[&amp;quot;+2^-8&amp;quot;,1.01783e-09,2.18576],[&amp;quot;+2^-7&amp;quot;,2.10562e-09,2.26089],[&amp;quot;+2^-6&amp;quot;,4.03954e-09,2.16871],[&amp;quot;+2^-5&amp;quot;,8.15746e-09,2.18975],[&amp;quot;+2^-4&amp;quot;,1.5545e-08,2.08641],[&amp;quot;+2^-3&amp;quot;,2.62153e-08,2.05864],[&amp;quot;+2^-2&amp;quot;,4.09295e-08,1.73804],[&amp;quot;+2^-1&amp;quot;,8.02409e-08,1.34622],[&amp;quot;+2^0&amp;quot;,8.66201e-08,1.45324],[&amp;quot;+2^1&amp;quot;,8.8654e-08,1.48737],[&amp;quot;+2^2&amp;quot;,6.7976e-08,1.14045],[&amp;quot;+2^3&amp;quot;,7.44117e-08,1.24842],[&amp;quot;+2^4&amp;quot;,2.53131e-14,4.24683e-07],[&amp;quot;+2^5&amp;quot;,0,null],[&amp;quot;+2^6&amp;quot;,0,null],[&amp;quot;+2^7&amp;quot;,0,null],[&amp;quot;+2^8&amp;quot;,0,null],[&amp;quot;+2^9&amp;quot;,0,null],[&amp;quot;+2^10&amp;quot;,0,null],[&amp;quot;+2^11&amp;quot;,0,null],[&amp;quot;+2^12&amp;quot;,0,null],[&amp;quot;+2^13&amp;quot;,0,null],[&amp;quot;+2^14&amp;quot;,0,null],[&amp;quot;+2^15&amp;quot;,0,null],[&amp;quot;+2^16&amp;quot;,0,null],[&amp;quot;+2^17&amp;quot;,0,null],[&amp;quot;+2^18&amp;quot;,0,null],[&amp;quot;+2^19&amp;quot;,0,null],[&amp;quot;+2^20&amp;quot;,0,null],[&amp;quot;+2^21&amp;quot;,0,null],[&amp;quot;+2^22&amp;quot;,0,null],[&amp;quot;+2^23&amp;quot;,0,null],[&amp;quot;+2^24&amp;quot;,0,null],[&amp;quot;+2^25&amp;quot;,0,null],[&amp;quot;+2^26&amp;quot;,0,null],[&amp;quot;+2^27&amp;quot;,0,null],[&amp;quot;+2^28&amp;quot;,0,null],[&amp;quot;+2^29&amp;quot;,0,null],[&amp;quot;+2^30&amp;quot;,0,null],[&amp;quot;+2^31&amp;quot;,0,null],[&amp;quot;+2^32&amp;quot;,0,null],[&amp;quot;+2^33&amp;quot;,0,null],[&amp;quot;+2^34&amp;quot;,0,null],[&amp;quot;+2^35&amp;quot;,0,null],[&amp;quot;+2^36&amp;quot;,0,null],[&amp;quot;+2^37&amp;quot;,0,null],[&amp;quot;+2^38&amp;quot;,0,null],[&amp;quot;+2^39&amp;quot;,0,null],[&amp;quot;+2^40&amp;quot;,0,null],[&amp;quot;+2^41&amp;quot;,0,null],[&amp;quot;+2^42&amp;quot;,0,null],[&amp;quot;+2^43&amp;quot;,0,null],[&amp;quot;+2^44&amp;quot;,0,null],[&amp;quot;+2^45&amp;quot;,0,null],[&amp;quot;+2^46&amp;quot;,0,null],[&amp;quot;+2^47&amp;quot;,0,null],[&amp;quot;+2^48&amp;quot;,0,null],[&amp;quot;+2^49&amp;quot;,0,null],[&amp;quot;+2^50&amp;quot;,0,null],[&amp;quot;+2^51&amp;quot;,0,null],[&amp;quot;+2^52&amp;quot;,0,null],[&amp;quot;+2^53&amp;quot;,0,null],[&amp;quot;+2^54&amp;quot;,0,null],[&amp;quot;+2^55&amp;quot;,0,null],[&amp;quot;+2^56&amp;quot;,0,null],[&amp;quot;+2^57&amp;quot;,0,null],[&amp;quot;+2^58&amp;quot;,0,null],[&amp;quot;+2^59&amp;quot;,0,null],[&amp;quot;+2^60&amp;quot;,0,null],[&amp;quot;+2^61&amp;quot;,0,null],[&amp;quot;+2^62&amp;quot;,0,null],[&amp;quot;+2^63&amp;quot;,0,null],[&amp;quot;+2^64&amp;quot;,0,null],[&amp;quot;+2^65&amp;quot;,0,null],[&amp;quot;+2^66&amp;quot;,0,null],[&amp;quot;+2^67&amp;quot;,0,null],[&amp;quot;+2^68&amp;quot;,0,null],[&amp;quot;+2^69&amp;quot;,0,null],[&amp;quot;+2^70&amp;quot;,0,null],[&amp;quot;+2^71&amp;quot;,0,null],[&amp;quot;+2^72&amp;quot;,0,null],[&amp;quot;+2^73&amp;quot;,0,null],[&amp;quot;+2^74&amp;quot;,0,null],[&amp;quot;+2^75&amp;quot;,0,null],[&amp;quot;+2^76&amp;quot;,0,null],[&amp;quot;+2^77&amp;quot;,0,null],[&amp;quot;+2^78&amp;quot;,0,null],[&amp;quot;+2^79&amp;quot;,0,null],[&amp;quot;+2^80&amp;quot;,0,null],[&amp;quot;+2^81&amp;quot;,0,null],[&amp;quot;+2^82&amp;quot;,0,null],[&amp;quot;+2^83&amp;quot;,0,null],[&amp;quot;+2^84&amp;quot;,0,null],[&amp;quot;+2^85&amp;quot;,0,null],[&amp;quot;+2^86&amp;quot;,0,null],[&amp;quot;+2^87&amp;quot;,0,null],[&amp;quot;+2^88&amp;quot;,0,null],[&amp;quot;+2^89&amp;quot;,0,null],[&amp;quot;+2^90&amp;quot;,0,null],[&amp;quot;+2^91&amp;quot;,0,null],[&amp;quot;+2^92&amp;quot;,0,null],[&amp;quot;+2^93&amp;quot;,0,null],[&amp;quot;+2^94&amp;quot;,0,null],[&amp;quot;+2^95&amp;quot;,0,null],[&amp;quot;+2^96&amp;quot;,0,null],[&amp;quot;+2^97&amp;quot;,0,null],[&amp;quot;+2^98&amp;quot;,0,null],[&amp;quot;+2^99&amp;quot;,0,null],[&amp;quot;+2^100&amp;quot;,0,null],[&amp;quot;+2^101&amp;quot;,0,null],[&amp;quot;+2^102&amp;quot;,0,null],[&amp;quot;+2^103&amp;quot;,0,null],[&amp;quot;+2^104&amp;quot;,0,null],[&amp;quot;+2^105&amp;quot;,0,null],[&amp;quot;+2^106&amp;quot;,0,null],[&amp;quot;+2^107&amp;quot;,0,null],[&amp;quot;+2^108&amp;quot;,0,null],[&amp;quot;+2^109&amp;quot;,0,null],[&amp;quot;+2^110&amp;quot;,0,null],[&amp;quot;+2^111&amp;quot;,0,null],[&amp;quot;+2^112&amp;quot;,0,null],[&amp;quot;+2^113&amp;quot;,0,null],[&amp;quot;+2^114&amp;quot;,0,null],[&amp;quot;+2^115&amp;quot;,0,null],[&amp;quot;+2^116&amp;quot;,0,null],[&amp;quot;+2^117&amp;quot;,0,null],[&amp;quot;+2^118&amp;quot;,0,null],[&amp;quot;+2^119&amp;quot;,0,null],[&amp;quot;+2^120&amp;quot;,0,null],[&amp;quot;+2^121&amp;quot;,0,null],[&amp;quot;+2^122&amp;quot;,0,null],[&amp;quot;+2^123&amp;quot;,0,null],[&amp;quot;+2^124&amp;quot;,0,null],[&amp;quot;+2^125&amp;quot;,0,null],[&amp;quot;+2^126&amp;quot;,0,null],[&amp;quot;+2^127&amp;quot;,0,null] ]&lt;br /&gt;
 }}&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Key observation:&#039;&#039;&#039;  &lt;br /&gt;
Compared to direct exponential formulations, the &amp;lt;code&amp;gt;expm1&amp;lt;/code&amp;gt; approach maintains low ULP error for small and medium values of &amp;lt;math&amp;gt;x&amp;lt;/math&amp;gt;. &lt;br /&gt;
Overflow is still present for very large inputs, but the overall error behavior is substantially more stable.&lt;br /&gt;
&lt;br /&gt;
== Numerical Issues and Error Metrics ==&lt;br /&gt;
&lt;br /&gt;
To evaluate and compare different software implementations of &amp;lt;code&amp;gt;tanh&amp;lt;/code&amp;gt;, several numerical error metrics are used throughout this article. These metrics make it possible to reason about accuracy, robustness, and failure modes in floating-point arithmetic.&lt;br /&gt;
&lt;br /&gt;
=== ULP Error ===&lt;br /&gt;
ULP (Unit in the Last Place) measures the distance between two adjacent representable floating-point numbers. The ULP error of a computed result is defined as the difference, measured in ULPs, between the computed value and the correctly rounded reference value.&lt;br /&gt;
&lt;br /&gt;
An error of &amp;lt;code&amp;gt;ULP ≤ 2&amp;lt;/code&amp;gt; is commonly considered acceptable for single-precision transcendental functions, as it indicates that the result is very close to the correctly rounded value.&lt;br /&gt;
&lt;br /&gt;
Large ULP errors typically indicate catastrophic cancellation, loss of significant bits, or severe rounding effects.&lt;br /&gt;
&lt;br /&gt;
=== NaN and Overflow ===&lt;br /&gt;
NaN (Not a Number) results occur when undefined floating-point operations are performed, such as &amp;lt;code&amp;gt;inf / inf&amp;lt;/code&amp;gt;. In &amp;lt;code&amp;gt;tanh&amp;lt;/code&amp;gt; implementations, this often happens when both the numerator and denominator of a formulation overflow to infinity.&lt;br /&gt;
&lt;br /&gt;
Tracking NaN generation is critical, as NaNs can silently propagate through numerical pipelines and invalidate downstream results.&lt;br /&gt;
&lt;br /&gt;
=== Error Graphs ===&lt;br /&gt;
The error graphs shown in the following sections visualize ULP error across the full &amp;lt;code&amp;gt;float32&amp;lt;/code&amp;gt; input domain. They make it possible to identify:&lt;br /&gt;
* regions with catastrophic cancellation,&lt;br /&gt;
* input ranges that trigger overflow or NaNs,&lt;br /&gt;
* and numerical stability differences between formulations.&lt;br /&gt;
&lt;br /&gt;
These graphs are a key tool for understanding not only the maximum error, but also how errors are distributed across different input magnitudes.&lt;br /&gt;
&lt;br /&gt;
==Taylor==&lt;br /&gt;
&lt;br /&gt;
The following graph shows the numerical error of a Taylor series approximation of &amp;lt;code&amp;gt;tanh(x)&amp;lt;/code&amp;gt;.&lt;br /&gt;
Taylor expansions approximate the function locally around &amp;lt;math&amp;gt;x = 0&amp;lt;/math&amp;gt; and are therefore only meaningful for very small input magnitudes.&lt;br /&gt;
&lt;br /&gt;
{{#widget:FloatErrorGraph|id=taylor&lt;br /&gt;
  |data=[ [&amp;quot;-2^127&amp;quot;,0,0],[&amp;quot;-2^126&amp;quot;,0,0],[&amp;quot;-2^125&amp;quot;,0,0],[&amp;quot;-2^124&amp;quot;,0,0],[&amp;quot;-2^123&amp;quot;,0,0],[&amp;quot;-2^122&amp;quot;,0,0],[&amp;quot;-2^121&amp;quot;,0,0],[&amp;quot;-2^120&amp;quot;,0,0],[&amp;quot;-2^119&amp;quot;,0,0],[&amp;quot;-2^118&amp;quot;,0,0],[&amp;quot;-2^117&amp;quot;,0,0],[&amp;quot;-2^116&amp;quot;,0,0],[&amp;quot;-2^115&amp;quot;,0,0],[&amp;quot;-2^114&amp;quot;,0,0],[&amp;quot;-2^113&amp;quot;,0,0],[&amp;quot;-2^112&amp;quot;,0,0],[&amp;quot;-2^111&amp;quot;,0,0],[&amp;quot;-2^110&amp;quot;,0,0],[&amp;quot;-2^109&amp;quot;,0,0],[&amp;quot;-2^108&amp;quot;,0,0],[&amp;quot;-2^107&amp;quot;,0,0],[&amp;quot;-2^106&amp;quot;,0,0],[&amp;quot;-2^105&amp;quot;,0,0],[&amp;quot;-2^104&amp;quot;,0,0],[&amp;quot;-2^103&amp;quot;,0,0],[&amp;quot;-2^102&amp;quot;,0,0],[&amp;quot;-2^101&amp;quot;,0,0],[&amp;quot;-2^100&amp;quot;,0,0],[&amp;quot;-2^99&amp;quot;,0,0],[&amp;quot;-2^98&amp;quot;,0,0],[&amp;quot;-2^97&amp;quot;,0,0],[&amp;quot;-2^96&amp;quot;,0,0],[&amp;quot;-2^95&amp;quot;,0,0],[&amp;quot;-2^94&amp;quot;,0,0],[&amp;quot;-2^93&amp;quot;,0,0],[&amp;quot;-2^92&amp;quot;,0,0],[&amp;quot;-2^91&amp;quot;,0,0],[&amp;quot;-2^90&amp;quot;,0,0],[&amp;quot;-2^89&amp;quot;,0,0],[&amp;quot;-2^88&amp;quot;,0,0],[&amp;quot;-2^87&amp;quot;,0,0],[&amp;quot;-2^86&amp;quot;,0,0],[&amp;quot;-2^85&amp;quot;,0,0],[&amp;quot;-2^84&amp;quot;,0,0],[&amp;quot;-2^83&amp;quot;,0,0],[&amp;quot;-2^82&amp;quot;,0,0],[&amp;quot;-2^81&amp;quot;,0,0],[&amp;quot;-2^80&amp;quot;,0,0],[&amp;quot;-2^79&amp;quot;,0,0],[&amp;quot;-2^78&amp;quot;,0,0],[&amp;quot;-2^77&amp;quot;,0,0],[&amp;quot;-2^76&amp;quot;,0,0],[&amp;quot;-2^75&amp;quot;,0,0],[&amp;quot;-2^74&amp;quot;,0,0],[&amp;quot;-2^73&amp;quot;,0,0],[&amp;quot;-2^72&amp;quot;,0,0],[&amp;quot;-2^71&amp;quot;,0,0],[&amp;quot;-2^70&amp;quot;,0,0],[&amp;quot;-2^69&amp;quot;,0,0],[&amp;quot;-2^68&amp;quot;,0,0],[&amp;quot;-2^67&amp;quot;,0,0],[&amp;quot;-2^66&amp;quot;,0,0],[&amp;quot;-2^65&amp;quot;,0,0],[&amp;quot;-2^64&amp;quot;,0,0],[&amp;quot;-2^63&amp;quot;,1.84467e+19,3.09485e+26],[&amp;quot;-2^62&amp;quot;,9.22336e+18,1.54742e+26],[&amp;quot;-2^61&amp;quot;,4.61168e+18,7.73712e+25],[&amp;quot;-2^60&amp;quot;,2.30584e+18,3.86856e+25],[&amp;quot;-2^59&amp;quot;,1.15292e+18,1.93428e+25],[&amp;quot;-2^58&amp;quot;,5.7646e+17,9.6714e+24],[&amp;quot;-2^57&amp;quot;,2.8823e+17,4.8357e+24],[&amp;quot;-2^56&amp;quot;,1.44115e+17,2.41785e+24],[&amp;quot;-2^55&amp;quot;,7.20575e+16,1.20892e+24],[&amp;quot;-2^54&amp;quot;,3.60288e+16,6.04462e+23],[&amp;quot;-2^53&amp;quot;,1.80144e+16,3.02231e+23],[&amp;quot;-2^52&amp;quot;,9.00719e+15,1.51116e+23],[&amp;quot;-2^51&amp;quot;,4.5036e+15,7.55578e+22],[&amp;quot;-2^50&amp;quot;,2.2518e+15,3.77789e+22],[&amp;quot;-2^49&amp;quot;,1.1259e+15,1.88894e+22],[&amp;quot;-2^48&amp;quot;,5.62949e+14,9.44472e+21],[&amp;quot;-2^47&amp;quot;,2.81475e+14,4.72236e+21],[&amp;quot;-2^46&amp;quot;,1.40737e+14,2.36118e+21],[&amp;quot;-2^45&amp;quot;,7.03687e+13,1.18059e+21],[&amp;quot;-2^44&amp;quot;,3.51843e+13,5.90295e+20],[&amp;quot;-2^43&amp;quot;,1.75922e+13,2.95148e+20],[&amp;quot;-2^42&amp;quot;,8.79608e+12,1.47574e+20],[&amp;quot;-2^41&amp;quot;,4.39804e+12,7.37869e+19],[&amp;quot;-2^40&amp;quot;,2.19902e+12,3.68935e+19],[&amp;quot;-2^39&amp;quot;,1.09951e+12,1.84467e+19],[&amp;quot;-2^38&amp;quot;,5.49755e+11,9.22336e+18],[&amp;quot;-2^37&amp;quot;,2.74878e+11,4.61168e+18],[&amp;quot;-2^36&amp;quot;,1.37439e+11,2.30584e+18],[&amp;quot;-2^35&amp;quot;,6.87194e+10,1.15292e+18],[&amp;quot;-2^34&amp;quot;,3.43597e+10,5.7646e+17],[&amp;quot;-2^33&amp;quot;,1.71799e+10,2.8823e+17],[&amp;quot;-2^32&amp;quot;,8.58993e+09,1.44115e+17],[&amp;quot;-2^31&amp;quot;,4.29085e+09,7.19885e+16],[&amp;quot;-2^30&amp;quot;,2.14542e+09,3.59943e+16],[&amp;quot;-2^29&amp;quot;,1.07271e+09,1.79971e+16],[&amp;quot;-2^28&amp;quot;,5.36356e+08,8.99856e+15],[&amp;quot;-2^27&amp;quot;,2.68178e+08,4.49928e+15],[&amp;quot;-2^26&amp;quot;,1.34089e+08,2.24964e+15],[&amp;quot;-2^25&amp;quot;,6.70445e+07,1.12482e+15],[&amp;quot;-2^24&amp;quot;,3.35223e+07,5.6241e+14],[&amp;quot;-2^23&amp;quot;,1.67611e+07,2.81205e+14],[&amp;quot;-2^22&amp;quot;,8.38056e+06,1.40603e+14],[&amp;quot;-2^21&amp;quot;,4.19028e+06,7.03013e+13],[&amp;quot;-2^20&amp;quot;,2.09514e+06,3.51506e+13],[&amp;quot;-2^19&amp;quot;,1.04757e+06,1.75753e+13],[&amp;quot;-2^18&amp;quot;,523784,8.78764e+12],[&amp;quot;-2^17&amp;quot;,261892,4.39381e+12],[&amp;quot;-2^16&amp;quot;,130945,2.1969e+12],[&amp;quot;-2^15&amp;quot;,65472.2,1.09844e+12],[&amp;quot;-2^14&amp;quot;,32735.6,5.49212e+11],[&amp;quot;-2^13&amp;quot;,16367.3,2.74598e+11],[&amp;quot;-2^12&amp;quot;,8183.15,1.3729e+11],[&amp;quot;-2^11&amp;quot;,4091.07,6.86368e+10],[&amp;quot;-2^10&amp;quot;,2045.04,3.431e+10],[&amp;quot;-2^9&amp;quot;,1022.02,1.71466e+10],[&amp;quot;-2^8&amp;quot;,510.509,8.56492e+09],[&amp;quot;-2^7&amp;quot;,254.754,4.27406e+09],[&amp;quot;-2^6&amp;quot;,126.876,2.12862e+09],[&amp;quot;-2^5&amp;quot;,62.9354,1.05588e+09],[&amp;quot;-2^4&amp;quot;,30.9629,5.19471e+08],[&amp;quot;-2^3&amp;quot;,14.9718,2.51185e+08],[&amp;quot;-2^2&amp;quot;,6.96672,1.16882e+08],[&amp;quot;-2^1&amp;quot;,2.94633,4.94312e+07],[&amp;quot;-2^0&amp;quot;,0.940715,1.57826e+07],[&amp;quot;-2^-1&amp;quot;,0.127635,2.14135e+06],[&amp;quot;-2^-2&amp;quot;,0.00318735,106950],[&amp;quot;-2^-3&amp;quot;,3.11475e-05,2090.28],[&amp;quot;-2^-4&amp;quot;,2.57737e-07,34.5929],[&amp;quot;-2^-5&amp;quot;,3.89213e-09,1.04478],[&amp;quot;-2^-6&amp;quot;,9.51472e-10,0.510818],[&amp;quot;-2^-7&amp;quot;,4.66402e-10,0.500795],[&amp;quot;-2^-8&amp;quot;,2.32917e-10,0.500185],[&amp;quot;-2^-9&amp;quot;,1.16423e-10,0.500033],[&amp;quot;-2^-10&amp;quot;,5.82082e-11,0.500004],[&amp;quot;-2^-11&amp;quot;,2.91037e-11,0.499998],[&amp;quot;-2^-12&amp;quot;,1.45519e-11,0.5],[&amp;quot;-2^-13&amp;quot;,4.85062e-12,0.333332],[&amp;quot;-2^-14&amp;quot;,6.06328e-13,0.0833331],[&amp;quot;-2^-15&amp;quot;,7.5791e-14,0.0208333],[&amp;quot;-2^-16&amp;quot;,9.47387e-15,0.00520832],[&amp;quot;-2^-17&amp;quot;,1.18424e-15,0.00130208],[&amp;quot;-2^-18&amp;quot;,1.48029e-16,0.00032552],[&amp;quot;-2^-19&amp;quot;,1.85039e-17,8.13808e-05],[&amp;quot;-2^-20&amp;quot;,2.31304e-18,2.03457e-05],[&amp;quot;-2^-21&amp;quot;,2.89156e-19,5.08688e-06],[&amp;quot;-2^-22&amp;quot;,3.61577e-20,1.27219e-06],[&amp;quot;-2^-23&amp;quot;,4.52633e-21,3.18512e-07],[&amp;quot;-2^-24&amp;quot;,5.691e-22,8.00937e-08],[&amp;quot;-2^-25&amp;quot;,7.27919e-23,2.04891e-08],[&amp;quot;-2^-26&amp;quot;,9.92617e-24,5.58794e-09],[&amp;quot;-2^-27&amp;quot;,1.65436e-24,1.86265e-09],[&amp;quot;-2^-28&amp;quot;,8.27181e-25,1.86265e-09],[&amp;quot;-2^-29&amp;quot;,4.1359e-25,1.86265e-09],[&amp;quot;-2^-30&amp;quot;,2.06795e-25,1.86265e-09],[&amp;quot;-2^-31&amp;quot;,1.03398e-25,1.86265e-09],[&amp;quot;-2^-32&amp;quot;,5.16988e-26,1.86265e-09],[&amp;quot;-2^-33&amp;quot;,2.58494e-26,1.86265e-09],[&amp;quot;-2^-34&amp;quot;,1.29247e-26,1.86265e-09],[&amp;quot;-2^-35&amp;quot;,6.46235e-27,1.86265e-09],[&amp;quot;-2^-36&amp;quot;,3.23117e-27,1.86265e-09],[&amp;quot;-2^-37&amp;quot;,1.61559e-27,1.86265e-09],[&amp;quot;-2^-38&amp;quot;,8.07794e-28,1.86265e-09],[&amp;quot;-2^-39&amp;quot;,4.03897e-28,1.86265e-09],[&amp;quot;-2^-40&amp;quot;,2.01948e-28,1.86265e-09],[&amp;quot;-2^-41&amp;quot;,1.00974e-28,1.86265e-09],[&amp;quot;-2^-42&amp;quot;,5.04871e-29,1.86265e-09],[&amp;quot;-2^-43&amp;quot;,2.52435e-29,1.86265e-09],[&amp;quot;-2^-44&amp;quot;,1.26218e-29,1.86265e-09],[&amp;quot;-2^-45&amp;quot;,6.31089e-30,1.86265e-09],[&amp;quot;-2^-46&amp;quot;,3.15544e-30,1.86265e-09],[&amp;quot;-2^-47&amp;quot;,1.57772e-30,1.86265e-09],[&amp;quot;-2^-48&amp;quot;,7.88861e-31,1.86265e-09],[&amp;quot;-2^-49&amp;quot;,3.9443e-31,1.86265e-09],[&amp;quot;-2^-50&amp;quot;,1.97215e-31,1.86265e-09],[&amp;quot;-2^-51&amp;quot;,9.86076e-32,1.86265e-09],[&amp;quot;-2^-52&amp;quot;,4.93038e-32,1.86265e-09],[&amp;quot;-2^-53&amp;quot;,2.46519e-32,1.86265e-09],[&amp;quot;-2^-54&amp;quot;,1.2326e-32,1.86265e-09],[&amp;quot;-2^-55&amp;quot;,0,0],[&amp;quot;-2^-56&amp;quot;,0,0],[&amp;quot;-2^-57&amp;quot;,0,0],[&amp;quot;-2^-58&amp;quot;,0,0],[&amp;quot;-2^-59&amp;quot;,0,0],[&amp;quot;-2^-60&amp;quot;,0,0],[&amp;quot;-2^-61&amp;quot;,0,0],[&amp;quot;-2^-62&amp;quot;,0,0],[&amp;quot;-2^-63&amp;quot;,0,0],[&amp;quot;-2^-64&amp;quot;,0,0],[&amp;quot;-2^-65&amp;quot;,0,0],[&amp;quot;-2^-66&amp;quot;,0,0],[&amp;quot;-2^-67&amp;quot;,0,0],[&amp;quot;-2^-68&amp;quot;,0,0],[&amp;quot;-2^-69&amp;quot;,0,0],[&amp;quot;-2^-70&amp;quot;,0,0],[&amp;quot;-2^-71&amp;quot;,0,0],[&amp;quot;-2^-72&amp;quot;,0,0],[&amp;quot;-2^-73&amp;quot;,0,0],[&amp;quot;-2^-74&amp;quot;,0,0],[&amp;quot;-2^-75&amp;quot;,0,0],[&amp;quot;-2^-76&amp;quot;,0,0],[&amp;quot;-2^-77&amp;quot;,0,0],[&amp;quot;-2^-78&amp;quot;,0,0],[&amp;quot;-2^-79&amp;quot;,0,0],[&amp;quot;-2^-80&amp;quot;,0,0],[&amp;quot;-2^-81&amp;quot;,0,0],[&amp;quot;-2^-82&amp;quot;,0,0],[&amp;quot;-2^-83&amp;quot;,0,0],[&amp;quot;-2^-84&amp;quot;,0,0],[&amp;quot;-2^-85&amp;quot;,0,0],[&amp;quot;-2^-86&amp;quot;,0,0],[&amp;quot;-2^-87&amp;quot;,0,0],[&amp;quot;-2^-88&amp;quot;,0,0],[&amp;quot;-2^-89&amp;quot;,0,0],[&amp;quot;-2^-90&amp;quot;,0,0],[&amp;quot;-2^-91&amp;quot;,0,0],[&amp;quot;-2^-92&amp;quot;,0,0],[&amp;quot;-2^-93&amp;quot;,0,0],[&amp;quot;-2^-94&amp;quot;,0,0],[&amp;quot;-2^-95&amp;quot;,0,0],[&amp;quot;-2^-96&amp;quot;,0,0],[&amp;quot;-2^-97&amp;quot;,0,0],[&amp;quot;-2^-98&amp;quot;,0,0],[&amp;quot;-2^-99&amp;quot;,0,0],[&amp;quot;-2^-100&amp;quot;,0,0],[&amp;quot;-2^-101&amp;quot;,0,0],[&amp;quot;-2^-102&amp;quot;,0,0],[&amp;quot;-2^-103&amp;quot;,0,0],[&amp;quot;-2^-104&amp;quot;,0,0],[&amp;quot;-2^-105&amp;quot;,0,0],[&amp;quot;-2^-106&amp;quot;,0,0],[&amp;quot;-2^-107&amp;quot;,0,0],[&amp;quot;-2^-108&amp;quot;,0,0],[&amp;quot;-2^-109&amp;quot;,0,0],[&amp;quot;-2^-110&amp;quot;,0,0],[&amp;quot;-2^-111&amp;quot;,0,0],[&amp;quot;-2^-112&amp;quot;,0,0],[&amp;quot;-2^-113&amp;quot;,0,0],[&amp;quot;-2^-114&amp;quot;,0,0],[&amp;quot;-2^-115&amp;quot;,0,0],[&amp;quot;-2^-116&amp;quot;,0,0],[&amp;quot;-2^-117&amp;quot;,0,0],[&amp;quot;-2^-118&amp;quot;,0,0],[&amp;quot;-2^-119&amp;quot;,0,0],[&amp;quot;-2^-120&amp;quot;,0,0],[&amp;quot;-2^-121&amp;quot;,0,0],[&amp;quot;-2^-122&amp;quot;,0,0],[&amp;quot;-2^-123&amp;quot;,0,0],[&amp;quot;-2^-124&amp;quot;,0,0],[&amp;quot;-2^-125&amp;quot;,0,0],[&amp;quot;-2^-126&amp;quot;,0,0],[&amp;quot;-SN&amp;quot;,0,0],[&amp;quot;0&amp;quot;,0,0],[&amp;quot;+SN&amp;quot;,0,0],[&amp;quot;+2^-126&amp;quot;,0,0],[&amp;quot;+2^-125&amp;quot;,0,0],[&amp;quot;+2^-124&amp;quot;,0,0],[&amp;quot;+2^-123&amp;quot;,0,0],[&amp;quot;+2^-122&amp;quot;,0,0],[&amp;quot;+2^-121&amp;quot;,0,0],[&amp;quot;+2^-120&amp;quot;,0,0],[&amp;quot;+2^-119&amp;quot;,0,0],[&amp;quot;+2^-118&amp;quot;,0,0],[&amp;quot;+2^-117&amp;quot;,0,0],[&amp;quot;+2^-116&amp;quot;,0,0],[&amp;quot;+2^-115&amp;quot;,0,0],[&amp;quot;+2^-114&amp;quot;,0,0],[&amp;quot;+2^-113&amp;quot;,0,0],[&amp;quot;+2^-112&amp;quot;,0,0],[&amp;quot;+2^-111&amp;quot;,0,0],[&amp;quot;+2^-110&amp;quot;,0,0],[&amp;quot;+2^-109&amp;quot;,0,0],[&amp;quot;+2^-108&amp;quot;,0,0],[&amp;quot;+2^-107&amp;quot;,0,0],[&amp;quot;+2^-106&amp;quot;,0,0],[&amp;quot;+2^-105&amp;quot;,0,0],[&amp;quot;+2^-104&amp;quot;,0,0],[&amp;quot;+2^-103&amp;quot;,0,0],[&amp;quot;+2^-102&amp;quot;,0,0],[&amp;quot;+2^-101&amp;quot;,0,0],[&amp;quot;+2^-100&amp;quot;,0,0],[&amp;quot;+2^-99&amp;quot;,0,0],[&amp;quot;+2^-98&amp;quot;,0,0],[&amp;quot;+2^-97&amp;quot;,0,0],[&amp;quot;+2^-96&amp;quot;,0,0],[&amp;quot;+2^-95&amp;quot;,0,0],[&amp;quot;+2^-94&amp;quot;,0,0],[&amp;quot;+2^-93&amp;quot;,0,0],[&amp;quot;+2^-92&amp;quot;,0,0],[&amp;quot;+2^-91&amp;quot;,0,0],[&amp;quot;+2^-90&amp;quot;,0,0],[&amp;quot;+2^-89&amp;quot;,0,0],[&amp;quot;+2^-88&amp;quot;,0,0],[&amp;quot;+2^-87&amp;quot;,0,0],[&amp;quot;+2^-86&amp;quot;,0,0],[&amp;quot;+2^-85&amp;quot;,0,0],[&amp;quot;+2^-84&amp;quot;,0,0],[&amp;quot;+2^-83&amp;quot;,0,0],[&amp;quot;+2^-82&amp;quot;,0,0],[&amp;quot;+2^-81&amp;quot;,0,0],[&amp;quot;+2^-80&amp;quot;,0,0],[&amp;quot;+2^-79&amp;quot;,0,0],[&amp;quot;+2^-78&amp;quot;,0,0],[&amp;quot;+2^-77&amp;quot;,0,0],[&amp;quot;+2^-76&amp;quot;,0,0],[&amp;quot;+2^-75&amp;quot;,0,0],[&amp;quot;+2^-74&amp;quot;,0,0],[&amp;quot;+2^-73&amp;quot;,0,0],[&amp;quot;+2^-72&amp;quot;,0,0],[&amp;quot;+2^-71&amp;quot;,0,0],[&amp;quot;+2^-70&amp;quot;,0,0],[&amp;quot;+2^-69&amp;quot;,0,0],[&amp;quot;+2^-68&amp;quot;,0,0],[&amp;quot;+2^-67&amp;quot;,0,0],[&amp;quot;+2^-66&amp;quot;,0,0],[&amp;quot;+2^-65&amp;quot;,0,0],[&amp;quot;+2^-64&amp;quot;,0,0],[&amp;quot;+2^-63&amp;quot;,0,0],[&amp;quot;+2^-62&amp;quot;,0,0],[&amp;quot;+2^-61&amp;quot;,0,0],[&amp;quot;+2^-60&amp;quot;,0,0],[&amp;quot;+2^-59&amp;quot;,0,0],[&amp;quot;+2^-58&amp;quot;,0,0],[&amp;quot;+2^-57&amp;quot;,0,0],[&amp;quot;+2^-56&amp;quot;,0,0],[&amp;quot;+2^-55&amp;quot;,0,0],[&amp;quot;+2^-54&amp;quot;,1.2326e-32,1.86265e-09],[&amp;quot;+2^-53&amp;quot;,2.46519e-32,1.86265e-09],[&amp;quot;+2^-52&amp;quot;,4.93038e-32,1.86265e-09],[&amp;quot;+2^-51&amp;quot;,9.86076e-32,1.86265e-09],[&amp;quot;+2^-50&amp;quot;,1.97215e-31,1.86265e-09],[&amp;quot;+2^-49&amp;quot;,3.9443e-31,1.86265e-09],[&amp;quot;+2^-48&amp;quot;,7.88861e-31,1.86265e-09],[&amp;quot;+2^-47&amp;quot;,1.57772e-30,1.86265e-09],[&amp;quot;+2^-46&amp;quot;,3.15544e-30,1.86265e-09],[&amp;quot;+2^-45&amp;quot;,6.31089e-30,1.86265e-09],[&amp;quot;+2^-44&amp;quot;,1.26218e-29,1.86265e-09],[&amp;quot;+2^-43&amp;quot;,2.52435e-29,1.86265e-09],[&amp;quot;+2^-42&amp;quot;,5.04871e-29,1.86265e-09],[&amp;quot;+2^-41&amp;quot;,1.00974e-28,1.86265e-09],[&amp;quot;+2^-40&amp;quot;,2.01948e-28,1.86265e-09],[&amp;quot;+2^-39&amp;quot;,4.03897e-28,1.86265e-09],[&amp;quot;+2^-38&amp;quot;,8.07794e-28,1.86265e-09],[&amp;quot;+2^-37&amp;quot;,1.61559e-27,1.86265e-09],[&amp;quot;+2^-36&amp;quot;,3.23117e-27,1.86265e-09],[&amp;quot;+2^-35&amp;quot;,6.46235e-27,1.86265e-09],[&amp;quot;+2^-34&amp;quot;,1.29247e-26,1.86265e-09],[&amp;quot;+2^-33&amp;quot;,2.58494e-26,1.86265e-09],[&amp;quot;+2^-32&amp;quot;,5.16988e-26,1.86265e-09],[&amp;quot;+2^-31&amp;quot;,1.03398e-25,1.86265e-09],[&amp;quot;+2^-30&amp;quot;,2.06795e-25,1.86265e-09],[&amp;quot;+2^-29&amp;quot;,4.1359e-25,1.86265e-09],[&amp;quot;+2^-28&amp;quot;,8.27181e-25,1.86265e-09],[&amp;quot;+2^-27&amp;quot;,1.65436e-24,1.86265e-09],[&amp;quot;+2^-26&amp;quot;,9.92617e-24,5.58794e-09],[&amp;quot;+2^-25&amp;quot;,7.27919e-23,2.04891e-08],[&amp;quot;+2^-24&amp;quot;,5.691e-22,8.00937e-08],[&amp;quot;+2^-23&amp;quot;,4.52633e-21,3.18512e-07],[&amp;quot;+2^-22&amp;quot;,3.61577e-20,1.27219e-06],[&amp;quot;+2^-21&amp;quot;,2.89156e-19,5.08688e-06],[&amp;quot;+2^-20&amp;quot;,2.31304e-18,2.03457e-05],[&amp;quot;+2^-19&amp;quot;,1.85039e-17,8.13808e-05],[&amp;quot;+2^-18&amp;quot;,1.48029e-16,0.00032552],[&amp;quot;+2^-17&amp;quot;,1.18424e-15,0.00130208],[&amp;quot;+2^-16&amp;quot;,9.47387e-15,0.00520832],[&amp;quot;+2^-15&amp;quot;,7.5791e-14,0.0208333],[&amp;quot;+2^-14&amp;quot;,6.06328e-13,0.0833331],[&amp;quot;+2^-13&amp;quot;,4.85062e-12,0.333332],[&amp;quot;+2^-12&amp;quot;,1.45519e-11,0.5],[&amp;quot;+2^-11&amp;quot;,2.91037e-11,0.499998],[&amp;quot;+2^-10&amp;quot;,5.82082e-11,0.500004],[&amp;quot;+2^-9&amp;quot;,1.16423e-10,0.500033],[&amp;quot;+2^-8&amp;quot;,2.32917e-10,0.500185],[&amp;quot;+2^-7&amp;quot;,4.66402e-10,0.500795],[&amp;quot;+2^-6&amp;quot;,9.51472e-10,0.510818],[&amp;quot;+2^-5&amp;quot;,3.89213e-09,1.04478],[&amp;quot;+2^-4&amp;quot;,2.57737e-07,34.5929],[&amp;quot;+2^-3&amp;quot;,3.11475e-05,2090.28],[&amp;quot;+2^-2&amp;quot;,0.00318735,106950],[&amp;quot;+2^-1&amp;quot;,0.127635,2.14135e+06],[&amp;quot;+2^0&amp;quot;,0.940715,1.57826e+07],[&amp;quot;+2^1&amp;quot;,2.94633,4.94312e+07],[&amp;quot;+2^2&amp;quot;,6.96672,1.16882e+08],[&amp;quot;+2^3&amp;quot;,14.9718,1.33874e+08],[&amp;quot;+2^4&amp;quot;,30.9629,2.59736e+08],[&amp;quot;+2^5&amp;quot;,62.9354,5.27941e+08],[&amp;quot;+2^6&amp;quot;,126.876,1.06431e+09],[&amp;quot;+2^7&amp;quot;,254.754,2.13703e+09],[&amp;quot;+2^8&amp;quot;,510.509,4.28246e+09],[&amp;quot;+2^9&amp;quot;,1022.02,8.57331e+09],[&amp;quot;+2^10&amp;quot;,2045.04,1.7155e+10],[&amp;quot;+2^11&amp;quot;,4091.07,3.43184e+10],[&amp;quot;+2^12&amp;quot;,8183.15,6.86452e+10],[&amp;quot;+2^13&amp;quot;,16367.3,1.37299e+11],[&amp;quot;+2^14&amp;quot;,32735.6,2.74606e+11],[&amp;quot;+2^15&amp;quot;,65472.2,5.4922e+11],[&amp;quot;+2^16&amp;quot;,130945,1.09845e+12],[&amp;quot;+2^17&amp;quot;,261892,2.19691e+12],[&amp;quot;+2^18&amp;quot;,523784,4.39382e+12],[&amp;quot;+2^19&amp;quot;,1.04757e+06,8.78765e+12],[&amp;quot;+2^20&amp;quot;,2.09514e+06,1.75753e+13],[&amp;quot;+2^21&amp;quot;,4.19028e+06,3.51506e+13],[&amp;quot;+2^22&amp;quot;,8.38056e+06,7.03013e+13],[&amp;quot;+2^23&amp;quot;,1.67611e+07,1.40603e+14],[&amp;quot;+2^24&amp;quot;,3.35223e+07,2.81205e+14],[&amp;quot;+2^25&amp;quot;,6.70445e+07,5.6241e+14],[&amp;quot;+2^26&amp;quot;,1.34089e+08,1.12482e+15],[&amp;quot;+2^27&amp;quot;,2.68178e+08,2.24964e+15],[&amp;quot;+2^28&amp;quot;,5.36356e+08,4.49928e+15],[&amp;quot;+2^29&amp;quot;,1.07271e+09,8.99856e+15],[&amp;quot;+2^30&amp;quot;,2.14542e+09,1.79971e+16],[&amp;quot;+2^31&amp;quot;,4.29085e+09,3.59943e+16],[&amp;quot;+2^32&amp;quot;,8.58993e+09,7.20575e+16],[&amp;quot;+2^33&amp;quot;,1.71799e+10,1.44115e+17],[&amp;quot;+2^34&amp;quot;,3.43597e+10,2.8823e+17],[&amp;quot;+2^35&amp;quot;,6.87194e+10,5.7646e+17],[&amp;quot;+2^36&amp;quot;,1.37439e+11,1.15292e+18],[&amp;quot;+2^37&amp;quot;,2.74878e+11,2.30584e+18],[&amp;quot;+2^38&amp;quot;,5.49755e+11,4.61168e+18],[&amp;quot;+2^39&amp;quot;,1.09951e+12,9.22336e+18],[&amp;quot;+2^40&amp;quot;,2.19902e+12,1.84467e+19],[&amp;quot;+2^41&amp;quot;,4.39804e+12,3.68935e+19],[&amp;quot;+2^42&amp;quot;,8.79608e+12,7.37869e+19],[&amp;quot;+2^43&amp;quot;,1.75922e+13,1.47574e+20],[&amp;quot;+2^44&amp;quot;,3.51843e+13,2.95148e+20],[&amp;quot;+2^45&amp;quot;,7.03687e+13,5.90295e+20],[&amp;quot;+2^46&amp;quot;,1.40737e+14,1.18059e+21],[&amp;quot;+2^47&amp;quot;,2.81475e+14,2.36118e+21],[&amp;quot;+2^48&amp;quot;,5.62949e+14,4.72236e+21],[&amp;quot;+2^49&amp;quot;,1.1259e+15,9.44472e+21],[&amp;quot;+2^50&amp;quot;,2.2518e+15,1.88894e+22],[&amp;quot;+2^51&amp;quot;,4.5036e+15,3.77789e+22],[&amp;quot;+2^52&amp;quot;,9.00719e+15,7.55578e+22],[&amp;quot;+2^53&amp;quot;,1.80144e+16,1.51116e+23],[&amp;quot;+2^54&amp;quot;,3.60288e+16,3.02231e+23],[&amp;quot;+2^55&amp;quot;,7.20575e+16,6.04462e+23],[&amp;quot;+2^56&amp;quot;,1.44115e+17,1.20892e+24],[&amp;quot;+2^57&amp;quot;,2.8823e+17,2.41785e+24],[&amp;quot;+2^58&amp;quot;,5.7646e+17,4.8357e+24],[&amp;quot;+2^59&amp;quot;,1.15292e+18,9.6714e+24],[&amp;quot;+2^60&amp;quot;,2.30584e+18,1.93428e+25],[&amp;quot;+2^61&amp;quot;,4.61168e+18,3.86856e+25],[&amp;quot;+2^62&amp;quot;,9.22336e+18,7.73712e+25],[&amp;quot;+2^63&amp;quot;,1.84467e+19,1.54742e+26],[&amp;quot;+2^64&amp;quot;,0,0],[&amp;quot;+2^65&amp;quot;,0,0],[&amp;quot;+2^66&amp;quot;,0,0],[&amp;quot;+2^67&amp;quot;,0,0],[&amp;quot;+2^68&amp;quot;,0,0],[&amp;quot;+2^69&amp;quot;,0,0],[&amp;quot;+2^70&amp;quot;,0,0],[&amp;quot;+2^71&amp;quot;,0,0],[&amp;quot;+2^72&amp;quot;,0,0],[&amp;quot;+2^73&amp;quot;,0,0],[&amp;quot;+2^74&amp;quot;,0,0],[&amp;quot;+2^75&amp;quot;,0,0],[&amp;quot;+2^76&amp;quot;,0,0],[&amp;quot;+2^77&amp;quot;,0,0],[&amp;quot;+2^78&amp;quot;,0,0],[&amp;quot;+2^79&amp;quot;,0,0],[&amp;quot;+2^80&amp;quot;,0,0],[&amp;quot;+2^81&amp;quot;,0,0],[&amp;quot;+2^82&amp;quot;,0,0],[&amp;quot;+2^83&amp;quot;,0,0],[&amp;quot;+2^84&amp;quot;,0,0],[&amp;quot;+2^85&amp;quot;,0,0],[&amp;quot;+2^86&amp;quot;,0,0],[&amp;quot;+2^87&amp;quot;,0,0],[&amp;quot;+2^88&amp;quot;,0,0],[&amp;quot;+2^89&amp;quot;,0,0],[&amp;quot;+2^90&amp;quot;,0,0],[&amp;quot;+2^91&amp;quot;,0,0],[&amp;quot;+2^92&amp;quot;,0,0],[&amp;quot;+2^93&amp;quot;,0,0],[&amp;quot;+2^94&amp;quot;,0,0],[&amp;quot;+2^95&amp;quot;,0,0],[&amp;quot;+2^96&amp;quot;,0,0],[&amp;quot;+2^97&amp;quot;,0,0],[&amp;quot;+2^98&amp;quot;,0,0],[&amp;quot;+2^99&amp;quot;,0,0],[&amp;quot;+2^100&amp;quot;,0,0],[&amp;quot;+2^101&amp;quot;,0,0],[&amp;quot;+2^102&amp;quot;,0,0],[&amp;quot;+2^103&amp;quot;,0,0],[&amp;quot;+2^104&amp;quot;,0,0],[&amp;quot;+2^105&amp;quot;,0,0],[&amp;quot;+2^106&amp;quot;,0,0],[&amp;quot;+2^107&amp;quot;,0,0],[&amp;quot;+2^108&amp;quot;,0,0],[&amp;quot;+2^109&amp;quot;,0,0],[&amp;quot;+2^110&amp;quot;,0,0],[&amp;quot;+2^111&amp;quot;,0,0],[&amp;quot;+2^112&amp;quot;,0,0],[&amp;quot;+2^113&amp;quot;,0,0],[&amp;quot;+2^114&amp;quot;,0,0],[&amp;quot;+2^115&amp;quot;,0,0],[&amp;quot;+2^116&amp;quot;,0,0],[&amp;quot;+2^117&amp;quot;,0,0],[&amp;quot;+2^118&amp;quot;,0,0],[&amp;quot;+2^119&amp;quot;,0,0],[&amp;quot;+2^120&amp;quot;,0,0],[&amp;quot;+2^121&amp;quot;,0,0],[&amp;quot;+2^122&amp;quot;,0,0],[&amp;quot;+2^123&amp;quot;,0,0],[&amp;quot;+2^124&amp;quot;,0,0],[&amp;quot;+2^125&amp;quot;,0,0],[&amp;quot;+2^126&amp;quot;,0,0],[&amp;quot;+2^127&amp;quot;,0,0] ]&lt;br /&gt;
 }}&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Key observation:&#039;&#039;&#039;  &lt;br /&gt;
The Taylor approximation is highly accurate near zero but diverges extremely fast outside its convergence region.&lt;br /&gt;
This makes it unsuitable as a standalone &amp;lt;code&amp;gt;tanh&amp;lt;/code&amp;gt; implementation, but useful as a local approximation in range-based implementations.&lt;br /&gt;
&lt;br /&gt;
== Localized Approximation Functions ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
!Id&lt;br /&gt;
!Formula&lt;br /&gt;
!ULP &amp;lt;= 2.0&lt;br /&gt;
!NAN&lt;br /&gt;
|-&lt;br /&gt;
|signum&lt;br /&gt;
|&amp;lt;math&amp;gt;\sgn{x}&amp;lt;/math&amp;gt;&lt;br /&gt;
| -inf &amp;lt; x &amp;lt; -8.31763&lt;br /&gt;
8.31763 &amp;lt; x &amp;lt; inf&lt;br /&gt;
| -&lt;br /&gt;
|-&lt;br /&gt;
|poly1&lt;br /&gt;
|&amp;lt;math&amp;gt;x&amp;lt;/math&amp;gt;&lt;br /&gt;
| -0.000704229 &amp;lt; x &amp;lt; 0.000704229&lt;br /&gt;
| -&lt;br /&gt;
|-&lt;br /&gt;
|poly3&lt;br /&gt;
|&amp;lt;math&amp;gt;x - \frac{1}{3} x^3&amp;lt;/math&amp;gt;&lt;br /&gt;
| -0.0291781 &amp;lt; x &amp;lt; 0.0291781 &lt;br /&gt;
| -&lt;br /&gt;
|-&lt;br /&gt;
|lampert7&lt;br /&gt;
|&amp;lt;math&amp;gt;\frac{x \cdot (135135 + x^2 \cdot (17325 + x^2 \cdot (378 + x^2)))}{135135 + x^2 \cdot (62370 + x^2 \cdot (3150 + 28 \cdot x^2))}&lt;br /&gt;
&amp;lt;/math&amp;gt;&lt;br /&gt;
| -0.000947416 &amp;lt; x &amp;lt; 0.000947207&lt;br /&gt;
| -inf &amp;lt; x &amp;lt; -1.51629e+06&lt;br /&gt;
1.51629e+06 &amp;lt; x &amp;lt; inf&lt;br /&gt;
|-&lt;br /&gt;
|pade&lt;br /&gt;
|&amp;lt;math&amp;gt;x + x \cdot \frac{x^2 \cdot \left( p_1 \cdot x^2 + p_0 \right)}{\left( x^2 + q_1 \right) \cdot x^2 + q_0} &lt;br /&gt;
&lt;br /&gt;
&amp;lt;/math&amp;gt; &amp;lt;math&amp;gt;\begin{align}&lt;br /&gt;
p_0 &amp;amp;= -0.2059432032 \\&lt;br /&gt;
p_1 &amp;amp;= -0.0009577527 \\&lt;br /&gt;
q_0 &amp;amp;= 0.6178299136 \\ &lt;br /&gt;
q_1 &amp;amp;= 0.25&lt;br /&gt;
\end{align}&lt;br /&gt;
&amp;lt;/math&amp;gt;&lt;br /&gt;
| -0.0016973 &amp;lt; x &amp;lt; 0.0016973 &lt;br /&gt;
| -inf &amp;lt; x &amp;lt; -6.14896e+18&lt;br /&gt;
6.14896e+18 &amp;lt; x &amp;lt; inf&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== External Links ==&lt;br /&gt;
&lt;br /&gt;
* https://yaikhom.com/2020-04-28-localised-approximation-of-hyperbolic-tangents.html#mjx-eqn-eqn7-th%20degree%20lambert%20approximant&lt;br /&gt;
*https://github.com/llvm/llvm-project/blob/main/libc/src/math/generic/tanhf.cpp&lt;br /&gt;
*https://forums.developer.nvidia.com/t/hardware-accelerated-tanh-on-turing/173291&lt;br /&gt;
[[Category:Numerical Precision]]&lt;/div&gt;</summary>
		<author><name>Timo.stripf</name></author>
	</entry>
	<entry>
		<id>https://www.emmtrix.com/w139/index.php?title=tanh_Software_Implementation&amp;diff=2931</id>
		<title>tanh Software Implementation</title>
		<link rel="alternate" type="text/html" href="https://www.emmtrix.com/w139/index.php?title=tanh_Software_Implementation&amp;diff=2931"/>
		<updated>2026-02-04T12:01:31Z</updated>

		<summary type="html">&lt;p&gt;Timo.stripf: /* exp_v2 */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;[[File:Hyperbolic Tangent.svg|thumb]]&lt;br /&gt;
The hyperbolic tangent function, commonly referred to as &amp;lt;code&amp;gt;tanh&amp;lt;/code&amp;gt;, is a mathematical function that maps real numbers to the range &amp;lt;math&amp;gt;(-1, 1)&amp;lt;/math&amp;gt;. It is defined as:&lt;br /&gt;
&lt;br /&gt;
:&amp;lt;math&amp;gt;\tanh(x) = \frac{e^x - e^{-x}}{e^x + e^{-x}}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
From a software implementation perspective, &amp;lt;code&amp;gt;tanh&amp;lt;/code&amp;gt; is a numerically challenging function due to the use of exponential functions, which can easily overflow for large inputs and suffer from catastrophic cancellation for very small values of &amp;lt;math&amp;gt;x&amp;lt;/math&amp;gt;. These issues are particularly relevant in single-precision floating-point (&amp;lt;code&amp;gt;float32&amp;lt;/code&amp;gt;) implementations, where both accuracy and performance are critical.&lt;br /&gt;
&lt;br /&gt;
In machine learning and deep learning, &amp;lt;code&amp;gt;tanh&amp;lt;/code&amp;gt; is widely used as an activation function because it introduces non-linearity and produces outputs centered around zero, which can improve gradient-based optimization. As a result, &amp;lt;code&amp;gt;tanh&amp;lt;/code&amp;gt; is often evaluated billions of times during training and inference, making efficient and numerically stable implementations essential.&lt;br /&gt;
&lt;br /&gt;
This article focuses on practical software implementations of &amp;lt;code&amp;gt;tanh&amp;lt;/code&amp;gt;, analyzing different computational formulations with respect to floating-point accuracy, ULP error, overflow behavior, and NaN generation. The goal is to provide a foundation for designing robust &amp;lt;code&amp;gt;tanh&amp;lt;/code&amp;gt; implementations suitable for performance-critical environments such as numerical libraries, embedded systems, and AI workloads.&lt;br /&gt;
&lt;br /&gt;
== Problem Overview ==&lt;br /&gt;
The hyperbolic tangent function poses several challenges when implemented in finite-precision floating-point arithmetic. While the mathematical definition is simple, direct translations into software often suffer from numerical instability and performance issues.&lt;br /&gt;
&lt;br /&gt;
A primary challenge is the use of exponential functions. For large input values, &amp;lt;code&amp;gt;exp(x)&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;exp(-x)&amp;lt;/code&amp;gt; can overflow in single-precision floating-point arithmetic, leading to infinities and, in some formulations, NaN results. Avoiding overflow therefore requires careful reformulation or explicit range handling.&lt;br /&gt;
&lt;br /&gt;
For very small input values, catastrophic cancellation becomes the dominant issue. Expressions such as &amp;lt;math&amp;gt;e^x - e^{-x}&amp;lt;/math&amp;gt; involve the subtraction of nearly equal numbers, causing a severe loss of significant bits and large ULP errors. In these regions, naive implementations may return zero or highly inaccurate results even though &amp;lt;math&amp;gt;\tanh(x) \approx x&amp;lt;/math&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
In addition to accuracy concerns, performance is a critical factor. In many applications, especially machine learning and numerical simulations, &amp;lt;code&amp;gt;tanh&amp;lt;/code&amp;gt; is evaluated extremely frequently. This makes it necessary to balance numerical robustness with the cost of transcendental function calls, branch complexity, and instruction-level efficiency.&lt;br /&gt;
&lt;br /&gt;
==Direct Exponential Formulations==&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+&lt;br /&gt;
!Id&lt;br /&gt;
!Formula&lt;br /&gt;
!Highest ULP Error&lt;br /&gt;
!ULP &amp;lt;= 2.0&lt;br /&gt;
!NaN&lt;br /&gt;
|-&lt;br /&gt;
|exp_v1&lt;br /&gt;
|&amp;lt;math&amp;gt;\frac{e^x - e^{-x}}{e^x + e^{-x}}&amp;lt;/math&amp;gt;&lt;br /&gt;
|x &amp;lt; 0: 8.55638e+08 at -2.98023e-08 with 0x0p+0 instead of -0x1p-25&lt;br /&gt;
&lt;br /&gt;
x &amp;gt; 0: 8.55638e+08 at 2.98023e-08 with 0x0p+0 instead of 0x1p-25&lt;br /&gt;
| -88.7246 &amp;lt; x &amp;lt; -7.14197&lt;br /&gt;
7.14197 &amp;lt; x &amp;lt; 88.7246&lt;br /&gt;
| -inf &amp;lt; x &amp;lt; -88.7246&lt;br /&gt;
88.7246 &amp;lt; x &amp;lt; inf&lt;br /&gt;
|-&lt;br /&gt;
|exp_v2&lt;br /&gt;
|&amp;lt;math&amp;gt;\frac{e^{2x} - 1}{e^{2x} + 1}&amp;lt;/math&amp;gt;&lt;br /&gt;
|x &amp;lt; 0: 8.47249e+08 at -1.49012e-08 with 0x0p+0 instead of -0x1p-26&lt;br /&gt;
x &amp;gt; 0: 8.55638e+08 at 2.98023e-08 with 0x0p+0 instead of 0x1.fffffep-26&lt;br /&gt;
| -inf &amp;lt; x &amp;lt; -0.547821&lt;br /&gt;
0.254822 &amp;lt; x &amp;lt; 8.31787&lt;br /&gt;
&lt;br /&gt;
8.38379 &amp;lt; x &amp;lt; 44.3623&lt;br /&gt;
|44.3623 &amp;lt; x &amp;lt; inf&lt;br /&gt;
|-&lt;br /&gt;
|exp_v3&lt;br /&gt;
|&amp;lt;math&amp;gt;1 - \frac{2}{e^{2x} + 1}&amp;lt;/math&amp;gt;&lt;br /&gt;
|x &amp;lt; 0: 8.59832e+08 at -4.47035e-08 with 0x0p+0 instead of -0x1.8p-25&lt;br /&gt;
x &amp;gt; 0: 8.68221e+08 at 8.9407e-08 with 0x0p+0 instead of 0x1.7ffffcp-24&lt;br /&gt;
| -inf &amp;lt; x &amp;lt; -6.26233&lt;br /&gt;
0.346382 &amp;lt; x &amp;lt; inf&lt;br /&gt;
| -&lt;br /&gt;
|-&lt;br /&gt;
|expm1&lt;br /&gt;
|&amp;lt;math&amp;gt;\frac{expm1(2x)}{expm1(2x) + 2}&amp;lt;/math&amp;gt; &lt;br /&gt;
|x &amp;lt; 0: 2.49585 at -3.95627 with -0x1.ffa00ap-1 instead of -0x1.ffa00ep-1&lt;br /&gt;
x &amp;gt; 0: 2.41657 at 0.0155837 with 0x1.fe9b66p-7 instead of 0x1.fe9b62p-7&lt;br /&gt;
| -inf &amp;lt; x &amp;lt; -5.57227&lt;br /&gt;
-0.42981 &amp;lt; x &amp;lt; 0.00777948&lt;br /&gt;
&lt;br /&gt;
0.125351 &amp;lt; x &amp;lt; 44.3623&lt;br /&gt;
|44.3623 &amp;lt; x &amp;lt; inf&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
The first equation is the definition of the hyperbolic tangent function. This equation requires two exponentials with different values. Since exponentials are expensive to compute, we can use the second equation to reduce the number of exponentials to one.&lt;br /&gt;
&lt;br /&gt;
The second equation has the drawback that both the numerator and the denominator can become infinity for large values of x which results in NaN. That happens for &amp;lt;math&amp;gt;|x| &amp;gt; \log(FLT\_MAX)/2 \approx 44&amp;lt;/math&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
The third equation solves this problem but has a slightly higher error for &amp;lt;math&amp;gt;x \approx 2^{-25}&amp;lt;/math&amp;gt;. In the following graph you can see the error chart for equation 3. The error charts for the other two equations are very similar. All three equations have an error of up to 16 mio ULPs for &amp;lt;math&amp;gt;x &amp;lt; 2^{-5}&amp;lt;/math&amp;gt; which means that up to 24 bits could be wrong.&lt;br /&gt;
&lt;br /&gt;
=== exp_v2 ===&lt;br /&gt;
&lt;br /&gt;
The following error graph shows the ULP error of the &amp;lt;code&amp;gt;exp_v2&amp;lt;/code&amp;gt; formulation across the full &amp;lt;code&amp;gt;float32&amp;lt;/code&amp;gt; input range. &lt;br /&gt;
This formulation reduces the number of exponential evaluations to one, but is particularly sensitive to catastrophic cancellation for very small input values and to overflow for large &amp;lt;math&amp;gt;|x|&amp;lt;/math&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
{{#widget:FloatErrorGraph|id=tanhf_exp_v2&lt;br /&gt;
  |data=[ [&amp;quot;-2^127&amp;quot;,0,1.17549e-38],[&amp;quot;-2^126&amp;quot;,0,1.17549e-38],[&amp;quot;-2^125&amp;quot;,0,1.17549e-38],[&amp;quot;-2^124&amp;quot;,0,1.17549e-38],[&amp;quot;-2^123&amp;quot;,0,1.17549e-38],[&amp;quot;-2^122&amp;quot;,0,1.17549e-38],[&amp;quot;-2^121&amp;quot;,0,1.17549e-38],[&amp;quot;-2^120&amp;quot;,0,1.17549e-38],[&amp;quot;-2^119&amp;quot;,0,1.17549e-38],[&amp;quot;-2^118&amp;quot;,0,1.17549e-38],[&amp;quot;-2^117&amp;quot;,0,1.17549e-38],[&amp;quot;-2^116&amp;quot;,0,1.17549e-38],[&amp;quot;-2^115&amp;quot;,0,1.17549e-38],[&amp;quot;-2^114&amp;quot;,0,1.17549e-38],[&amp;quot;-2^113&amp;quot;,0,1.17549e-38],[&amp;quot;-2^112&amp;quot;,0,1.17549e-38],[&amp;quot;-2^111&amp;quot;,0,1.17549e-38],[&amp;quot;-2^110&amp;quot;,0,1.17549e-38],[&amp;quot;-2^109&amp;quot;,0,1.17549e-38],[&amp;quot;-2^108&amp;quot;,0,1.17549e-38],[&amp;quot;-2^107&amp;quot;,0,1.17549e-38],[&amp;quot;-2^106&amp;quot;,0,1.17549e-38],[&amp;quot;-2^105&amp;quot;,0,1.17549e-38],[&amp;quot;-2^104&amp;quot;,0,1.17549e-38],[&amp;quot;-2^103&amp;quot;,0,1.17549e-38],[&amp;quot;-2^102&amp;quot;,0,1.17549e-38],[&amp;quot;-2^101&amp;quot;,0,1.17549e-38],[&amp;quot;-2^100&amp;quot;,0,1.17549e-38],[&amp;quot;-2^99&amp;quot;,0,1.17549e-38],[&amp;quot;-2^98&amp;quot;,0,1.17549e-38],[&amp;quot;-2^97&amp;quot;,0,1.17549e-38],[&amp;quot;-2^96&amp;quot;,0,1.17549e-38],[&amp;quot;-2^95&amp;quot;,0,1.17549e-38],[&amp;quot;-2^94&amp;quot;,0,1.17549e-38],[&amp;quot;-2^93&amp;quot;,0,1.17549e-38],[&amp;quot;-2^92&amp;quot;,0,1.17549e-38],[&amp;quot;-2^91&amp;quot;,0,1.17549e-38],[&amp;quot;-2^90&amp;quot;,0,1.17549e-38],[&amp;quot;-2^89&amp;quot;,0,1.17549e-38],[&amp;quot;-2^88&amp;quot;,0,1.17549e-38],[&amp;quot;-2^87&amp;quot;,0,1.17549e-38],[&amp;quot;-2^86&amp;quot;,0,1.17549e-38],[&amp;quot;-2^85&amp;quot;,0,1.17549e-38],[&amp;quot;-2^84&amp;quot;,0,1.17549e-38],[&amp;quot;-2^83&amp;quot;,0,1.17549e-38],[&amp;quot;-2^82&amp;quot;,0,1.17549e-38],[&amp;quot;-2^81&amp;quot;,0,1.17549e-38],[&amp;quot;-2^80&amp;quot;,0,1.17549e-38],[&amp;quot;-2^79&amp;quot;,0,1.17549e-38],[&amp;quot;-2^78&amp;quot;,0,1.17549e-38],[&amp;quot;-2^77&amp;quot;,0,1.17549e-38],[&amp;quot;-2^76&amp;quot;,0,1.17549e-38],[&amp;quot;-2^75&amp;quot;,0,1.17549e-38],[&amp;quot;-2^74&amp;quot;,0,1.17549e-38],[&amp;quot;-2^73&amp;quot;,0,1.17549e-38],[&amp;quot;-2^72&amp;quot;,0,1.17549e-38],[&amp;quot;-2^71&amp;quot;,0,1.17549e-38],[&amp;quot;-2^70&amp;quot;,0,1.17549e-38],[&amp;quot;-2^69&amp;quot;,0,1.17549e-38],[&amp;quot;-2^68&amp;quot;,0,1.17549e-38],[&amp;quot;-2^67&amp;quot;,0,1.17549e-38],[&amp;quot;-2^66&amp;quot;,0,1.17549e-38],[&amp;quot;-2^65&amp;quot;,0,1.17549e-38],[&amp;quot;-2^64&amp;quot;,0,1.17549e-38],[&amp;quot;-2^63&amp;quot;,0,1.17549e-38],[&amp;quot;-2^62&amp;quot;,0,1.17549e-38],[&amp;quot;-2^61&amp;quot;,0,1.17549e-38],[&amp;quot;-2^60&amp;quot;,0,1.17549e-38],[&amp;quot;-2^59&amp;quot;,0,1.17549e-38],[&amp;quot;-2^58&amp;quot;,0,1.17549e-38],[&amp;quot;-2^57&amp;quot;,0,1.17549e-38],[&amp;quot;-2^56&amp;quot;,0,1.17549e-38],[&amp;quot;-2^55&amp;quot;,0,1.17549e-38],[&amp;quot;-2^54&amp;quot;,0,1.17549e-38],[&amp;quot;-2^53&amp;quot;,0,1.17549e-38],[&amp;quot;-2^52&amp;quot;,0,1.17549e-38],[&amp;quot;-2^51&amp;quot;,0,1.17549e-38],[&amp;quot;-2^50&amp;quot;,0,1.17549e-38],[&amp;quot;-2^49&amp;quot;,0,1.17549e-38],[&amp;quot;-2^48&amp;quot;,0,1.17549e-38],[&amp;quot;-2^47&amp;quot;,0,1.17549e-38],[&amp;quot;-2^46&amp;quot;,0,1.17549e-38],[&amp;quot;-2^45&amp;quot;,0,1.17549e-38],[&amp;quot;-2^44&amp;quot;,0,1.17549e-38],[&amp;quot;-2^43&amp;quot;,0,1.17549e-38],[&amp;quot;-2^42&amp;quot;,0,1.17549e-38],[&amp;quot;-2^41&amp;quot;,0,1.17549e-38],[&amp;quot;-2^40&amp;quot;,0,1.17549e-38],[&amp;quot;-2^39&amp;quot;,0,1.17549e-38],[&amp;quot;-2^38&amp;quot;,0,1.17549e-38],[&amp;quot;-2^37&amp;quot;,0,1.17549e-38],[&amp;quot;-2^36&amp;quot;,0,1.17549e-38],[&amp;quot;-2^35&amp;quot;,0,1.17549e-38],[&amp;quot;-2^34&amp;quot;,0,1.17549e-38],[&amp;quot;-2^33&amp;quot;,0,1.17549e-38],[&amp;quot;-2^32&amp;quot;,0,1.17549e-38],[&amp;quot;-2^31&amp;quot;,0,1.17549e-38],[&amp;quot;-2^30&amp;quot;,0,1.17549e-38],[&amp;quot;-2^29&amp;quot;,0,1.17549e-38],[&amp;quot;-2^28&amp;quot;,0,1.17549e-38],[&amp;quot;-2^27&amp;quot;,0,1.17549e-38],[&amp;quot;-2^26&amp;quot;,0,1.17549e-38],[&amp;quot;-2^25&amp;quot;,0,1.17549e-38],[&amp;quot;-2^24&amp;quot;,0,1.17549e-38],[&amp;quot;-2^23&amp;quot;,0,1.17549e-38],[&amp;quot;-2^22&amp;quot;,0,1.17549e-38],[&amp;quot;-2^21&amp;quot;,0,1.17549e-38],[&amp;quot;-2^20&amp;quot;,0,1.17549e-38],[&amp;quot;-2^19&amp;quot;,0,1.17549e-38],[&amp;quot;-2^18&amp;quot;,0,1.17549e-38],[&amp;quot;-2^17&amp;quot;,0,1.17549e-38],[&amp;quot;-2^16&amp;quot;,0,1.17549e-38],[&amp;quot;-2^15&amp;quot;,0,1.17549e-38],[&amp;quot;-2^14&amp;quot;,0,1.17549e-38],[&amp;quot;-2^13&amp;quot;,0,1.17549e-38],[&amp;quot;-2^12&amp;quot;,0,1.17549e-38],[&amp;quot;-2^11&amp;quot;,0,1.17549e-38],[&amp;quot;-2^10&amp;quot;,0,1.17549e-38],[&amp;quot;-2^9&amp;quot;,0,1.17549e-38],[&amp;quot;-2^8&amp;quot;,0,1.17549e-38],[&amp;quot;-2^7&amp;quot;,0,1.17549e-38],[&amp;quot;-2^6&amp;quot;,0,1.17549e-38],[&amp;quot;-2^5&amp;quot;,0,1.17549e-38],[&amp;quot;-2^4&amp;quot;,2.53131e-14,4.24683e-07],[&amp;quot;-2^3&amp;quot;,5.95888e-08,0.999734],[&amp;quot;-2^2&amp;quot;,8.86696e-08,1.48763],[&amp;quot;-2^1&amp;quot;,8.91609e-08,1.49587],[&amp;quot;-2^0&amp;quot;,8.73743e-08,1.4659],[&amp;quot;-2^-1&amp;quot;,8.65382e-08,2.13222],[&amp;quot;-2^-2&amp;quot;,6.18481e-08,2.61428],[&amp;quot;-2^-3&amp;quot;,3.88071e-08,3.08594],[&amp;quot;-2^-4&amp;quot;,2.6234e-08,4.23594],[&amp;quot;-2^-5&amp;quot;,2.02799e-08,8.19726],[&amp;quot;-2^-6&amp;quot;,1.75183e-08,12.4495],[&amp;quot;-2^-7&amp;quot;,1.61937e-08,17.3878],[&amp;quot;-2^-8&amp;quot;,1.5566e-08,33.4278],[&amp;quot;-2^-9&amp;quot;,1.51688e-08,65.1495],[&amp;quot;-2^-10&amp;quot;,1.50281e-08,129.09],[&amp;quot;-2^-11&amp;quot;,1.49613e-08,257.033],[&amp;quot;-2^-12&amp;quot;,1.49296e-08,512.977],[&amp;quot;-2^-13&amp;quot;,1.49174e-08,1025.12],[&amp;quot;-2^-14&amp;quot;,1.4909e-08,2049.08],[&amp;quot;-2^-15&amp;quot;,1.49049e-08,4097.02],[&amp;quot;-2^-16&amp;quot;,1.4903e-08,8193],[&amp;quot;-2^-17&amp;quot;,1.49021e-08,16385],[&amp;quot;-2^-18&amp;quot;,1.48862e-08,32735],[&amp;quot;-2^-19&amp;quot;,1.48973e-08,65519],[&amp;quot;-2^-20&amp;quot;,1.49001e-08,131063],[&amp;quot;-2^-21&amp;quot;,1.49009e-08,262139],[&amp;quot;-2^-22&amp;quot;,1.49011e-08,524285],[&amp;quot;-2^-23&amp;quot;,1.49011e-08,1.04857e+06],[&amp;quot;-2^-24&amp;quot;,1.49011e-08,2.09715e+06],[&amp;quot;-2^-25&amp;quot;,1.49012e-08,4.1943e+06],[&amp;quot;-2^-26&amp;quot;,1.49007e-08,8.38835e+06],[&amp;quot;-2^-27&amp;quot;,1.49012e-08,8.47249e+08],[&amp;quot;-2^-28&amp;quot;,7.45058e-09,8.38861e+08],[&amp;quot;-2^-29&amp;quot;,3.72529e-09,8.30472e+08],[&amp;quot;-2^-30&amp;quot;,1.86265e-09,8.22084e+08],[&amp;quot;-2^-31&amp;quot;,9.31323e-10,8.13695e+08],[&amp;quot;-2^-32&amp;quot;,4.65661e-10,8.05306e+08],[&amp;quot;-2^-33&amp;quot;,2.32831e-10,7.96918e+08],[&amp;quot;-2^-34&amp;quot;,1.16415e-10,7.88529e+08],[&amp;quot;-2^-35&amp;quot;,5.82077e-11,7.80141e+08],[&amp;quot;-2^-36&amp;quot;,2.91038e-11,7.71752e+08],[&amp;quot;-2^-37&amp;quot;,1.45519e-11,7.63363e+08],[&amp;quot;-2^-38&amp;quot;,7.27596e-12,7.54975e+08],[&amp;quot;-2^-39&amp;quot;,3.63798e-12,7.46586e+08],[&amp;quot;-2^-40&amp;quot;,1.81899e-12,7.38198e+08],[&amp;quot;-2^-41&amp;quot;,9.09495e-13,7.29809e+08],[&amp;quot;-2^-42&amp;quot;,4.54747e-13,7.2142e+08],[&amp;quot;-2^-43&amp;quot;,2.27374e-13,7.13032e+08],[&amp;quot;-2^-44&amp;quot;,1.13687e-13,7.04643e+08],[&amp;quot;-2^-45&amp;quot;,5.68434e-14,6.96254e+08],[&amp;quot;-2^-46&amp;quot;,2.84217e-14,6.87866e+08],[&amp;quot;-2^-47&amp;quot;,1.42109e-14,6.79477e+08],[&amp;quot;-2^-48&amp;quot;,7.10543e-15,6.71089e+08],[&amp;quot;-2^-49&amp;quot;,3.55271e-15,6.627e+08],[&amp;quot;-2^-50&amp;quot;,1.77636e-15,6.54311e+08],[&amp;quot;-2^-51&amp;quot;,8.88178e-16,6.45923e+08],[&amp;quot;-2^-52&amp;quot;,4.44089e-16,6.37534e+08],[&amp;quot;-2^-53&amp;quot;,2.22045e-16,6.29146e+08],[&amp;quot;-2^-54&amp;quot;,1.11022e-16,6.20757e+08],[&amp;quot;-2^-55&amp;quot;,5.55111e-17,6.12368e+08],[&amp;quot;-2^-56&amp;quot;,2.77556e-17,6.0398e+08],[&amp;quot;-2^-57&amp;quot;,1.38778e-17,5.95591e+08],[&amp;quot;-2^-58&amp;quot;,6.93889e-18,5.87203e+08],[&amp;quot;-2^-59&amp;quot;,3.46945e-18,5.78814e+08],[&amp;quot;-2^-60&amp;quot;,1.73472e-18,5.70425e+08],[&amp;quot;-2^-61&amp;quot;,8.67362e-19,5.62037e+08],[&amp;quot;-2^-62&amp;quot;,4.33681e-19,5.53648e+08],[&amp;quot;-2^-63&amp;quot;,2.1684e-19,5.4526e+08],[&amp;quot;-2^-64&amp;quot;,1.0842e-19,5.36871e+08],[&amp;quot;-2^-65&amp;quot;,5.42101e-20,5.28482e+08],[&amp;quot;-2^-66&amp;quot;,2.71051e-20,5.20094e+08],[&amp;quot;-2^-67&amp;quot;,1.35525e-20,5.11705e+08],[&amp;quot;-2^-68&amp;quot;,6.77626e-21,5.03316e+08],[&amp;quot;-2^-69&amp;quot;,3.38813e-21,4.94928e+08],[&amp;quot;-2^-70&amp;quot;,1.69407e-21,4.86539e+08],[&amp;quot;-2^-71&amp;quot;,8.47033e-22,4.78151e+08],[&amp;quot;-2^-72&amp;quot;,4.23516e-22,4.69762e+08],[&amp;quot;-2^-73&amp;quot;,2.11758e-22,4.61373e+08],[&amp;quot;-2^-74&amp;quot;,1.05879e-22,4.52985e+08],[&amp;quot;-2^-75&amp;quot;,5.29396e-23,4.44596e+08],[&amp;quot;-2^-76&amp;quot;,2.64698e-23,4.36208e+08],[&amp;quot;-2^-77&amp;quot;,1.32349e-23,4.27819e+08],[&amp;quot;-2^-78&amp;quot;,6.61744e-24,4.1943e+08],[&amp;quot;-2^-79&amp;quot;,3.30872e-24,4.11042e+08],[&amp;quot;-2^-80&amp;quot;,1.65436e-24,4.02653e+08],[&amp;quot;-2^-81&amp;quot;,8.27181e-25,3.94265e+08],[&amp;quot;-2^-82&amp;quot;,4.1359e-25,3.85876e+08],[&amp;quot;-2^-83&amp;quot;,2.06795e-25,3.77487e+08],[&amp;quot;-2^-84&amp;quot;,1.03398e-25,3.69099e+08],[&amp;quot;-2^-85&amp;quot;,5.16988e-26,3.6071e+08],[&amp;quot;-2^-86&amp;quot;,2.58494e-26,3.52322e+08],[&amp;quot;-2^-87&amp;quot;,1.29247e-26,3.43933e+08],[&amp;quot;-2^-88&amp;quot;,6.46235e-27,3.35544e+08],[&amp;quot;-2^-89&amp;quot;,3.23117e-27,3.27156e+08],[&amp;quot;-2^-90&amp;quot;,1.61559e-27,3.18767e+08],[&amp;quot;-2^-91&amp;quot;,8.07794e-28,3.10378e+08],[&amp;quot;-2^-92&amp;quot;,4.03897e-28,3.0199e+08],[&amp;quot;-2^-93&amp;quot;,2.01948e-28,2.93601e+08],[&amp;quot;-2^-94&amp;quot;,1.00974e-28,2.85213e+08],[&amp;quot;-2^-95&amp;quot;,5.04871e-29,2.76824e+08],[&amp;quot;-2^-96&amp;quot;,2.52435e-29,2.68435e+08],[&amp;quot;-2^-97&amp;quot;,1.26218e-29,2.60047e+08],[&amp;quot;-2^-98&amp;quot;,6.31089e-30,2.51658e+08],[&amp;quot;-2^-99&amp;quot;,3.15544e-30,2.4327e+08],[&amp;quot;-2^-100&amp;quot;,1.57772e-30,2.34881e+08],[&amp;quot;-2^-101&amp;quot;,7.88861e-31,2.26492e+08],[&amp;quot;-2^-102&amp;quot;,3.9443e-31,2.18104e+08],[&amp;quot;-2^-103&amp;quot;,1.97215e-31,2.09715e+08],[&amp;quot;-2^-104&amp;quot;,9.86076e-32,2.01327e+08],[&amp;quot;-2^-105&amp;quot;,4.93038e-32,1.92938e+08],[&amp;quot;-2^-106&amp;quot;,2.46519e-32,1.84549e+08],[&amp;quot;-2^-107&amp;quot;,1.2326e-32,1.76161e+08],[&amp;quot;-2^-108&amp;quot;,6.16298e-33,1.67772e+08],[&amp;quot;-2^-109&amp;quot;,3.08149e-33,1.59384e+08],[&amp;quot;-2^-110&amp;quot;,1.54074e-33,1.50995e+08],[&amp;quot;-2^-111&amp;quot;,7.70372e-34,1.42606e+08],[&amp;quot;-2^-112&amp;quot;,3.85186e-34,1.34218e+08],[&amp;quot;-2^-113&amp;quot;,1.92593e-34,1.25829e+08],[&amp;quot;-2^-114&amp;quot;,9.62965e-35,1.17441e+08],[&amp;quot;-2^-115&amp;quot;,4.81482e-35,1.09052e+08],[&amp;quot;-2^-116&amp;quot;,2.40741e-35,1.00663e+08],[&amp;quot;-2^-117&amp;quot;,1.20371e-35,9.22747e+07],[&amp;quot;-2^-118&amp;quot;,6.01853e-36,8.38861e+07],[&amp;quot;-2^-119&amp;quot;,3.00927e-36,7.54975e+07],[&amp;quot;-2^-120&amp;quot;,1.50463e-36,6.71089e+07],[&amp;quot;-2^-121&amp;quot;,7.52316e-37,5.87203e+07],[&amp;quot;-2^-122&amp;quot;,3.76158e-37,5.03316e+07],[&amp;quot;-2^-123&amp;quot;,1.88079e-37,4.1943e+07],[&amp;quot;-2^-124&amp;quot;,9.40395e-38,3.35544e+07],[&amp;quot;-2^-125&amp;quot;,4.70198e-38,2.51658e+07],[&amp;quot;-2^-126&amp;quot;,2.35099e-38,1.67772e+07],[&amp;quot;-SN&amp;quot;,1.17546e-38,8.38835e+06],[&amp;quot;0&amp;quot;,0,1.17549e-38],[&amp;quot;+SN&amp;quot;,1.17546e-38,8.38835e+06],[&amp;quot;+2^-126&amp;quot;,2.35095e-38,1.6777e+07],[&amp;quot;+2^-125&amp;quot;,4.70191e-38,2.51656e+07],[&amp;quot;+2^-124&amp;quot;,9.40381e-38,3.35542e+07],[&amp;quot;+2^-123&amp;quot;,1.88076e-37,4.19428e+07],[&amp;quot;+2^-122&amp;quot;,3.76152e-37,5.03314e+07],[&amp;quot;+2^-121&amp;quot;,7.52305e-37,5.872e+07],[&amp;quot;+2^-120&amp;quot;,1.50461e-36,6.71086e+07],[&amp;quot;+2^-119&amp;quot;,3.00922e-36,7.54972e+07],[&amp;quot;+2^-118&amp;quot;,6.01844e-36,8.38858e+07],[&amp;quot;+2^-117&amp;quot;,1.20369e-35,9.22744e+07],[&amp;quot;+2^-116&amp;quot;,2.40738e-35,1.00663e+08],[&amp;quot;+2^-115&amp;quot;,4.81475e-35,1.09052e+08],[&amp;quot;+2^-114&amp;quot;,9.6295e-35,1.1744e+08],[&amp;quot;+2^-113&amp;quot;,1.9259e-34,1.25829e+08],[&amp;quot;+2^-112&amp;quot;,3.8518e-34,1.34217e+08],[&amp;quot;+2^-111&amp;quot;,7.7036e-34,1.42606e+08],[&amp;quot;+2^-110&amp;quot;,1.54072e-33,1.50995e+08],[&amp;quot;+2^-109&amp;quot;,3.08144e-33,1.59383e+08],[&amp;quot;+2^-108&amp;quot;,6.16288e-33,1.67772e+08],[&amp;quot;+2^-107&amp;quot;,1.23258e-32,1.76161e+08],[&amp;quot;+2^-106&amp;quot;,2.46515e-32,1.84549e+08],[&amp;quot;+2^-105&amp;quot;,4.93031e-32,1.92938e+08],[&amp;quot;+2^-104&amp;quot;,9.86061e-32,2.01326e+08],[&amp;quot;+2^-103&amp;quot;,1.97212e-31,2.09715e+08],[&amp;quot;+2^-102&amp;quot;,3.94424e-31,2.18104e+08],[&amp;quot;+2^-101&amp;quot;,7.88849e-31,2.26492e+08],[&amp;quot;+2^-100&amp;quot;,1.5777e-30,2.34881e+08],[&amp;quot;+2^-99&amp;quot;,3.1554e-30,2.43269e+08],[&amp;quot;+2^-98&amp;quot;,6.31079e-30,2.51658e+08],[&amp;quot;+2^-97&amp;quot;,1.26216e-29,2.60047e+08],[&amp;quot;+2^-96&amp;quot;,2.52432e-29,2.68435e+08],[&amp;quot;+2^-95&amp;quot;,5.04863e-29,2.76824e+08],[&amp;quot;+2^-94&amp;quot;,1.00973e-28,2.85212e+08],[&amp;quot;+2^-93&amp;quot;,2.01945e-28,2.93601e+08],[&amp;quot;+2^-92&amp;quot;,4.03891e-28,3.0199e+08],[&amp;quot;+2^-91&amp;quot;,8.07781e-28,3.10378e+08],[&amp;quot;+2^-90&amp;quot;,1.61556e-27,3.18767e+08],[&amp;quot;+2^-89&amp;quot;,3.23112e-27,3.27155e+08],[&amp;quot;+2^-88&amp;quot;,6.46225e-27,3.35544e+08],[&amp;quot;+2^-87&amp;quot;,1.29245e-26,3.43933e+08],[&amp;quot;+2^-86&amp;quot;,2.5849e-26,3.52321e+08],[&amp;quot;+2^-85&amp;quot;,5.1698e-26,3.6071e+08],[&amp;quot;+2^-84&amp;quot;,1.03396e-25,3.69098e+08],[&amp;quot;+2^-83&amp;quot;,2.06792e-25,3.77487e+08],[&amp;quot;+2^-82&amp;quot;,4.13584e-25,3.85876e+08],[&amp;quot;+2^-81&amp;quot;,8.27168e-25,3.94264e+08],[&amp;quot;+2^-80&amp;quot;,1.65434e-24,4.02653e+08],[&amp;quot;+2^-79&amp;quot;,3.30867e-24,4.11042e+08],[&amp;quot;+2^-78&amp;quot;,6.61734e-24,4.1943e+08],[&amp;quot;+2^-77&amp;quot;,1.32347e-23,4.27819e+08],[&amp;quot;+2^-76&amp;quot;,2.64694e-23,4.36207e+08],[&amp;quot;+2^-75&amp;quot;,5.29388e-23,4.44596e+08],[&amp;quot;+2^-74&amp;quot;,1.05878e-22,4.52985e+08],[&amp;quot;+2^-73&amp;quot;,2.11755e-22,4.61373e+08],[&amp;quot;+2^-72&amp;quot;,4.2351e-22,4.69762e+08],[&amp;quot;+2^-71&amp;quot;,8.4702e-22,4.7815e+08],[&amp;quot;+2^-70&amp;quot;,1.69404e-21,4.86539e+08],[&amp;quot;+2^-69&amp;quot;,3.38808e-21,4.94928e+08],[&amp;quot;+2^-68&amp;quot;,6.77616e-21,5.03316e+08],[&amp;quot;+2^-67&amp;quot;,1.35523e-20,5.11705e+08],[&amp;quot;+2^-66&amp;quot;,2.71046e-20,5.20093e+08],[&amp;quot;+2^-65&amp;quot;,5.42093e-20,5.28482e+08],[&amp;quot;+2^-64&amp;quot;,1.08419e-19,5.36871e+08],[&amp;quot;+2^-63&amp;quot;,2.16837e-19,5.45259e+08],[&amp;quot;+2^-62&amp;quot;,4.33674e-19,5.53648e+08],[&amp;quot;+2^-61&amp;quot;,8.67349e-19,5.62036e+08],[&amp;quot;+2^-60&amp;quot;,1.7347e-18,5.70425e+08],[&amp;quot;+2^-59&amp;quot;,3.46939e-18,5.78814e+08],[&amp;quot;+2^-58&amp;quot;,6.93879e-18,5.87202e+08],[&amp;quot;+2^-57&amp;quot;,1.38776e-17,5.95591e+08],[&amp;quot;+2^-56&amp;quot;,2.77552e-17,6.0398e+08],[&amp;quot;+2^-55&amp;quot;,5.55103e-17,6.12368e+08],[&amp;quot;+2^-54&amp;quot;,1.11021e-16,6.20757e+08],[&amp;quot;+2^-53&amp;quot;,2.22041e-16,6.29145e+08],[&amp;quot;+2^-52&amp;quot;,4.44082e-16,6.37534e+08],[&amp;quot;+2^-51&amp;quot;,8.88165e-16,6.45923e+08],[&amp;quot;+2^-50&amp;quot;,1.77633e-15,6.54311e+08],[&amp;quot;+2^-49&amp;quot;,3.55266e-15,6.627e+08],[&amp;quot;+2^-48&amp;quot;,7.10532e-15,6.71088e+08],[&amp;quot;+2^-47&amp;quot;,1.42106e-14,6.79477e+08],[&amp;quot;+2^-46&amp;quot;,2.84213e-14,6.87866e+08],[&amp;quot;+2^-45&amp;quot;,5.68426e-14,6.96254e+08],[&amp;quot;+2^-44&amp;quot;,1.13685e-13,7.04643e+08],[&amp;quot;+2^-43&amp;quot;,2.2737e-13,7.13031e+08],[&amp;quot;+2^-42&amp;quot;,4.5474e-13,7.2142e+08],[&amp;quot;+2^-41&amp;quot;,9.09481e-13,7.29809e+08],[&amp;quot;+2^-40&amp;quot;,1.81896e-12,7.38197e+08],[&amp;quot;+2^-39&amp;quot;,3.63792e-12,7.46586e+08],[&amp;quot;+2^-38&amp;quot;,7.27585e-12,7.54974e+08],[&amp;quot;+2^-37&amp;quot;,1.45517e-11,7.63363e+08],[&amp;quot;+2^-36&amp;quot;,2.91034e-11,7.71752e+08],[&amp;quot;+2^-35&amp;quot;,5.82068e-11,7.8014e+08],[&amp;quot;+2^-34&amp;quot;,1.16414e-10,7.88529e+08],[&amp;quot;+2^-33&amp;quot;,2.32827e-10,7.96918e+08],[&amp;quot;+2^-32&amp;quot;,4.65654e-10,8.05306e+08],[&amp;quot;+2^-31&amp;quot;,9.31308e-10,8.13695e+08],[&amp;quot;+2^-30&amp;quot;,1.86262e-09,8.22083e+08],[&amp;quot;+2^-29&amp;quot;,3.72523e-09,8.30472e+08],[&amp;quot;+2^-28&amp;quot;,7.45047e-09,8.38861e+08],[&amp;quot;+2^-27&amp;quot;,1.49009e-08,8.47249e+08],[&amp;quot;+2^-26&amp;quot;,2.98019e-08,8.55638e+08],[&amp;quot;+2^-25&amp;quot;,2.98023e-08,8.38861e+06],[&amp;quot;+2^-24&amp;quot;,2.98023e-08,4.1943e+06],[&amp;quot;+2^-23&amp;quot;,2.98023e-08,2.09715e+06],[&amp;quot;+2^-22&amp;quot;,2.98023e-08,1.04857e+06],[&amp;quot;+2^-21&amp;quot;,2.98021e-08,524284],[&amp;quot;+2^-20&amp;quot;,2.98014e-08,262136],[&amp;quot;+2^-19&amp;quot;,2.97987e-08,131056],[&amp;quot;+2^-18&amp;quot;,2.97878e-08,65504],[&amp;quot;+2^-17&amp;quot;,2.98014e-08,32767],[&amp;quot;+2^-16&amp;quot;,2.98005e-08,16384],[&amp;quot;+2^-15&amp;quot;,2.98023e-08,8192],[&amp;quot;+2^-14&amp;quot;,2.98025e-08,4351.99],[&amp;quot;+2^-13&amp;quot;,2.98044e-08,2815.96],[&amp;quot;+2^-12&amp;quot;,2.98091e-08,1024.23],[&amp;quot;+2^-11&amp;quot;,2.9812e-08,512.167],[&amp;quot;+2^-10&amp;quot;,2.98154e-08,256.113],[&amp;quot;+2^-9&amp;quot;,2.98591e-08,128.244],[&amp;quot;+2^-8&amp;quot;,3.00204e-08,85.3328],[&amp;quot;+2^-7&amp;quot;,3.01947e-08,43.675],[&amp;quot;+2^-6&amp;quot;,3.05713e-08,30.45],[&amp;quot;+2^-5&amp;quot;,3.14319e-08,8.43743],[&amp;quot;+2^-4&amp;quot;,3.31291e-08,7.77058],[&amp;quot;+2^-3&amp;quot;,3.58597e-08,3.99659],[&amp;quot;+2^-2&amp;quot;,4.14639e-08,2.29246],[&amp;quot;+2^-1&amp;quot;,8.02409e-08,1.34622],[&amp;quot;+2^0&amp;quot;,8.66201e-08,1.45324],[&amp;quot;+2^1&amp;quot;,8.8654e-08,1.48737],[&amp;quot;+2^2&amp;quot;,6.7976e-08,1.14045],[&amp;quot;+2^3&amp;quot;,1.33955e-07,2.2474],[&amp;quot;+2^4&amp;quot;,2.53131e-14,4.24683e-07],[&amp;quot;+2^5&amp;quot;,0,null],[&amp;quot;+2^6&amp;quot;,0,null],[&amp;quot;+2^7&amp;quot;,0,null],[&amp;quot;+2^8&amp;quot;,0,null],[&amp;quot;+2^9&amp;quot;,0,null],[&amp;quot;+2^10&amp;quot;,0,null],[&amp;quot;+2^11&amp;quot;,0,null],[&amp;quot;+2^12&amp;quot;,0,null],[&amp;quot;+2^13&amp;quot;,0,null],[&amp;quot;+2^14&amp;quot;,0,null],[&amp;quot;+2^15&amp;quot;,0,null],[&amp;quot;+2^16&amp;quot;,0,null],[&amp;quot;+2^17&amp;quot;,0,null],[&amp;quot;+2^18&amp;quot;,0,null],[&amp;quot;+2^19&amp;quot;,0,null],[&amp;quot;+2^20&amp;quot;,0,null],[&amp;quot;+2^21&amp;quot;,0,null],[&amp;quot;+2^22&amp;quot;,0,null],[&amp;quot;+2^23&amp;quot;,0,null],[&amp;quot;+2^24&amp;quot;,0,null],[&amp;quot;+2^25&amp;quot;,0,null],[&amp;quot;+2^26&amp;quot;,0,null],[&amp;quot;+2^27&amp;quot;,0,null],[&amp;quot;+2^28&amp;quot;,0,null],[&amp;quot;+2^29&amp;quot;,0,null],[&amp;quot;+2^30&amp;quot;,0,null],[&amp;quot;+2^31&amp;quot;,0,null],[&amp;quot;+2^32&amp;quot;,0,null],[&amp;quot;+2^33&amp;quot;,0,null],[&amp;quot;+2^34&amp;quot;,0,null],[&amp;quot;+2^35&amp;quot;,0,null],[&amp;quot;+2^36&amp;quot;,0,null],[&amp;quot;+2^37&amp;quot;,0,null],[&amp;quot;+2^38&amp;quot;,0,null],[&amp;quot;+2^39&amp;quot;,0,null],[&amp;quot;+2^40&amp;quot;,0,null],[&amp;quot;+2^41&amp;quot;,0,null],[&amp;quot;+2^42&amp;quot;,0,null],[&amp;quot;+2^43&amp;quot;,0,null],[&amp;quot;+2^44&amp;quot;,0,null],[&amp;quot;+2^45&amp;quot;,0,null],[&amp;quot;+2^46&amp;quot;,0,null],[&amp;quot;+2^47&amp;quot;,0,null],[&amp;quot;+2^48&amp;quot;,0,null],[&amp;quot;+2^49&amp;quot;,0,null],[&amp;quot;+2^50&amp;quot;,0,null],[&amp;quot;+2^51&amp;quot;,0,null],[&amp;quot;+2^52&amp;quot;,0,null],[&amp;quot;+2^53&amp;quot;,0,null],[&amp;quot;+2^54&amp;quot;,0,null],[&amp;quot;+2^55&amp;quot;,0,null],[&amp;quot;+2^56&amp;quot;,0,null],[&amp;quot;+2^57&amp;quot;,0,null],[&amp;quot;+2^58&amp;quot;,0,null],[&amp;quot;+2^59&amp;quot;,0,null],[&amp;quot;+2^60&amp;quot;,0,null],[&amp;quot;+2^61&amp;quot;,0,null],[&amp;quot;+2^62&amp;quot;,0,null],[&amp;quot;+2^63&amp;quot;,0,null],[&amp;quot;+2^64&amp;quot;,0,null],[&amp;quot;+2^65&amp;quot;,0,null],[&amp;quot;+2^66&amp;quot;,0,null],[&amp;quot;+2^67&amp;quot;,0,null],[&amp;quot;+2^68&amp;quot;,0,null],[&amp;quot;+2^69&amp;quot;,0,null],[&amp;quot;+2^70&amp;quot;,0,null],[&amp;quot;+2^71&amp;quot;,0,null],[&amp;quot;+2^72&amp;quot;,0,null],[&amp;quot;+2^73&amp;quot;,0,null],[&amp;quot;+2^74&amp;quot;,0,null],[&amp;quot;+2^75&amp;quot;,0,null],[&amp;quot;+2^76&amp;quot;,0,null],[&amp;quot;+2^77&amp;quot;,0,null],[&amp;quot;+2^78&amp;quot;,0,null],[&amp;quot;+2^79&amp;quot;,0,null],[&amp;quot;+2^80&amp;quot;,0,null],[&amp;quot;+2^81&amp;quot;,0,null],[&amp;quot;+2^82&amp;quot;,0,null],[&amp;quot;+2^83&amp;quot;,0,null],[&amp;quot;+2^84&amp;quot;,0,null],[&amp;quot;+2^85&amp;quot;,0,null],[&amp;quot;+2^86&amp;quot;,0,null],[&amp;quot;+2^87&amp;quot;,0,null],[&amp;quot;+2^88&amp;quot;,0,null],[&amp;quot;+2^89&amp;quot;,0,null],[&amp;quot;+2^90&amp;quot;,0,null],[&amp;quot;+2^91&amp;quot;,0,null],[&amp;quot;+2^92&amp;quot;,0,null],[&amp;quot;+2^93&amp;quot;,0,null],[&amp;quot;+2^94&amp;quot;,0,null],[&amp;quot;+2^95&amp;quot;,0,null],[&amp;quot;+2^96&amp;quot;,0,null],[&amp;quot;+2^97&amp;quot;,0,null],[&amp;quot;+2^98&amp;quot;,0,null],[&amp;quot;+2^99&amp;quot;,0,null],[&amp;quot;+2^100&amp;quot;,0,null],[&amp;quot;+2^101&amp;quot;,0,null],[&amp;quot;+2^102&amp;quot;,0,null],[&amp;quot;+2^103&amp;quot;,0,null],[&amp;quot;+2^104&amp;quot;,0,null],[&amp;quot;+2^105&amp;quot;,0,null],[&amp;quot;+2^106&amp;quot;,0,null],[&amp;quot;+2^107&amp;quot;,0,null],[&amp;quot;+2^108&amp;quot;,0,null],[&amp;quot;+2^109&amp;quot;,0,null],[&amp;quot;+2^110&amp;quot;,0,null],[&amp;quot;+2^111&amp;quot;,0,null],[&amp;quot;+2^112&amp;quot;,0,null],[&amp;quot;+2^113&amp;quot;,0,null],[&amp;quot;+2^114&amp;quot;,0,null],[&amp;quot;+2^115&amp;quot;,0,null],[&amp;quot;+2^116&amp;quot;,0,null],[&amp;quot;+2^117&amp;quot;,0,null],[&amp;quot;+2^118&amp;quot;,0,null],[&amp;quot;+2^119&amp;quot;,0,null],[&amp;quot;+2^120&amp;quot;,0,null],[&amp;quot;+2^121&amp;quot;,0,null],[&amp;quot;+2^122&amp;quot;,0,null],[&amp;quot;+2^123&amp;quot;,0,null],[&amp;quot;+2^124&amp;quot;,0,null],[&amp;quot;+2^125&amp;quot;,0,null],[&amp;quot;+2^126&amp;quot;,0,null],[&amp;quot;+2^127&amp;quot;,0,null] ]&lt;br /&gt;
 }}&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Key observation:&#039;&#039;&#039;  &lt;br /&gt;
For &amp;lt;math&amp;gt;|x| \ll 1&amp;lt;/math&amp;gt;, the ULP error grows rapidly and reaches extremely large values due to loss of significance. &lt;br /&gt;
For sufficiently large &amp;lt;math&amp;gt;x&amp;lt;/math&amp;gt;, both numerator and denominator overflow, leading to NaN results.&lt;br /&gt;
&lt;br /&gt;
=== expm1 ===&lt;br /&gt;
&lt;br /&gt;
This graph visualizes the ULP error of the &amp;lt;code&amp;gt;expm1&amp;lt;/code&amp;gt;-based formulation.&lt;br /&gt;
Using &amp;lt;code&amp;gt;expm1&amp;lt;/code&amp;gt; significantly improves numerical accuracy for small input values by avoiding catastrophic cancellation in the expression &amp;lt;math&amp;gt;e^{2x} - 1&amp;lt;/math&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
{{#widget:FloatErrorGraph|id=tanhf_expm1&lt;br /&gt;
  |data=[ [&amp;quot;-2^127&amp;quot;,0,1.17549e-38],[&amp;quot;-2^126&amp;quot;,0,1.17549e-38],[&amp;quot;-2^125&amp;quot;,0,1.17549e-38],[&amp;quot;-2^124&amp;quot;,0,1.17549e-38],[&amp;quot;-2^123&amp;quot;,0,1.17549e-38],[&amp;quot;-2^122&amp;quot;,0,1.17549e-38],[&amp;quot;-2^121&amp;quot;,0,1.17549e-38],[&amp;quot;-2^120&amp;quot;,0,1.17549e-38],[&amp;quot;-2^119&amp;quot;,0,1.17549e-38],[&amp;quot;-2^118&amp;quot;,0,1.17549e-38],[&amp;quot;-2^117&amp;quot;,0,1.17549e-38],[&amp;quot;-2^116&amp;quot;,0,1.17549e-38],[&amp;quot;-2^115&amp;quot;,0,1.17549e-38],[&amp;quot;-2^114&amp;quot;,0,1.17549e-38],[&amp;quot;-2^113&amp;quot;,0,1.17549e-38],[&amp;quot;-2^112&amp;quot;,0,1.17549e-38],[&amp;quot;-2^111&amp;quot;,0,1.17549e-38],[&amp;quot;-2^110&amp;quot;,0,1.17549e-38],[&amp;quot;-2^109&amp;quot;,0,1.17549e-38],[&amp;quot;-2^108&amp;quot;,0,1.17549e-38],[&amp;quot;-2^107&amp;quot;,0,1.17549e-38],[&amp;quot;-2^106&amp;quot;,0,1.17549e-38],[&amp;quot;-2^105&amp;quot;,0,1.17549e-38],[&amp;quot;-2^104&amp;quot;,0,1.17549e-38],[&amp;quot;-2^103&amp;quot;,0,1.17549e-38],[&amp;quot;-2^102&amp;quot;,0,1.17549e-38],[&amp;quot;-2^101&amp;quot;,0,1.17549e-38],[&amp;quot;-2^100&amp;quot;,0,1.17549e-38],[&amp;quot;-2^99&amp;quot;,0,1.17549e-38],[&amp;quot;-2^98&amp;quot;,0,1.17549e-38],[&amp;quot;-2^97&amp;quot;,0,1.17549e-38],[&amp;quot;-2^96&amp;quot;,0,1.17549e-38],[&amp;quot;-2^95&amp;quot;,0,1.17549e-38],[&amp;quot;-2^94&amp;quot;,0,1.17549e-38],[&amp;quot;-2^93&amp;quot;,0,1.17549e-38],[&amp;quot;-2^92&amp;quot;,0,1.17549e-38],[&amp;quot;-2^91&amp;quot;,0,1.17549e-38],[&amp;quot;-2^90&amp;quot;,0,1.17549e-38],[&amp;quot;-2^89&amp;quot;,0,1.17549e-38],[&amp;quot;-2^88&amp;quot;,0,1.17549e-38],[&amp;quot;-2^87&amp;quot;,0,1.17549e-38],[&amp;quot;-2^86&amp;quot;,0,1.17549e-38],[&amp;quot;-2^85&amp;quot;,0,1.17549e-38],[&amp;quot;-2^84&amp;quot;,0,1.17549e-38],[&amp;quot;-2^83&amp;quot;,0,1.17549e-38],[&amp;quot;-2^82&amp;quot;,0,1.17549e-38],[&amp;quot;-2^81&amp;quot;,0,1.17549e-38],[&amp;quot;-2^80&amp;quot;,0,1.17549e-38],[&amp;quot;-2^79&amp;quot;,0,1.17549e-38],[&amp;quot;-2^78&amp;quot;,0,1.17549e-38],[&amp;quot;-2^77&amp;quot;,0,1.17549e-38],[&amp;quot;-2^76&amp;quot;,0,1.17549e-38],[&amp;quot;-2^75&amp;quot;,0,1.17549e-38],[&amp;quot;-2^74&amp;quot;,0,1.17549e-38],[&amp;quot;-2^73&amp;quot;,0,1.17549e-38],[&amp;quot;-2^72&amp;quot;,0,1.17549e-38],[&amp;quot;-2^71&amp;quot;,0,1.17549e-38],[&amp;quot;-2^70&amp;quot;,0,1.17549e-38],[&amp;quot;-2^69&amp;quot;,0,1.17549e-38],[&amp;quot;-2^68&amp;quot;,0,1.17549e-38],[&amp;quot;-2^67&amp;quot;,0,1.17549e-38],[&amp;quot;-2^66&amp;quot;,0,1.17549e-38],[&amp;quot;-2^65&amp;quot;,0,1.17549e-38],[&amp;quot;-2^64&amp;quot;,0,1.17549e-38],[&amp;quot;-2^63&amp;quot;,0,1.17549e-38],[&amp;quot;-2^62&amp;quot;,0,1.17549e-38],[&amp;quot;-2^61&amp;quot;,0,1.17549e-38],[&amp;quot;-2^60&amp;quot;,0,1.17549e-38],[&amp;quot;-2^59&amp;quot;,0,1.17549e-38],[&amp;quot;-2^58&amp;quot;,0,1.17549e-38],[&amp;quot;-2^57&amp;quot;,0,1.17549e-38],[&amp;quot;-2^56&amp;quot;,0,1.17549e-38],[&amp;quot;-2^55&amp;quot;,0,1.17549e-38],[&amp;quot;-2^54&amp;quot;,0,1.17549e-38],[&amp;quot;-2^53&amp;quot;,0,1.17549e-38],[&amp;quot;-2^52&amp;quot;,0,1.17549e-38],[&amp;quot;-2^51&amp;quot;,0,1.17549e-38],[&amp;quot;-2^50&amp;quot;,0,1.17549e-38],[&amp;quot;-2^49&amp;quot;,0,1.17549e-38],[&amp;quot;-2^48&amp;quot;,0,1.17549e-38],[&amp;quot;-2^47&amp;quot;,0,1.17549e-38],[&amp;quot;-2^46&amp;quot;,0,1.17549e-38],[&amp;quot;-2^45&amp;quot;,0,1.17549e-38],[&amp;quot;-2^44&amp;quot;,0,1.17549e-38],[&amp;quot;-2^43&amp;quot;,0,1.17549e-38],[&amp;quot;-2^42&amp;quot;,0,1.17549e-38],[&amp;quot;-2^41&amp;quot;,0,1.17549e-38],[&amp;quot;-2^40&amp;quot;,0,1.17549e-38],[&amp;quot;-2^39&amp;quot;,0,1.17549e-38],[&amp;quot;-2^38&amp;quot;,0,1.17549e-38],[&amp;quot;-2^37&amp;quot;,0,1.17549e-38],[&amp;quot;-2^36&amp;quot;,0,1.17549e-38],[&amp;quot;-2^35&amp;quot;,0,1.17549e-38],[&amp;quot;-2^34&amp;quot;,0,1.17549e-38],[&amp;quot;-2^33&amp;quot;,0,1.17549e-38],[&amp;quot;-2^32&amp;quot;,0,1.17549e-38],[&amp;quot;-2^31&amp;quot;,0,1.17549e-38],[&amp;quot;-2^30&amp;quot;,0,1.17549e-38],[&amp;quot;-2^29&amp;quot;,0,1.17549e-38],[&amp;quot;-2^28&amp;quot;,0,1.17549e-38],[&amp;quot;-2^27&amp;quot;,0,1.17549e-38],[&amp;quot;-2^26&amp;quot;,0,1.17549e-38],[&amp;quot;-2^25&amp;quot;,0,1.17549e-38],[&amp;quot;-2^24&amp;quot;,0,1.17549e-38],[&amp;quot;-2^23&amp;quot;,0,1.17549e-38],[&amp;quot;-2^22&amp;quot;,0,1.17549e-38],[&amp;quot;-2^21&amp;quot;,0,1.17549e-38],[&amp;quot;-2^20&amp;quot;,0,1.17549e-38],[&amp;quot;-2^19&amp;quot;,0,1.17549e-38],[&amp;quot;-2^18&amp;quot;,0,1.17549e-38],[&amp;quot;-2^17&amp;quot;,0,1.17549e-38],[&amp;quot;-2^16&amp;quot;,0,1.17549e-38],[&amp;quot;-2^15&amp;quot;,0,1.17549e-38],[&amp;quot;-2^14&amp;quot;,0,1.17549e-38],[&amp;quot;-2^13&amp;quot;,0,1.17549e-38],[&amp;quot;-2^12&amp;quot;,0,1.17549e-38],[&amp;quot;-2^11&amp;quot;,0,1.17549e-38],[&amp;quot;-2^10&amp;quot;,0,1.17549e-38],[&amp;quot;-2^9&amp;quot;,0,1.17549e-38],[&amp;quot;-2^8&amp;quot;,0,1.17549e-38],[&amp;quot;-2^7&amp;quot;,0,1.17549e-38],[&amp;quot;-2^6&amp;quot;,0,1.17549e-38],[&amp;quot;-2^5&amp;quot;,0,1.17549e-38],[&amp;quot;-2^4&amp;quot;,2.53131e-14,4.24683e-07],[&amp;quot;-2^3&amp;quot;,1.19138e-07,1.9988],[&amp;quot;-2^2&amp;quot;,1.46791e-07,2.46274],[&amp;quot;-2^1&amp;quot;,1.47723e-07,2.47839],[&amp;quot;-2^0&amp;quot;,1.40758e-07,2.36153],[&amp;quot;-2^-1&amp;quot;,1.13422e-07,2.23445],[&amp;quot;-2^-2&amp;quot;,6.38012e-08,2.14081],[&amp;quot;-2^-3&amp;quot;,2.69538e-08,1.80884],[&amp;quot;-2^-4&amp;quot;,1.21308e-08,1.62817],[&amp;quot;-2^-5&amp;quot;,5.59535e-09,1.50199],[&amp;quot;-2^-6&amp;quot;,2.69177e-09,1.44513],[&amp;quot;-2^-7&amp;quot;,1.31289e-09,1.4097],[&amp;quot;-2^-8&amp;quot;,6.49688e-10,1.3952],[&amp;quot;-2^-9&amp;quot;,3.32043e-10,1.42611],[&amp;quot;-2^-10&amp;quot;,1.66275e-10,1.4283],[&amp;quot;-2^-11&amp;quot;,8.08595e-11,1.38916],[&amp;quot;-2^-12&amp;quot;,3.8939e-11,1.33794],[&amp;quot;-2^-13&amp;quot;,1.9401e-11,1.33323],[&amp;quot;-2^-14&amp;quot;,7.88212e-12,1.08331],[&amp;quot;-2^-15&amp;quot;,3.71372e-12,1.02082],[&amp;quot;-2^-16&amp;quot;,1.82845e-12,1.0052],[&amp;quot;-2^-17&amp;quot;,9.10676e-13,1.0013],[&amp;quot;-2^-18&amp;quot;,4.54895e-13,1.00032],[&amp;quot;-2^-19&amp;quot;,2.27392e-13,1.00008],[&amp;quot;-2^-20&amp;quot;,1.13689e-13,1.00002],[&amp;quot;-2^-21&amp;quot;,5.68437e-14,1],[&amp;quot;-2^-22&amp;quot;,2.84217e-14,1],[&amp;quot;-2^-23&amp;quot;,1.42109e-14,1],[&amp;quot;-2^-24&amp;quot;,7.10543e-15,1],[&amp;quot;-2^-25&amp;quot;,3.55271e-15,1],[&amp;quot;-2^-26&amp;quot;,9.92617e-24,5.58794e-09],[&amp;quot;-2^-27&amp;quot;,1.65436e-24,1.86265e-09],[&amp;quot;-2^-28&amp;quot;,8.27181e-25,1.86265e-09],[&amp;quot;-2^-29&amp;quot;,4.1359e-25,1.86265e-09],[&amp;quot;-2^-30&amp;quot;,2.06795e-25,1.86265e-09],[&amp;quot;-2^-31&amp;quot;,1.03398e-25,1.86265e-09],[&amp;quot;-2^-32&amp;quot;,5.16988e-26,1.86265e-09],[&amp;quot;-2^-33&amp;quot;,2.58494e-26,1.86265e-09],[&amp;quot;-2^-34&amp;quot;,1.29247e-26,1.86265e-09],[&amp;quot;-2^-35&amp;quot;,6.46235e-27,1.86265e-09],[&amp;quot;-2^-36&amp;quot;,3.23117e-27,1.86265e-09],[&amp;quot;-2^-37&amp;quot;,1.61559e-27,1.86265e-09],[&amp;quot;-2^-38&amp;quot;,8.07794e-28,1.86265e-09],[&amp;quot;-2^-39&amp;quot;,4.03897e-28,1.86265e-09],[&amp;quot;-2^-40&amp;quot;,2.01948e-28,1.86265e-09],[&amp;quot;-2^-41&amp;quot;,1.00974e-28,1.86265e-09],[&amp;quot;-2^-42&amp;quot;,5.04871e-29,1.86265e-09],[&amp;quot;-2^-43&amp;quot;,2.52435e-29,1.86265e-09],[&amp;quot;-2^-44&amp;quot;,1.26218e-29,1.86265e-09],[&amp;quot;-2^-45&amp;quot;,6.31089e-30,1.86265e-09],[&amp;quot;-2^-46&amp;quot;,3.15544e-30,1.86265e-09],[&amp;quot;-2^-47&amp;quot;,1.57772e-30,1.86265e-09],[&amp;quot;-2^-48&amp;quot;,7.88861e-31,1.86265e-09],[&amp;quot;-2^-49&amp;quot;,3.9443e-31,1.86265e-09],[&amp;quot;-2^-50&amp;quot;,1.97215e-31,1.86265e-09],[&amp;quot;-2^-51&amp;quot;,9.86076e-32,1.86265e-09],[&amp;quot;-2^-52&amp;quot;,4.93038e-32,1.86265e-09],[&amp;quot;-2^-53&amp;quot;,2.46519e-32,1.86265e-09],[&amp;quot;-2^-54&amp;quot;,1.2326e-32,1.86265e-09],[&amp;quot;-2^-55&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-56&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-57&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-58&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-59&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-60&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-61&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-62&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-63&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-64&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-65&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-66&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-67&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-68&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-69&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-70&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-71&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-72&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-73&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-74&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-75&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-76&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-77&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-78&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-79&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-80&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-81&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-82&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-83&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-84&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-85&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-86&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-87&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-88&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-89&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-90&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-91&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-92&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-93&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-94&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-95&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-96&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-97&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-98&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-99&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-100&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-101&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-102&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-103&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-104&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-105&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-106&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-107&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-108&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-109&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-110&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-111&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-112&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-113&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-114&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-115&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-116&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-117&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-118&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-119&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-120&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-121&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-122&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-123&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-124&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-125&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-126&amp;quot;,0,1.17549e-38],[&amp;quot;-SN&amp;quot;,0,1.17549e-38],[&amp;quot;0&amp;quot;,0,1.17549e-38],[&amp;quot;+SN&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-126&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-125&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-124&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-123&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-122&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-121&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-120&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-119&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-118&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-117&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-116&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-115&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-114&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-113&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-112&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-111&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-110&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-109&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-108&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-107&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-106&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-105&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-104&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-103&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-102&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-101&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-100&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-99&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-98&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-97&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-96&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-95&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-94&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-93&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-92&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-91&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-90&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-89&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-88&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-87&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-86&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-85&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-84&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-83&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-82&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-81&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-80&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-79&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-78&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-77&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-76&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-75&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-74&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-73&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-72&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-71&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-70&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-69&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-68&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-67&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-66&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-65&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-64&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-63&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-62&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-61&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-60&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-59&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-58&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-57&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-56&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-55&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-54&amp;quot;,1.2326e-32,1.86265e-09],[&amp;quot;+2^-53&amp;quot;,2.46519e-32,1.86265e-09],[&amp;quot;+2^-52&amp;quot;,4.93038e-32,1.86265e-09],[&amp;quot;+2^-51&amp;quot;,9.86076e-32,1.86265e-09],[&amp;quot;+2^-50&amp;quot;,1.97215e-31,1.86265e-09],[&amp;quot;+2^-49&amp;quot;,3.9443e-31,1.86265e-09],[&amp;quot;+2^-48&amp;quot;,7.88861e-31,1.86265e-09],[&amp;quot;+2^-47&amp;quot;,1.57772e-30,1.86265e-09],[&amp;quot;+2^-46&amp;quot;,3.15544e-30,1.86265e-09],[&amp;quot;+2^-45&amp;quot;,6.31089e-30,1.86265e-09],[&amp;quot;+2^-44&amp;quot;,1.26218e-29,1.86265e-09],[&amp;quot;+2^-43&amp;quot;,2.52435e-29,1.86265e-09],[&amp;quot;+2^-42&amp;quot;,5.04871e-29,1.86265e-09],[&amp;quot;+2^-41&amp;quot;,1.00974e-28,1.86265e-09],[&amp;quot;+2^-40&amp;quot;,2.01948e-28,1.86265e-09],[&amp;quot;+2^-39&amp;quot;,4.03897e-28,1.86265e-09],[&amp;quot;+2^-38&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-37&amp;quot;,1.61559e-27,1.86265e-09],[&amp;quot;+2^-36&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-35&amp;quot;,6.46235e-27,1.86265e-09],[&amp;quot;+2^-34&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-33&amp;quot;,2.58494e-26,1.86265e-09],[&amp;quot;+2^-32&amp;quot;,5.16988e-26,1.86265e-09],[&amp;quot;+2^-31&amp;quot;,1.03398e-25,1.86265e-09],[&amp;quot;+2^-30&amp;quot;,2.06795e-25,1.86265e-09],[&amp;quot;+2^-29&amp;quot;,4.1359e-25,1.86265e-09],[&amp;quot;+2^-28&amp;quot;,8.27181e-25,1.86265e-09],[&amp;quot;+2^-27&amp;quot;,1.65436e-24,1.86265e-09],[&amp;quot;+2^-26&amp;quot;,9.92617e-24,5.58794e-09],[&amp;quot;+2^-25&amp;quot;,3.55271e-15,1],[&amp;quot;+2^-24&amp;quot;,7.10543e-15,1],[&amp;quot;+2^-23&amp;quot;,1.42109e-14,1],[&amp;quot;+2^-22&amp;quot;,2.84217e-14,1],[&amp;quot;+2^-21&amp;quot;,5.68437e-14,1],[&amp;quot;+2^-20&amp;quot;,1.13689e-13,1.00002],[&amp;quot;+2^-19&amp;quot;,2.27391e-13,1.00008],[&amp;quot;+2^-18&amp;quot;,4.54892e-13,1.00032],[&amp;quot;+2^-17&amp;quot;,9.10665e-13,1.00129],[&amp;quot;+2^-16&amp;quot;,1.82841e-12,1.00518],[&amp;quot;+2^-15&amp;quot;,3.71353e-12,1.02077],[&amp;quot;+2^-14&amp;quot;,1.39798e-11,1.92136],[&amp;quot;+2^-13&amp;quot;,2.63239e-11,1.80896],[&amp;quot;+2^-12&amp;quot;,5.37712e-11,1.84757],[&amp;quot;+2^-11&amp;quot;,1.09932e-10,1.88861],[&amp;quot;+2^-10&amp;quot;,2.24787e-10,1.93091],[&amp;quot;+2^-9&amp;quot;,4.3452e-10,1.86625],[&amp;quot;+2^-8&amp;quot;,1.01783e-09,2.18576],[&amp;quot;+2^-7&amp;quot;,2.10562e-09,2.26089],[&amp;quot;+2^-6&amp;quot;,4.03954e-09,2.16871],[&amp;quot;+2^-5&amp;quot;,8.15746e-09,2.18975],[&amp;quot;+2^-4&amp;quot;,1.5545e-08,2.08641],[&amp;quot;+2^-3&amp;quot;,2.62153e-08,2.05864],[&amp;quot;+2^-2&amp;quot;,4.09295e-08,1.73804],[&amp;quot;+2^-1&amp;quot;,8.02409e-08,1.34622],[&amp;quot;+2^0&amp;quot;,8.66201e-08,1.45324],[&amp;quot;+2^1&amp;quot;,8.8654e-08,1.48737],[&amp;quot;+2^2&amp;quot;,6.7976e-08,1.14045],[&amp;quot;+2^3&amp;quot;,7.44117e-08,1.24842],[&amp;quot;+2^4&amp;quot;,2.53131e-14,4.24683e-07],[&amp;quot;+2^5&amp;quot;,0,null],[&amp;quot;+2^6&amp;quot;,0,null],[&amp;quot;+2^7&amp;quot;,0,null],[&amp;quot;+2^8&amp;quot;,0,null],[&amp;quot;+2^9&amp;quot;,0,null],[&amp;quot;+2^10&amp;quot;,0,null],[&amp;quot;+2^11&amp;quot;,0,null],[&amp;quot;+2^12&amp;quot;,0,null],[&amp;quot;+2^13&amp;quot;,0,null],[&amp;quot;+2^14&amp;quot;,0,null],[&amp;quot;+2^15&amp;quot;,0,null],[&amp;quot;+2^16&amp;quot;,0,null],[&amp;quot;+2^17&amp;quot;,0,null],[&amp;quot;+2^18&amp;quot;,0,null],[&amp;quot;+2^19&amp;quot;,0,null],[&amp;quot;+2^20&amp;quot;,0,null],[&amp;quot;+2^21&amp;quot;,0,null],[&amp;quot;+2^22&amp;quot;,0,null],[&amp;quot;+2^23&amp;quot;,0,null],[&amp;quot;+2^24&amp;quot;,0,null],[&amp;quot;+2^25&amp;quot;,0,null],[&amp;quot;+2^26&amp;quot;,0,null],[&amp;quot;+2^27&amp;quot;,0,null],[&amp;quot;+2^28&amp;quot;,0,null],[&amp;quot;+2^29&amp;quot;,0,null],[&amp;quot;+2^30&amp;quot;,0,null],[&amp;quot;+2^31&amp;quot;,0,null],[&amp;quot;+2^32&amp;quot;,0,null],[&amp;quot;+2^33&amp;quot;,0,null],[&amp;quot;+2^34&amp;quot;,0,null],[&amp;quot;+2^35&amp;quot;,0,null],[&amp;quot;+2^36&amp;quot;,0,null],[&amp;quot;+2^37&amp;quot;,0,null],[&amp;quot;+2^38&amp;quot;,0,null],[&amp;quot;+2^39&amp;quot;,0,null],[&amp;quot;+2^40&amp;quot;,0,null],[&amp;quot;+2^41&amp;quot;,0,null],[&amp;quot;+2^42&amp;quot;,0,null],[&amp;quot;+2^43&amp;quot;,0,null],[&amp;quot;+2^44&amp;quot;,0,null],[&amp;quot;+2^45&amp;quot;,0,null],[&amp;quot;+2^46&amp;quot;,0,null],[&amp;quot;+2^47&amp;quot;,0,null],[&amp;quot;+2^48&amp;quot;,0,null],[&amp;quot;+2^49&amp;quot;,0,null],[&amp;quot;+2^50&amp;quot;,0,null],[&amp;quot;+2^51&amp;quot;,0,null],[&amp;quot;+2^52&amp;quot;,0,null],[&amp;quot;+2^53&amp;quot;,0,null],[&amp;quot;+2^54&amp;quot;,0,null],[&amp;quot;+2^55&amp;quot;,0,null],[&amp;quot;+2^56&amp;quot;,0,null],[&amp;quot;+2^57&amp;quot;,0,null],[&amp;quot;+2^58&amp;quot;,0,null],[&amp;quot;+2^59&amp;quot;,0,null],[&amp;quot;+2^60&amp;quot;,0,null],[&amp;quot;+2^61&amp;quot;,0,null],[&amp;quot;+2^62&amp;quot;,0,null],[&amp;quot;+2^63&amp;quot;,0,null],[&amp;quot;+2^64&amp;quot;,0,null],[&amp;quot;+2^65&amp;quot;,0,null],[&amp;quot;+2^66&amp;quot;,0,null],[&amp;quot;+2^67&amp;quot;,0,null],[&amp;quot;+2^68&amp;quot;,0,null],[&amp;quot;+2^69&amp;quot;,0,null],[&amp;quot;+2^70&amp;quot;,0,null],[&amp;quot;+2^71&amp;quot;,0,null],[&amp;quot;+2^72&amp;quot;,0,null],[&amp;quot;+2^73&amp;quot;,0,null],[&amp;quot;+2^74&amp;quot;,0,null],[&amp;quot;+2^75&amp;quot;,0,null],[&amp;quot;+2^76&amp;quot;,0,null],[&amp;quot;+2^77&amp;quot;,0,null],[&amp;quot;+2^78&amp;quot;,0,null],[&amp;quot;+2^79&amp;quot;,0,null],[&amp;quot;+2^80&amp;quot;,0,null],[&amp;quot;+2^81&amp;quot;,0,null],[&amp;quot;+2^82&amp;quot;,0,null],[&amp;quot;+2^83&amp;quot;,0,null],[&amp;quot;+2^84&amp;quot;,0,null],[&amp;quot;+2^85&amp;quot;,0,null],[&amp;quot;+2^86&amp;quot;,0,null],[&amp;quot;+2^87&amp;quot;,0,null],[&amp;quot;+2^88&amp;quot;,0,null],[&amp;quot;+2^89&amp;quot;,0,null],[&amp;quot;+2^90&amp;quot;,0,null],[&amp;quot;+2^91&amp;quot;,0,null],[&amp;quot;+2^92&amp;quot;,0,null],[&amp;quot;+2^93&amp;quot;,0,null],[&amp;quot;+2^94&amp;quot;,0,null],[&amp;quot;+2^95&amp;quot;,0,null],[&amp;quot;+2^96&amp;quot;,0,null],[&amp;quot;+2^97&amp;quot;,0,null],[&amp;quot;+2^98&amp;quot;,0,null],[&amp;quot;+2^99&amp;quot;,0,null],[&amp;quot;+2^100&amp;quot;,0,null],[&amp;quot;+2^101&amp;quot;,0,null],[&amp;quot;+2^102&amp;quot;,0,null],[&amp;quot;+2^103&amp;quot;,0,null],[&amp;quot;+2^104&amp;quot;,0,null],[&amp;quot;+2^105&amp;quot;,0,null],[&amp;quot;+2^106&amp;quot;,0,null],[&amp;quot;+2^107&amp;quot;,0,null],[&amp;quot;+2^108&amp;quot;,0,null],[&amp;quot;+2^109&amp;quot;,0,null],[&amp;quot;+2^110&amp;quot;,0,null],[&amp;quot;+2^111&amp;quot;,0,null],[&amp;quot;+2^112&amp;quot;,0,null],[&amp;quot;+2^113&amp;quot;,0,null],[&amp;quot;+2^114&amp;quot;,0,null],[&amp;quot;+2^115&amp;quot;,0,null],[&amp;quot;+2^116&amp;quot;,0,null],[&amp;quot;+2^117&amp;quot;,0,null],[&amp;quot;+2^118&amp;quot;,0,null],[&amp;quot;+2^119&amp;quot;,0,null],[&amp;quot;+2^120&amp;quot;,0,null],[&amp;quot;+2^121&amp;quot;,0,null],[&amp;quot;+2^122&amp;quot;,0,null],[&amp;quot;+2^123&amp;quot;,0,null],[&amp;quot;+2^124&amp;quot;,0,null],[&amp;quot;+2^125&amp;quot;,0,null],[&amp;quot;+2^126&amp;quot;,0,null],[&amp;quot;+2^127&amp;quot;,0,null] ]&lt;br /&gt;
 }}&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Key observation:&#039;&#039;&#039;  &lt;br /&gt;
Compared to direct exponential formulations, the &amp;lt;code&amp;gt;expm1&amp;lt;/code&amp;gt; approach maintains low ULP error for small and medium values of &amp;lt;math&amp;gt;x&amp;lt;/math&amp;gt;. &lt;br /&gt;
Overflow is still present for very large inputs, but the overall error behavior is substantially more stable.&lt;br /&gt;
&lt;br /&gt;
== Numerical Issues and Error Metrics ==&lt;br /&gt;
&lt;br /&gt;
To evaluate and compare different software implementations of &amp;lt;code&amp;gt;tanh&amp;lt;/code&amp;gt;, several numerical error metrics are used throughout this article. These metrics make it possible to reason about accuracy, robustness, and failure modes in floating-point arithmetic.&lt;br /&gt;
&lt;br /&gt;
=== ULP Error ===&lt;br /&gt;
ULP (Unit in the Last Place) measures the distance between two adjacent representable floating-point numbers. The ULP error of a computed result is defined as the difference, measured in ULPs, between the computed value and the correctly rounded reference value.&lt;br /&gt;
&lt;br /&gt;
An error of &amp;lt;code&amp;gt;ULP ≤ 2&amp;lt;/code&amp;gt; is commonly considered acceptable for single-precision transcendental functions, as it indicates that the result is very close to the correctly rounded value.&lt;br /&gt;
&lt;br /&gt;
Large ULP errors typically indicate catastrophic cancellation, loss of significant bits, or severe rounding effects.&lt;br /&gt;
&lt;br /&gt;
=== NaN and Overflow ===&lt;br /&gt;
NaN (Not a Number) results occur when undefined floating-point operations are performed, such as &amp;lt;code&amp;gt;inf / inf&amp;lt;/code&amp;gt;. In &amp;lt;code&amp;gt;tanh&amp;lt;/code&amp;gt; implementations, this often happens when both the numerator and denominator of a formulation overflow to infinity.&lt;br /&gt;
&lt;br /&gt;
Tracking NaN generation is critical, as NaNs can silently propagate through numerical pipelines and invalidate downstream results.&lt;br /&gt;
&lt;br /&gt;
=== Error Graphs ===&lt;br /&gt;
The error graphs shown in the following sections visualize ULP error across the full &amp;lt;code&amp;gt;float32&amp;lt;/code&amp;gt; input domain. They make it possible to identify:&lt;br /&gt;
* regions with catastrophic cancellation,&lt;br /&gt;
* input ranges that trigger overflow or NaNs,&lt;br /&gt;
* and numerical stability differences between formulations.&lt;br /&gt;
&lt;br /&gt;
These graphs are a key tool for understanding not only the maximum error, but also how errors are distributed across different input magnitudes.&lt;br /&gt;
&lt;br /&gt;
==Taylor==&lt;br /&gt;
&lt;br /&gt;
{{#widget:FloatErrorGraph|id=taylor&lt;br /&gt;
  |data=[ [&amp;quot;-2^127&amp;quot;,0,0],[&amp;quot;-2^126&amp;quot;,0,0],[&amp;quot;-2^125&amp;quot;,0,0],[&amp;quot;-2^124&amp;quot;,0,0],[&amp;quot;-2^123&amp;quot;,0,0],[&amp;quot;-2^122&amp;quot;,0,0],[&amp;quot;-2^121&amp;quot;,0,0],[&amp;quot;-2^120&amp;quot;,0,0],[&amp;quot;-2^119&amp;quot;,0,0],[&amp;quot;-2^118&amp;quot;,0,0],[&amp;quot;-2^117&amp;quot;,0,0],[&amp;quot;-2^116&amp;quot;,0,0],[&amp;quot;-2^115&amp;quot;,0,0],[&amp;quot;-2^114&amp;quot;,0,0],[&amp;quot;-2^113&amp;quot;,0,0],[&amp;quot;-2^112&amp;quot;,0,0],[&amp;quot;-2^111&amp;quot;,0,0],[&amp;quot;-2^110&amp;quot;,0,0],[&amp;quot;-2^109&amp;quot;,0,0],[&amp;quot;-2^108&amp;quot;,0,0],[&amp;quot;-2^107&amp;quot;,0,0],[&amp;quot;-2^106&amp;quot;,0,0],[&amp;quot;-2^105&amp;quot;,0,0],[&amp;quot;-2^104&amp;quot;,0,0],[&amp;quot;-2^103&amp;quot;,0,0],[&amp;quot;-2^102&amp;quot;,0,0],[&amp;quot;-2^101&amp;quot;,0,0],[&amp;quot;-2^100&amp;quot;,0,0],[&amp;quot;-2^99&amp;quot;,0,0],[&amp;quot;-2^98&amp;quot;,0,0],[&amp;quot;-2^97&amp;quot;,0,0],[&amp;quot;-2^96&amp;quot;,0,0],[&amp;quot;-2^95&amp;quot;,0,0],[&amp;quot;-2^94&amp;quot;,0,0],[&amp;quot;-2^93&amp;quot;,0,0],[&amp;quot;-2^92&amp;quot;,0,0],[&amp;quot;-2^91&amp;quot;,0,0],[&amp;quot;-2^90&amp;quot;,0,0],[&amp;quot;-2^89&amp;quot;,0,0],[&amp;quot;-2^88&amp;quot;,0,0],[&amp;quot;-2^87&amp;quot;,0,0],[&amp;quot;-2^86&amp;quot;,0,0],[&amp;quot;-2^85&amp;quot;,0,0],[&amp;quot;-2^84&amp;quot;,0,0],[&amp;quot;-2^83&amp;quot;,0,0],[&amp;quot;-2^82&amp;quot;,0,0],[&amp;quot;-2^81&amp;quot;,0,0],[&amp;quot;-2^80&amp;quot;,0,0],[&amp;quot;-2^79&amp;quot;,0,0],[&amp;quot;-2^78&amp;quot;,0,0],[&amp;quot;-2^77&amp;quot;,0,0],[&amp;quot;-2^76&amp;quot;,0,0],[&amp;quot;-2^75&amp;quot;,0,0],[&amp;quot;-2^74&amp;quot;,0,0],[&amp;quot;-2^73&amp;quot;,0,0],[&amp;quot;-2^72&amp;quot;,0,0],[&amp;quot;-2^71&amp;quot;,0,0],[&amp;quot;-2^70&amp;quot;,0,0],[&amp;quot;-2^69&amp;quot;,0,0],[&amp;quot;-2^68&amp;quot;,0,0],[&amp;quot;-2^67&amp;quot;,0,0],[&amp;quot;-2^66&amp;quot;,0,0],[&amp;quot;-2^65&amp;quot;,0,0],[&amp;quot;-2^64&amp;quot;,0,0],[&amp;quot;-2^63&amp;quot;,1.84467e+19,3.09485e+26],[&amp;quot;-2^62&amp;quot;,9.22336e+18,1.54742e+26],[&amp;quot;-2^61&amp;quot;,4.61168e+18,7.73712e+25],[&amp;quot;-2^60&amp;quot;,2.30584e+18,3.86856e+25],[&amp;quot;-2^59&amp;quot;,1.15292e+18,1.93428e+25],[&amp;quot;-2^58&amp;quot;,5.7646e+17,9.6714e+24],[&amp;quot;-2^57&amp;quot;,2.8823e+17,4.8357e+24],[&amp;quot;-2^56&amp;quot;,1.44115e+17,2.41785e+24],[&amp;quot;-2^55&amp;quot;,7.20575e+16,1.20892e+24],[&amp;quot;-2^54&amp;quot;,3.60288e+16,6.04462e+23],[&amp;quot;-2^53&amp;quot;,1.80144e+16,3.02231e+23],[&amp;quot;-2^52&amp;quot;,9.00719e+15,1.51116e+23],[&amp;quot;-2^51&amp;quot;,4.5036e+15,7.55578e+22],[&amp;quot;-2^50&amp;quot;,2.2518e+15,3.77789e+22],[&amp;quot;-2^49&amp;quot;,1.1259e+15,1.88894e+22],[&amp;quot;-2^48&amp;quot;,5.62949e+14,9.44472e+21],[&amp;quot;-2^47&amp;quot;,2.81475e+14,4.72236e+21],[&amp;quot;-2^46&amp;quot;,1.40737e+14,2.36118e+21],[&amp;quot;-2^45&amp;quot;,7.03687e+13,1.18059e+21],[&amp;quot;-2^44&amp;quot;,3.51843e+13,5.90295e+20],[&amp;quot;-2^43&amp;quot;,1.75922e+13,2.95148e+20],[&amp;quot;-2^42&amp;quot;,8.79608e+12,1.47574e+20],[&amp;quot;-2^41&amp;quot;,4.39804e+12,7.37869e+19],[&amp;quot;-2^40&amp;quot;,2.19902e+12,3.68935e+19],[&amp;quot;-2^39&amp;quot;,1.09951e+12,1.84467e+19],[&amp;quot;-2^38&amp;quot;,5.49755e+11,9.22336e+18],[&amp;quot;-2^37&amp;quot;,2.74878e+11,4.61168e+18],[&amp;quot;-2^36&amp;quot;,1.37439e+11,2.30584e+18],[&amp;quot;-2^35&amp;quot;,6.87194e+10,1.15292e+18],[&amp;quot;-2^34&amp;quot;,3.43597e+10,5.7646e+17],[&amp;quot;-2^33&amp;quot;,1.71799e+10,2.8823e+17],[&amp;quot;-2^32&amp;quot;,8.58993e+09,1.44115e+17],[&amp;quot;-2^31&amp;quot;,4.29085e+09,7.19885e+16],[&amp;quot;-2^30&amp;quot;,2.14542e+09,3.59943e+16],[&amp;quot;-2^29&amp;quot;,1.07271e+09,1.79971e+16],[&amp;quot;-2^28&amp;quot;,5.36356e+08,8.99856e+15],[&amp;quot;-2^27&amp;quot;,2.68178e+08,4.49928e+15],[&amp;quot;-2^26&amp;quot;,1.34089e+08,2.24964e+15],[&amp;quot;-2^25&amp;quot;,6.70445e+07,1.12482e+15],[&amp;quot;-2^24&amp;quot;,3.35223e+07,5.6241e+14],[&amp;quot;-2^23&amp;quot;,1.67611e+07,2.81205e+14],[&amp;quot;-2^22&amp;quot;,8.38056e+06,1.40603e+14],[&amp;quot;-2^21&amp;quot;,4.19028e+06,7.03013e+13],[&amp;quot;-2^20&amp;quot;,2.09514e+06,3.51506e+13],[&amp;quot;-2^19&amp;quot;,1.04757e+06,1.75753e+13],[&amp;quot;-2^18&amp;quot;,523784,8.78764e+12],[&amp;quot;-2^17&amp;quot;,261892,4.39381e+12],[&amp;quot;-2^16&amp;quot;,130945,2.1969e+12],[&amp;quot;-2^15&amp;quot;,65472.2,1.09844e+12],[&amp;quot;-2^14&amp;quot;,32735.6,5.49212e+11],[&amp;quot;-2^13&amp;quot;,16367.3,2.74598e+11],[&amp;quot;-2^12&amp;quot;,8183.15,1.3729e+11],[&amp;quot;-2^11&amp;quot;,4091.07,6.86368e+10],[&amp;quot;-2^10&amp;quot;,2045.04,3.431e+10],[&amp;quot;-2^9&amp;quot;,1022.02,1.71466e+10],[&amp;quot;-2^8&amp;quot;,510.509,8.56492e+09],[&amp;quot;-2^7&amp;quot;,254.754,4.27406e+09],[&amp;quot;-2^6&amp;quot;,126.876,2.12862e+09],[&amp;quot;-2^5&amp;quot;,62.9354,1.05588e+09],[&amp;quot;-2^4&amp;quot;,30.9629,5.19471e+08],[&amp;quot;-2^3&amp;quot;,14.9718,2.51185e+08],[&amp;quot;-2^2&amp;quot;,6.96672,1.16882e+08],[&amp;quot;-2^1&amp;quot;,2.94633,4.94312e+07],[&amp;quot;-2^0&amp;quot;,0.940715,1.57826e+07],[&amp;quot;-2^-1&amp;quot;,0.127635,2.14135e+06],[&amp;quot;-2^-2&amp;quot;,0.00318735,106950],[&amp;quot;-2^-3&amp;quot;,3.11475e-05,2090.28],[&amp;quot;-2^-4&amp;quot;,2.57737e-07,34.5929],[&amp;quot;-2^-5&amp;quot;,3.89213e-09,1.04478],[&amp;quot;-2^-6&amp;quot;,9.51472e-10,0.510818],[&amp;quot;-2^-7&amp;quot;,4.66402e-10,0.500795],[&amp;quot;-2^-8&amp;quot;,2.32917e-10,0.500185],[&amp;quot;-2^-9&amp;quot;,1.16423e-10,0.500033],[&amp;quot;-2^-10&amp;quot;,5.82082e-11,0.500004],[&amp;quot;-2^-11&amp;quot;,2.91037e-11,0.499998],[&amp;quot;-2^-12&amp;quot;,1.45519e-11,0.5],[&amp;quot;-2^-13&amp;quot;,4.85062e-12,0.333332],[&amp;quot;-2^-14&amp;quot;,6.06328e-13,0.0833331],[&amp;quot;-2^-15&amp;quot;,7.5791e-14,0.0208333],[&amp;quot;-2^-16&amp;quot;,9.47387e-15,0.00520832],[&amp;quot;-2^-17&amp;quot;,1.18424e-15,0.00130208],[&amp;quot;-2^-18&amp;quot;,1.48029e-16,0.00032552],[&amp;quot;-2^-19&amp;quot;,1.85039e-17,8.13808e-05],[&amp;quot;-2^-20&amp;quot;,2.31304e-18,2.03457e-05],[&amp;quot;-2^-21&amp;quot;,2.89156e-19,5.08688e-06],[&amp;quot;-2^-22&amp;quot;,3.61577e-20,1.27219e-06],[&amp;quot;-2^-23&amp;quot;,4.52633e-21,3.18512e-07],[&amp;quot;-2^-24&amp;quot;,5.691e-22,8.00937e-08],[&amp;quot;-2^-25&amp;quot;,7.27919e-23,2.04891e-08],[&amp;quot;-2^-26&amp;quot;,9.92617e-24,5.58794e-09],[&amp;quot;-2^-27&amp;quot;,1.65436e-24,1.86265e-09],[&amp;quot;-2^-28&amp;quot;,8.27181e-25,1.86265e-09],[&amp;quot;-2^-29&amp;quot;,4.1359e-25,1.86265e-09],[&amp;quot;-2^-30&amp;quot;,2.06795e-25,1.86265e-09],[&amp;quot;-2^-31&amp;quot;,1.03398e-25,1.86265e-09],[&amp;quot;-2^-32&amp;quot;,5.16988e-26,1.86265e-09],[&amp;quot;-2^-33&amp;quot;,2.58494e-26,1.86265e-09],[&amp;quot;-2^-34&amp;quot;,1.29247e-26,1.86265e-09],[&amp;quot;-2^-35&amp;quot;,6.46235e-27,1.86265e-09],[&amp;quot;-2^-36&amp;quot;,3.23117e-27,1.86265e-09],[&amp;quot;-2^-37&amp;quot;,1.61559e-27,1.86265e-09],[&amp;quot;-2^-38&amp;quot;,8.07794e-28,1.86265e-09],[&amp;quot;-2^-39&amp;quot;,4.03897e-28,1.86265e-09],[&amp;quot;-2^-40&amp;quot;,2.01948e-28,1.86265e-09],[&amp;quot;-2^-41&amp;quot;,1.00974e-28,1.86265e-09],[&amp;quot;-2^-42&amp;quot;,5.04871e-29,1.86265e-09],[&amp;quot;-2^-43&amp;quot;,2.52435e-29,1.86265e-09],[&amp;quot;-2^-44&amp;quot;,1.26218e-29,1.86265e-09],[&amp;quot;-2^-45&amp;quot;,6.31089e-30,1.86265e-09],[&amp;quot;-2^-46&amp;quot;,3.15544e-30,1.86265e-09],[&amp;quot;-2^-47&amp;quot;,1.57772e-30,1.86265e-09],[&amp;quot;-2^-48&amp;quot;,7.88861e-31,1.86265e-09],[&amp;quot;-2^-49&amp;quot;,3.9443e-31,1.86265e-09],[&amp;quot;-2^-50&amp;quot;,1.97215e-31,1.86265e-09],[&amp;quot;-2^-51&amp;quot;,9.86076e-32,1.86265e-09],[&amp;quot;-2^-52&amp;quot;,4.93038e-32,1.86265e-09],[&amp;quot;-2^-53&amp;quot;,2.46519e-32,1.86265e-09],[&amp;quot;-2^-54&amp;quot;,1.2326e-32,1.86265e-09],[&amp;quot;-2^-55&amp;quot;,0,0],[&amp;quot;-2^-56&amp;quot;,0,0],[&amp;quot;-2^-57&amp;quot;,0,0],[&amp;quot;-2^-58&amp;quot;,0,0],[&amp;quot;-2^-59&amp;quot;,0,0],[&amp;quot;-2^-60&amp;quot;,0,0],[&amp;quot;-2^-61&amp;quot;,0,0],[&amp;quot;-2^-62&amp;quot;,0,0],[&amp;quot;-2^-63&amp;quot;,0,0],[&amp;quot;-2^-64&amp;quot;,0,0],[&amp;quot;-2^-65&amp;quot;,0,0],[&amp;quot;-2^-66&amp;quot;,0,0],[&amp;quot;-2^-67&amp;quot;,0,0],[&amp;quot;-2^-68&amp;quot;,0,0],[&amp;quot;-2^-69&amp;quot;,0,0],[&amp;quot;-2^-70&amp;quot;,0,0],[&amp;quot;-2^-71&amp;quot;,0,0],[&amp;quot;-2^-72&amp;quot;,0,0],[&amp;quot;-2^-73&amp;quot;,0,0],[&amp;quot;-2^-74&amp;quot;,0,0],[&amp;quot;-2^-75&amp;quot;,0,0],[&amp;quot;-2^-76&amp;quot;,0,0],[&amp;quot;-2^-77&amp;quot;,0,0],[&amp;quot;-2^-78&amp;quot;,0,0],[&amp;quot;-2^-79&amp;quot;,0,0],[&amp;quot;-2^-80&amp;quot;,0,0],[&amp;quot;-2^-81&amp;quot;,0,0],[&amp;quot;-2^-82&amp;quot;,0,0],[&amp;quot;-2^-83&amp;quot;,0,0],[&amp;quot;-2^-84&amp;quot;,0,0],[&amp;quot;-2^-85&amp;quot;,0,0],[&amp;quot;-2^-86&amp;quot;,0,0],[&amp;quot;-2^-87&amp;quot;,0,0],[&amp;quot;-2^-88&amp;quot;,0,0],[&amp;quot;-2^-89&amp;quot;,0,0],[&amp;quot;-2^-90&amp;quot;,0,0],[&amp;quot;-2^-91&amp;quot;,0,0],[&amp;quot;-2^-92&amp;quot;,0,0],[&amp;quot;-2^-93&amp;quot;,0,0],[&amp;quot;-2^-94&amp;quot;,0,0],[&amp;quot;-2^-95&amp;quot;,0,0],[&amp;quot;-2^-96&amp;quot;,0,0],[&amp;quot;-2^-97&amp;quot;,0,0],[&amp;quot;-2^-98&amp;quot;,0,0],[&amp;quot;-2^-99&amp;quot;,0,0],[&amp;quot;-2^-100&amp;quot;,0,0],[&amp;quot;-2^-101&amp;quot;,0,0],[&amp;quot;-2^-102&amp;quot;,0,0],[&amp;quot;-2^-103&amp;quot;,0,0],[&amp;quot;-2^-104&amp;quot;,0,0],[&amp;quot;-2^-105&amp;quot;,0,0],[&amp;quot;-2^-106&amp;quot;,0,0],[&amp;quot;-2^-107&amp;quot;,0,0],[&amp;quot;-2^-108&amp;quot;,0,0],[&amp;quot;-2^-109&amp;quot;,0,0],[&amp;quot;-2^-110&amp;quot;,0,0],[&amp;quot;-2^-111&amp;quot;,0,0],[&amp;quot;-2^-112&amp;quot;,0,0],[&amp;quot;-2^-113&amp;quot;,0,0],[&amp;quot;-2^-114&amp;quot;,0,0],[&amp;quot;-2^-115&amp;quot;,0,0],[&amp;quot;-2^-116&amp;quot;,0,0],[&amp;quot;-2^-117&amp;quot;,0,0],[&amp;quot;-2^-118&amp;quot;,0,0],[&amp;quot;-2^-119&amp;quot;,0,0],[&amp;quot;-2^-120&amp;quot;,0,0],[&amp;quot;-2^-121&amp;quot;,0,0],[&amp;quot;-2^-122&amp;quot;,0,0],[&amp;quot;-2^-123&amp;quot;,0,0],[&amp;quot;-2^-124&amp;quot;,0,0],[&amp;quot;-2^-125&amp;quot;,0,0],[&amp;quot;-2^-126&amp;quot;,0,0],[&amp;quot;-SN&amp;quot;,0,0],[&amp;quot;0&amp;quot;,0,0],[&amp;quot;+SN&amp;quot;,0,0],[&amp;quot;+2^-126&amp;quot;,0,0],[&amp;quot;+2^-125&amp;quot;,0,0],[&amp;quot;+2^-124&amp;quot;,0,0],[&amp;quot;+2^-123&amp;quot;,0,0],[&amp;quot;+2^-122&amp;quot;,0,0],[&amp;quot;+2^-121&amp;quot;,0,0],[&amp;quot;+2^-120&amp;quot;,0,0],[&amp;quot;+2^-119&amp;quot;,0,0],[&amp;quot;+2^-118&amp;quot;,0,0],[&amp;quot;+2^-117&amp;quot;,0,0],[&amp;quot;+2^-116&amp;quot;,0,0],[&amp;quot;+2^-115&amp;quot;,0,0],[&amp;quot;+2^-114&amp;quot;,0,0],[&amp;quot;+2^-113&amp;quot;,0,0],[&amp;quot;+2^-112&amp;quot;,0,0],[&amp;quot;+2^-111&amp;quot;,0,0],[&amp;quot;+2^-110&amp;quot;,0,0],[&amp;quot;+2^-109&amp;quot;,0,0],[&amp;quot;+2^-108&amp;quot;,0,0],[&amp;quot;+2^-107&amp;quot;,0,0],[&amp;quot;+2^-106&amp;quot;,0,0],[&amp;quot;+2^-105&amp;quot;,0,0],[&amp;quot;+2^-104&amp;quot;,0,0],[&amp;quot;+2^-103&amp;quot;,0,0],[&amp;quot;+2^-102&amp;quot;,0,0],[&amp;quot;+2^-101&amp;quot;,0,0],[&amp;quot;+2^-100&amp;quot;,0,0],[&amp;quot;+2^-99&amp;quot;,0,0],[&amp;quot;+2^-98&amp;quot;,0,0],[&amp;quot;+2^-97&amp;quot;,0,0],[&amp;quot;+2^-96&amp;quot;,0,0],[&amp;quot;+2^-95&amp;quot;,0,0],[&amp;quot;+2^-94&amp;quot;,0,0],[&amp;quot;+2^-93&amp;quot;,0,0],[&amp;quot;+2^-92&amp;quot;,0,0],[&amp;quot;+2^-91&amp;quot;,0,0],[&amp;quot;+2^-90&amp;quot;,0,0],[&amp;quot;+2^-89&amp;quot;,0,0],[&amp;quot;+2^-88&amp;quot;,0,0],[&amp;quot;+2^-87&amp;quot;,0,0],[&amp;quot;+2^-86&amp;quot;,0,0],[&amp;quot;+2^-85&amp;quot;,0,0],[&amp;quot;+2^-84&amp;quot;,0,0],[&amp;quot;+2^-83&amp;quot;,0,0],[&amp;quot;+2^-82&amp;quot;,0,0],[&amp;quot;+2^-81&amp;quot;,0,0],[&amp;quot;+2^-80&amp;quot;,0,0],[&amp;quot;+2^-79&amp;quot;,0,0],[&amp;quot;+2^-78&amp;quot;,0,0],[&amp;quot;+2^-77&amp;quot;,0,0],[&amp;quot;+2^-76&amp;quot;,0,0],[&amp;quot;+2^-75&amp;quot;,0,0],[&amp;quot;+2^-74&amp;quot;,0,0],[&amp;quot;+2^-73&amp;quot;,0,0],[&amp;quot;+2^-72&amp;quot;,0,0],[&amp;quot;+2^-71&amp;quot;,0,0],[&amp;quot;+2^-70&amp;quot;,0,0],[&amp;quot;+2^-69&amp;quot;,0,0],[&amp;quot;+2^-68&amp;quot;,0,0],[&amp;quot;+2^-67&amp;quot;,0,0],[&amp;quot;+2^-66&amp;quot;,0,0],[&amp;quot;+2^-65&amp;quot;,0,0],[&amp;quot;+2^-64&amp;quot;,0,0],[&amp;quot;+2^-63&amp;quot;,0,0],[&amp;quot;+2^-62&amp;quot;,0,0],[&amp;quot;+2^-61&amp;quot;,0,0],[&amp;quot;+2^-60&amp;quot;,0,0],[&amp;quot;+2^-59&amp;quot;,0,0],[&amp;quot;+2^-58&amp;quot;,0,0],[&amp;quot;+2^-57&amp;quot;,0,0],[&amp;quot;+2^-56&amp;quot;,0,0],[&amp;quot;+2^-55&amp;quot;,0,0],[&amp;quot;+2^-54&amp;quot;,1.2326e-32,1.86265e-09],[&amp;quot;+2^-53&amp;quot;,2.46519e-32,1.86265e-09],[&amp;quot;+2^-52&amp;quot;,4.93038e-32,1.86265e-09],[&amp;quot;+2^-51&amp;quot;,9.86076e-32,1.86265e-09],[&amp;quot;+2^-50&amp;quot;,1.97215e-31,1.86265e-09],[&amp;quot;+2^-49&amp;quot;,3.9443e-31,1.86265e-09],[&amp;quot;+2^-48&amp;quot;,7.88861e-31,1.86265e-09],[&amp;quot;+2^-47&amp;quot;,1.57772e-30,1.86265e-09],[&amp;quot;+2^-46&amp;quot;,3.15544e-30,1.86265e-09],[&amp;quot;+2^-45&amp;quot;,6.31089e-30,1.86265e-09],[&amp;quot;+2^-44&amp;quot;,1.26218e-29,1.86265e-09],[&amp;quot;+2^-43&amp;quot;,2.52435e-29,1.86265e-09],[&amp;quot;+2^-42&amp;quot;,5.04871e-29,1.86265e-09],[&amp;quot;+2^-41&amp;quot;,1.00974e-28,1.86265e-09],[&amp;quot;+2^-40&amp;quot;,2.01948e-28,1.86265e-09],[&amp;quot;+2^-39&amp;quot;,4.03897e-28,1.86265e-09],[&amp;quot;+2^-38&amp;quot;,8.07794e-28,1.86265e-09],[&amp;quot;+2^-37&amp;quot;,1.61559e-27,1.86265e-09],[&amp;quot;+2^-36&amp;quot;,3.23117e-27,1.86265e-09],[&amp;quot;+2^-35&amp;quot;,6.46235e-27,1.86265e-09],[&amp;quot;+2^-34&amp;quot;,1.29247e-26,1.86265e-09],[&amp;quot;+2^-33&amp;quot;,2.58494e-26,1.86265e-09],[&amp;quot;+2^-32&amp;quot;,5.16988e-26,1.86265e-09],[&amp;quot;+2^-31&amp;quot;,1.03398e-25,1.86265e-09],[&amp;quot;+2^-30&amp;quot;,2.06795e-25,1.86265e-09],[&amp;quot;+2^-29&amp;quot;,4.1359e-25,1.86265e-09],[&amp;quot;+2^-28&amp;quot;,8.27181e-25,1.86265e-09],[&amp;quot;+2^-27&amp;quot;,1.65436e-24,1.86265e-09],[&amp;quot;+2^-26&amp;quot;,9.92617e-24,5.58794e-09],[&amp;quot;+2^-25&amp;quot;,7.27919e-23,2.04891e-08],[&amp;quot;+2^-24&amp;quot;,5.691e-22,8.00937e-08],[&amp;quot;+2^-23&amp;quot;,4.52633e-21,3.18512e-07],[&amp;quot;+2^-22&amp;quot;,3.61577e-20,1.27219e-06],[&amp;quot;+2^-21&amp;quot;,2.89156e-19,5.08688e-06],[&amp;quot;+2^-20&amp;quot;,2.31304e-18,2.03457e-05],[&amp;quot;+2^-19&amp;quot;,1.85039e-17,8.13808e-05],[&amp;quot;+2^-18&amp;quot;,1.48029e-16,0.00032552],[&amp;quot;+2^-17&amp;quot;,1.18424e-15,0.00130208],[&amp;quot;+2^-16&amp;quot;,9.47387e-15,0.00520832],[&amp;quot;+2^-15&amp;quot;,7.5791e-14,0.0208333],[&amp;quot;+2^-14&amp;quot;,6.06328e-13,0.0833331],[&amp;quot;+2^-13&amp;quot;,4.85062e-12,0.333332],[&amp;quot;+2^-12&amp;quot;,1.45519e-11,0.5],[&amp;quot;+2^-11&amp;quot;,2.91037e-11,0.499998],[&amp;quot;+2^-10&amp;quot;,5.82082e-11,0.500004],[&amp;quot;+2^-9&amp;quot;,1.16423e-10,0.500033],[&amp;quot;+2^-8&amp;quot;,2.32917e-10,0.500185],[&amp;quot;+2^-7&amp;quot;,4.66402e-10,0.500795],[&amp;quot;+2^-6&amp;quot;,9.51472e-10,0.510818],[&amp;quot;+2^-5&amp;quot;,3.89213e-09,1.04478],[&amp;quot;+2^-4&amp;quot;,2.57737e-07,34.5929],[&amp;quot;+2^-3&amp;quot;,3.11475e-05,2090.28],[&amp;quot;+2^-2&amp;quot;,0.00318735,106950],[&amp;quot;+2^-1&amp;quot;,0.127635,2.14135e+06],[&amp;quot;+2^0&amp;quot;,0.940715,1.57826e+07],[&amp;quot;+2^1&amp;quot;,2.94633,4.94312e+07],[&amp;quot;+2^2&amp;quot;,6.96672,1.16882e+08],[&amp;quot;+2^3&amp;quot;,14.9718,1.33874e+08],[&amp;quot;+2^4&amp;quot;,30.9629,2.59736e+08],[&amp;quot;+2^5&amp;quot;,62.9354,5.27941e+08],[&amp;quot;+2^6&amp;quot;,126.876,1.06431e+09],[&amp;quot;+2^7&amp;quot;,254.754,2.13703e+09],[&amp;quot;+2^8&amp;quot;,510.509,4.28246e+09],[&amp;quot;+2^9&amp;quot;,1022.02,8.57331e+09],[&amp;quot;+2^10&amp;quot;,2045.04,1.7155e+10],[&amp;quot;+2^11&amp;quot;,4091.07,3.43184e+10],[&amp;quot;+2^12&amp;quot;,8183.15,6.86452e+10],[&amp;quot;+2^13&amp;quot;,16367.3,1.37299e+11],[&amp;quot;+2^14&amp;quot;,32735.6,2.74606e+11],[&amp;quot;+2^15&amp;quot;,65472.2,5.4922e+11],[&amp;quot;+2^16&amp;quot;,130945,1.09845e+12],[&amp;quot;+2^17&amp;quot;,261892,2.19691e+12],[&amp;quot;+2^18&amp;quot;,523784,4.39382e+12],[&amp;quot;+2^19&amp;quot;,1.04757e+06,8.78765e+12],[&amp;quot;+2^20&amp;quot;,2.09514e+06,1.75753e+13],[&amp;quot;+2^21&amp;quot;,4.19028e+06,3.51506e+13],[&amp;quot;+2^22&amp;quot;,8.38056e+06,7.03013e+13],[&amp;quot;+2^23&amp;quot;,1.67611e+07,1.40603e+14],[&amp;quot;+2^24&amp;quot;,3.35223e+07,2.81205e+14],[&amp;quot;+2^25&amp;quot;,6.70445e+07,5.6241e+14],[&amp;quot;+2^26&amp;quot;,1.34089e+08,1.12482e+15],[&amp;quot;+2^27&amp;quot;,2.68178e+08,2.24964e+15],[&amp;quot;+2^28&amp;quot;,5.36356e+08,4.49928e+15],[&amp;quot;+2^29&amp;quot;,1.07271e+09,8.99856e+15],[&amp;quot;+2^30&amp;quot;,2.14542e+09,1.79971e+16],[&amp;quot;+2^31&amp;quot;,4.29085e+09,3.59943e+16],[&amp;quot;+2^32&amp;quot;,8.58993e+09,7.20575e+16],[&amp;quot;+2^33&amp;quot;,1.71799e+10,1.44115e+17],[&amp;quot;+2^34&amp;quot;,3.43597e+10,2.8823e+17],[&amp;quot;+2^35&amp;quot;,6.87194e+10,5.7646e+17],[&amp;quot;+2^36&amp;quot;,1.37439e+11,1.15292e+18],[&amp;quot;+2^37&amp;quot;,2.74878e+11,2.30584e+18],[&amp;quot;+2^38&amp;quot;,5.49755e+11,4.61168e+18],[&amp;quot;+2^39&amp;quot;,1.09951e+12,9.22336e+18],[&amp;quot;+2^40&amp;quot;,2.19902e+12,1.84467e+19],[&amp;quot;+2^41&amp;quot;,4.39804e+12,3.68935e+19],[&amp;quot;+2^42&amp;quot;,8.79608e+12,7.37869e+19],[&amp;quot;+2^43&amp;quot;,1.75922e+13,1.47574e+20],[&amp;quot;+2^44&amp;quot;,3.51843e+13,2.95148e+20],[&amp;quot;+2^45&amp;quot;,7.03687e+13,5.90295e+20],[&amp;quot;+2^46&amp;quot;,1.40737e+14,1.18059e+21],[&amp;quot;+2^47&amp;quot;,2.81475e+14,2.36118e+21],[&amp;quot;+2^48&amp;quot;,5.62949e+14,4.72236e+21],[&amp;quot;+2^49&amp;quot;,1.1259e+15,9.44472e+21],[&amp;quot;+2^50&amp;quot;,2.2518e+15,1.88894e+22],[&amp;quot;+2^51&amp;quot;,4.5036e+15,3.77789e+22],[&amp;quot;+2^52&amp;quot;,9.00719e+15,7.55578e+22],[&amp;quot;+2^53&amp;quot;,1.80144e+16,1.51116e+23],[&amp;quot;+2^54&amp;quot;,3.60288e+16,3.02231e+23],[&amp;quot;+2^55&amp;quot;,7.20575e+16,6.04462e+23],[&amp;quot;+2^56&amp;quot;,1.44115e+17,1.20892e+24],[&amp;quot;+2^57&amp;quot;,2.8823e+17,2.41785e+24],[&amp;quot;+2^58&amp;quot;,5.7646e+17,4.8357e+24],[&amp;quot;+2^59&amp;quot;,1.15292e+18,9.6714e+24],[&amp;quot;+2^60&amp;quot;,2.30584e+18,1.93428e+25],[&amp;quot;+2^61&amp;quot;,4.61168e+18,3.86856e+25],[&amp;quot;+2^62&amp;quot;,9.22336e+18,7.73712e+25],[&amp;quot;+2^63&amp;quot;,1.84467e+19,1.54742e+26],[&amp;quot;+2^64&amp;quot;,0,0],[&amp;quot;+2^65&amp;quot;,0,0],[&amp;quot;+2^66&amp;quot;,0,0],[&amp;quot;+2^67&amp;quot;,0,0],[&amp;quot;+2^68&amp;quot;,0,0],[&amp;quot;+2^69&amp;quot;,0,0],[&amp;quot;+2^70&amp;quot;,0,0],[&amp;quot;+2^71&amp;quot;,0,0],[&amp;quot;+2^72&amp;quot;,0,0],[&amp;quot;+2^73&amp;quot;,0,0],[&amp;quot;+2^74&amp;quot;,0,0],[&amp;quot;+2^75&amp;quot;,0,0],[&amp;quot;+2^76&amp;quot;,0,0],[&amp;quot;+2^77&amp;quot;,0,0],[&amp;quot;+2^78&amp;quot;,0,0],[&amp;quot;+2^79&amp;quot;,0,0],[&amp;quot;+2^80&amp;quot;,0,0],[&amp;quot;+2^81&amp;quot;,0,0],[&amp;quot;+2^82&amp;quot;,0,0],[&amp;quot;+2^83&amp;quot;,0,0],[&amp;quot;+2^84&amp;quot;,0,0],[&amp;quot;+2^85&amp;quot;,0,0],[&amp;quot;+2^86&amp;quot;,0,0],[&amp;quot;+2^87&amp;quot;,0,0],[&amp;quot;+2^88&amp;quot;,0,0],[&amp;quot;+2^89&amp;quot;,0,0],[&amp;quot;+2^90&amp;quot;,0,0],[&amp;quot;+2^91&amp;quot;,0,0],[&amp;quot;+2^92&amp;quot;,0,0],[&amp;quot;+2^93&amp;quot;,0,0],[&amp;quot;+2^94&amp;quot;,0,0],[&amp;quot;+2^95&amp;quot;,0,0],[&amp;quot;+2^96&amp;quot;,0,0],[&amp;quot;+2^97&amp;quot;,0,0],[&amp;quot;+2^98&amp;quot;,0,0],[&amp;quot;+2^99&amp;quot;,0,0],[&amp;quot;+2^100&amp;quot;,0,0],[&amp;quot;+2^101&amp;quot;,0,0],[&amp;quot;+2^102&amp;quot;,0,0],[&amp;quot;+2^103&amp;quot;,0,0],[&amp;quot;+2^104&amp;quot;,0,0],[&amp;quot;+2^105&amp;quot;,0,0],[&amp;quot;+2^106&amp;quot;,0,0],[&amp;quot;+2^107&amp;quot;,0,0],[&amp;quot;+2^108&amp;quot;,0,0],[&amp;quot;+2^109&amp;quot;,0,0],[&amp;quot;+2^110&amp;quot;,0,0],[&amp;quot;+2^111&amp;quot;,0,0],[&amp;quot;+2^112&amp;quot;,0,0],[&amp;quot;+2^113&amp;quot;,0,0],[&amp;quot;+2^114&amp;quot;,0,0],[&amp;quot;+2^115&amp;quot;,0,0],[&amp;quot;+2^116&amp;quot;,0,0],[&amp;quot;+2^117&amp;quot;,0,0],[&amp;quot;+2^118&amp;quot;,0,0],[&amp;quot;+2^119&amp;quot;,0,0],[&amp;quot;+2^120&amp;quot;,0,0],[&amp;quot;+2^121&amp;quot;,0,0],[&amp;quot;+2^122&amp;quot;,0,0],[&amp;quot;+2^123&amp;quot;,0,0],[&amp;quot;+2^124&amp;quot;,0,0],[&amp;quot;+2^125&amp;quot;,0,0],[&amp;quot;+2^126&amp;quot;,0,0],[&amp;quot;+2^127&amp;quot;,0,0] ]&lt;br /&gt;
 }}&lt;br /&gt;
&lt;br /&gt;
== Localized Approximation Functions ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
!Id&lt;br /&gt;
!Formula&lt;br /&gt;
!ULP &amp;lt;= 2.0&lt;br /&gt;
!NAN&lt;br /&gt;
|-&lt;br /&gt;
|signum&lt;br /&gt;
|&amp;lt;math&amp;gt;\sgn{x}&amp;lt;/math&amp;gt;&lt;br /&gt;
| -inf &amp;lt; x &amp;lt; -8.31763&lt;br /&gt;
8.31763 &amp;lt; x &amp;lt; inf&lt;br /&gt;
| -&lt;br /&gt;
|-&lt;br /&gt;
|poly1&lt;br /&gt;
|&amp;lt;math&amp;gt;x&amp;lt;/math&amp;gt;&lt;br /&gt;
| -0.000704229 &amp;lt; x &amp;lt; 0.000704229&lt;br /&gt;
| -&lt;br /&gt;
|-&lt;br /&gt;
|poly3&lt;br /&gt;
|&amp;lt;math&amp;gt;x - \frac{1}{3} x^3&amp;lt;/math&amp;gt;&lt;br /&gt;
| -0.0291781 &amp;lt; x &amp;lt; 0.0291781 &lt;br /&gt;
| -&lt;br /&gt;
|-&lt;br /&gt;
|lampert7&lt;br /&gt;
|&amp;lt;math&amp;gt;\frac{x \cdot (135135 + x^2 \cdot (17325 + x^2 \cdot (378 + x^2)))}{135135 + x^2 \cdot (62370 + x^2 \cdot (3150 + 28 \cdot x^2))}&lt;br /&gt;
&amp;lt;/math&amp;gt;&lt;br /&gt;
| -0.000947416 &amp;lt; x &amp;lt; 0.000947207&lt;br /&gt;
| -inf &amp;lt; x &amp;lt; -1.51629e+06&lt;br /&gt;
1.51629e+06 &amp;lt; x &amp;lt; inf&lt;br /&gt;
|-&lt;br /&gt;
|pade&lt;br /&gt;
|&amp;lt;math&amp;gt;x + x \cdot \frac{x^2 \cdot \left( p_1 \cdot x^2 + p_0 \right)}{\left( x^2 + q_1 \right) \cdot x^2 + q_0} &lt;br /&gt;
&lt;br /&gt;
&amp;lt;/math&amp;gt; &amp;lt;math&amp;gt;\begin{align}&lt;br /&gt;
p_0 &amp;amp;= -0.2059432032 \\&lt;br /&gt;
p_1 &amp;amp;= -0.0009577527 \\&lt;br /&gt;
q_0 &amp;amp;= 0.6178299136 \\ &lt;br /&gt;
q_1 &amp;amp;= 0.25&lt;br /&gt;
\end{align}&lt;br /&gt;
&amp;lt;/math&amp;gt;&lt;br /&gt;
| -0.0016973 &amp;lt; x &amp;lt; 0.0016973 &lt;br /&gt;
| -inf &amp;lt; x &amp;lt; -6.14896e+18&lt;br /&gt;
6.14896e+18 &amp;lt; x &amp;lt; inf&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== External Links ==&lt;br /&gt;
&lt;br /&gt;
* https://yaikhom.com/2020-04-28-localised-approximation-of-hyperbolic-tangents.html#mjx-eqn-eqn7-th%20degree%20lambert%20approximant&lt;br /&gt;
*https://github.com/llvm/llvm-project/blob/main/libc/src/math/generic/tanhf.cpp&lt;br /&gt;
*https://forums.developer.nvidia.com/t/hardware-accelerated-tanh-on-turing/173291&lt;br /&gt;
[[Category:Numerical Precision]]&lt;/div&gt;</summary>
		<author><name>Timo.stripf</name></author>
	</entry>
	<entry>
		<id>https://www.emmtrix.com/w139/index.php?title=tanh_Software_Implementation&amp;diff=2930</id>
		<title>tanh Software Implementation</title>
		<link rel="alternate" type="text/html" href="https://www.emmtrix.com/w139/index.php?title=tanh_Software_Implementation&amp;diff=2930"/>
		<updated>2026-02-04T11:58:42Z</updated>

		<summary type="html">&lt;p&gt;Timo.stripf: /* Numerical Issues and Error Metrics */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;[[File:Hyperbolic Tangent.svg|thumb]]&lt;br /&gt;
The hyperbolic tangent function, commonly referred to as &amp;lt;code&amp;gt;tanh&amp;lt;/code&amp;gt;, is a mathematical function that maps real numbers to the range &amp;lt;math&amp;gt;(-1, 1)&amp;lt;/math&amp;gt;. It is defined as:&lt;br /&gt;
&lt;br /&gt;
:&amp;lt;math&amp;gt;\tanh(x) = \frac{e^x - e^{-x}}{e^x + e^{-x}}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
From a software implementation perspective, &amp;lt;code&amp;gt;tanh&amp;lt;/code&amp;gt; is a numerically challenging function due to the use of exponential functions, which can easily overflow for large inputs and suffer from catastrophic cancellation for very small values of &amp;lt;math&amp;gt;x&amp;lt;/math&amp;gt;. These issues are particularly relevant in single-precision floating-point (&amp;lt;code&amp;gt;float32&amp;lt;/code&amp;gt;) implementations, where both accuracy and performance are critical.&lt;br /&gt;
&lt;br /&gt;
In machine learning and deep learning, &amp;lt;code&amp;gt;tanh&amp;lt;/code&amp;gt; is widely used as an activation function because it introduces non-linearity and produces outputs centered around zero, which can improve gradient-based optimization. As a result, &amp;lt;code&amp;gt;tanh&amp;lt;/code&amp;gt; is often evaluated billions of times during training and inference, making efficient and numerically stable implementations essential.&lt;br /&gt;
&lt;br /&gt;
This article focuses on practical software implementations of &amp;lt;code&amp;gt;tanh&amp;lt;/code&amp;gt;, analyzing different computational formulations with respect to floating-point accuracy, ULP error, overflow behavior, and NaN generation. The goal is to provide a foundation for designing robust &amp;lt;code&amp;gt;tanh&amp;lt;/code&amp;gt; implementations suitable for performance-critical environments such as numerical libraries, embedded systems, and AI workloads.&lt;br /&gt;
&lt;br /&gt;
== Problem Overview ==&lt;br /&gt;
The hyperbolic tangent function poses several challenges when implemented in finite-precision floating-point arithmetic. While the mathematical definition is simple, direct translations into software often suffer from numerical instability and performance issues.&lt;br /&gt;
&lt;br /&gt;
A primary challenge is the use of exponential functions. For large input values, &amp;lt;code&amp;gt;exp(x)&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;exp(-x)&amp;lt;/code&amp;gt; can overflow in single-precision floating-point arithmetic, leading to infinities and, in some formulations, NaN results. Avoiding overflow therefore requires careful reformulation or explicit range handling.&lt;br /&gt;
&lt;br /&gt;
For very small input values, catastrophic cancellation becomes the dominant issue. Expressions such as &amp;lt;math&amp;gt;e^x - e^{-x}&amp;lt;/math&amp;gt; involve the subtraction of nearly equal numbers, causing a severe loss of significant bits and large ULP errors. In these regions, naive implementations may return zero or highly inaccurate results even though &amp;lt;math&amp;gt;\tanh(x) \approx x&amp;lt;/math&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
In addition to accuracy concerns, performance is a critical factor. In many applications, especially machine learning and numerical simulations, &amp;lt;code&amp;gt;tanh&amp;lt;/code&amp;gt; is evaluated extremely frequently. This makes it necessary to balance numerical robustness with the cost of transcendental function calls, branch complexity, and instruction-level efficiency.&lt;br /&gt;
&lt;br /&gt;
==Direct Exponential Formulations==&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+&lt;br /&gt;
!Id&lt;br /&gt;
!Formula&lt;br /&gt;
!Highest ULP Error&lt;br /&gt;
!ULP &amp;lt;= 2.0&lt;br /&gt;
!NaN&lt;br /&gt;
|-&lt;br /&gt;
|exp_v1&lt;br /&gt;
|&amp;lt;math&amp;gt;\frac{e^x - e^{-x}}{e^x + e^{-x}}&amp;lt;/math&amp;gt;&lt;br /&gt;
|x &amp;lt; 0: 8.55638e+08 at -2.98023e-08 with 0x0p+0 instead of -0x1p-25&lt;br /&gt;
&lt;br /&gt;
x &amp;gt; 0: 8.55638e+08 at 2.98023e-08 with 0x0p+0 instead of 0x1p-25&lt;br /&gt;
| -88.7246 &amp;lt; x &amp;lt; -7.14197&lt;br /&gt;
7.14197 &amp;lt; x &amp;lt; 88.7246&lt;br /&gt;
| -inf &amp;lt; x &amp;lt; -88.7246&lt;br /&gt;
88.7246 &amp;lt; x &amp;lt; inf&lt;br /&gt;
|-&lt;br /&gt;
|exp_v2&lt;br /&gt;
|&amp;lt;math&amp;gt;\frac{e^{2x} - 1}{e^{2x} + 1}&amp;lt;/math&amp;gt;&lt;br /&gt;
|x &amp;lt; 0: 8.47249e+08 at -1.49012e-08 with 0x0p+0 instead of -0x1p-26&lt;br /&gt;
x &amp;gt; 0: 8.55638e+08 at 2.98023e-08 with 0x0p+0 instead of 0x1.fffffep-26&lt;br /&gt;
| -inf &amp;lt; x &amp;lt; -0.547821&lt;br /&gt;
0.254822 &amp;lt; x &amp;lt; 8.31787&lt;br /&gt;
&lt;br /&gt;
8.38379 &amp;lt; x &amp;lt; 44.3623&lt;br /&gt;
|44.3623 &amp;lt; x &amp;lt; inf&lt;br /&gt;
|-&lt;br /&gt;
|exp_v3&lt;br /&gt;
|&amp;lt;math&amp;gt;1 - \frac{2}{e^{2x} + 1}&amp;lt;/math&amp;gt;&lt;br /&gt;
|x &amp;lt; 0: 8.59832e+08 at -4.47035e-08 with 0x0p+0 instead of -0x1.8p-25&lt;br /&gt;
x &amp;gt; 0: 8.68221e+08 at 8.9407e-08 with 0x0p+0 instead of 0x1.7ffffcp-24&lt;br /&gt;
| -inf &amp;lt; x &amp;lt; -6.26233&lt;br /&gt;
0.346382 &amp;lt; x &amp;lt; inf&lt;br /&gt;
| -&lt;br /&gt;
|-&lt;br /&gt;
|expm1&lt;br /&gt;
|&amp;lt;math&amp;gt;\frac{expm1(2x)}{expm1(2x) + 2}&amp;lt;/math&amp;gt; &lt;br /&gt;
|x &amp;lt; 0: 2.49585 at -3.95627 with -0x1.ffa00ap-1 instead of -0x1.ffa00ep-1&lt;br /&gt;
x &amp;gt; 0: 2.41657 at 0.0155837 with 0x1.fe9b66p-7 instead of 0x1.fe9b62p-7&lt;br /&gt;
| -inf &amp;lt; x &amp;lt; -5.57227&lt;br /&gt;
-0.42981 &amp;lt; x &amp;lt; 0.00777948&lt;br /&gt;
&lt;br /&gt;
0.125351 &amp;lt; x &amp;lt; 44.3623&lt;br /&gt;
|44.3623 &amp;lt; x &amp;lt; inf&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
The first equation is the definition of the hyperbolic tangent function. This equation requires two exponentials with different values. Since exponentials are expensive to compute, we can use the second equation to reduce the number of exponentials to one.&lt;br /&gt;
&lt;br /&gt;
The second equation has the drawback that both the numerator and the denominator can become infinity for large values of x which results in NaN. That happens for &amp;lt;math&amp;gt;|x| &amp;gt; \log(FLT\_MAX)/2 \approx 44&amp;lt;/math&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
The third equation solves this problem but has a slightly higher error for &amp;lt;math&amp;gt;x \approx 2^{-25}&amp;lt;/math&amp;gt;. In the following graph you can see the error chart for equation 3. The error charts for the other two equations are very similar. All three equations have an error of up to 16 mio ULPs for &amp;lt;math&amp;gt;x &amp;lt; 2^{-5}&amp;lt;/math&amp;gt; which means that up to 24 bits could be wrong.&lt;br /&gt;
&lt;br /&gt;
=== exp_v2 ===&lt;br /&gt;
&lt;br /&gt;
{{#widget:FloatErrorGraph|id=tanhf_exp_v2&lt;br /&gt;
  |data=[ [&amp;quot;-2^127&amp;quot;,0,1.17549e-38],[&amp;quot;-2^126&amp;quot;,0,1.17549e-38],[&amp;quot;-2^125&amp;quot;,0,1.17549e-38],[&amp;quot;-2^124&amp;quot;,0,1.17549e-38],[&amp;quot;-2^123&amp;quot;,0,1.17549e-38],[&amp;quot;-2^122&amp;quot;,0,1.17549e-38],[&amp;quot;-2^121&amp;quot;,0,1.17549e-38],[&amp;quot;-2^120&amp;quot;,0,1.17549e-38],[&amp;quot;-2^119&amp;quot;,0,1.17549e-38],[&amp;quot;-2^118&amp;quot;,0,1.17549e-38],[&amp;quot;-2^117&amp;quot;,0,1.17549e-38],[&amp;quot;-2^116&amp;quot;,0,1.17549e-38],[&amp;quot;-2^115&amp;quot;,0,1.17549e-38],[&amp;quot;-2^114&amp;quot;,0,1.17549e-38],[&amp;quot;-2^113&amp;quot;,0,1.17549e-38],[&amp;quot;-2^112&amp;quot;,0,1.17549e-38],[&amp;quot;-2^111&amp;quot;,0,1.17549e-38],[&amp;quot;-2^110&amp;quot;,0,1.17549e-38],[&amp;quot;-2^109&amp;quot;,0,1.17549e-38],[&amp;quot;-2^108&amp;quot;,0,1.17549e-38],[&amp;quot;-2^107&amp;quot;,0,1.17549e-38],[&amp;quot;-2^106&amp;quot;,0,1.17549e-38],[&amp;quot;-2^105&amp;quot;,0,1.17549e-38],[&amp;quot;-2^104&amp;quot;,0,1.17549e-38],[&amp;quot;-2^103&amp;quot;,0,1.17549e-38],[&amp;quot;-2^102&amp;quot;,0,1.17549e-38],[&amp;quot;-2^101&amp;quot;,0,1.17549e-38],[&amp;quot;-2^100&amp;quot;,0,1.17549e-38],[&amp;quot;-2^99&amp;quot;,0,1.17549e-38],[&amp;quot;-2^98&amp;quot;,0,1.17549e-38],[&amp;quot;-2^97&amp;quot;,0,1.17549e-38],[&amp;quot;-2^96&amp;quot;,0,1.17549e-38],[&amp;quot;-2^95&amp;quot;,0,1.17549e-38],[&amp;quot;-2^94&amp;quot;,0,1.17549e-38],[&amp;quot;-2^93&amp;quot;,0,1.17549e-38],[&amp;quot;-2^92&amp;quot;,0,1.17549e-38],[&amp;quot;-2^91&amp;quot;,0,1.17549e-38],[&amp;quot;-2^90&amp;quot;,0,1.17549e-38],[&amp;quot;-2^89&amp;quot;,0,1.17549e-38],[&amp;quot;-2^88&amp;quot;,0,1.17549e-38],[&amp;quot;-2^87&amp;quot;,0,1.17549e-38],[&amp;quot;-2^86&amp;quot;,0,1.17549e-38],[&amp;quot;-2^85&amp;quot;,0,1.17549e-38],[&amp;quot;-2^84&amp;quot;,0,1.17549e-38],[&amp;quot;-2^83&amp;quot;,0,1.17549e-38],[&amp;quot;-2^82&amp;quot;,0,1.17549e-38],[&amp;quot;-2^81&amp;quot;,0,1.17549e-38],[&amp;quot;-2^80&amp;quot;,0,1.17549e-38],[&amp;quot;-2^79&amp;quot;,0,1.17549e-38],[&amp;quot;-2^78&amp;quot;,0,1.17549e-38],[&amp;quot;-2^77&amp;quot;,0,1.17549e-38],[&amp;quot;-2^76&amp;quot;,0,1.17549e-38],[&amp;quot;-2^75&amp;quot;,0,1.17549e-38],[&amp;quot;-2^74&amp;quot;,0,1.17549e-38],[&amp;quot;-2^73&amp;quot;,0,1.17549e-38],[&amp;quot;-2^72&amp;quot;,0,1.17549e-38],[&amp;quot;-2^71&amp;quot;,0,1.17549e-38],[&amp;quot;-2^70&amp;quot;,0,1.17549e-38],[&amp;quot;-2^69&amp;quot;,0,1.17549e-38],[&amp;quot;-2^68&amp;quot;,0,1.17549e-38],[&amp;quot;-2^67&amp;quot;,0,1.17549e-38],[&amp;quot;-2^66&amp;quot;,0,1.17549e-38],[&amp;quot;-2^65&amp;quot;,0,1.17549e-38],[&amp;quot;-2^64&amp;quot;,0,1.17549e-38],[&amp;quot;-2^63&amp;quot;,0,1.17549e-38],[&amp;quot;-2^62&amp;quot;,0,1.17549e-38],[&amp;quot;-2^61&amp;quot;,0,1.17549e-38],[&amp;quot;-2^60&amp;quot;,0,1.17549e-38],[&amp;quot;-2^59&amp;quot;,0,1.17549e-38],[&amp;quot;-2^58&amp;quot;,0,1.17549e-38],[&amp;quot;-2^57&amp;quot;,0,1.17549e-38],[&amp;quot;-2^56&amp;quot;,0,1.17549e-38],[&amp;quot;-2^55&amp;quot;,0,1.17549e-38],[&amp;quot;-2^54&amp;quot;,0,1.17549e-38],[&amp;quot;-2^53&amp;quot;,0,1.17549e-38],[&amp;quot;-2^52&amp;quot;,0,1.17549e-38],[&amp;quot;-2^51&amp;quot;,0,1.17549e-38],[&amp;quot;-2^50&amp;quot;,0,1.17549e-38],[&amp;quot;-2^49&amp;quot;,0,1.17549e-38],[&amp;quot;-2^48&amp;quot;,0,1.17549e-38],[&amp;quot;-2^47&amp;quot;,0,1.17549e-38],[&amp;quot;-2^46&amp;quot;,0,1.17549e-38],[&amp;quot;-2^45&amp;quot;,0,1.17549e-38],[&amp;quot;-2^44&amp;quot;,0,1.17549e-38],[&amp;quot;-2^43&amp;quot;,0,1.17549e-38],[&amp;quot;-2^42&amp;quot;,0,1.17549e-38],[&amp;quot;-2^41&amp;quot;,0,1.17549e-38],[&amp;quot;-2^40&amp;quot;,0,1.17549e-38],[&amp;quot;-2^39&amp;quot;,0,1.17549e-38],[&amp;quot;-2^38&amp;quot;,0,1.17549e-38],[&amp;quot;-2^37&amp;quot;,0,1.17549e-38],[&amp;quot;-2^36&amp;quot;,0,1.17549e-38],[&amp;quot;-2^35&amp;quot;,0,1.17549e-38],[&amp;quot;-2^34&amp;quot;,0,1.17549e-38],[&amp;quot;-2^33&amp;quot;,0,1.17549e-38],[&amp;quot;-2^32&amp;quot;,0,1.17549e-38],[&amp;quot;-2^31&amp;quot;,0,1.17549e-38],[&amp;quot;-2^30&amp;quot;,0,1.17549e-38],[&amp;quot;-2^29&amp;quot;,0,1.17549e-38],[&amp;quot;-2^28&amp;quot;,0,1.17549e-38],[&amp;quot;-2^27&amp;quot;,0,1.17549e-38],[&amp;quot;-2^26&amp;quot;,0,1.17549e-38],[&amp;quot;-2^25&amp;quot;,0,1.17549e-38],[&amp;quot;-2^24&amp;quot;,0,1.17549e-38],[&amp;quot;-2^23&amp;quot;,0,1.17549e-38],[&amp;quot;-2^22&amp;quot;,0,1.17549e-38],[&amp;quot;-2^21&amp;quot;,0,1.17549e-38],[&amp;quot;-2^20&amp;quot;,0,1.17549e-38],[&amp;quot;-2^19&amp;quot;,0,1.17549e-38],[&amp;quot;-2^18&amp;quot;,0,1.17549e-38],[&amp;quot;-2^17&amp;quot;,0,1.17549e-38],[&amp;quot;-2^16&amp;quot;,0,1.17549e-38],[&amp;quot;-2^15&amp;quot;,0,1.17549e-38],[&amp;quot;-2^14&amp;quot;,0,1.17549e-38],[&amp;quot;-2^13&amp;quot;,0,1.17549e-38],[&amp;quot;-2^12&amp;quot;,0,1.17549e-38],[&amp;quot;-2^11&amp;quot;,0,1.17549e-38],[&amp;quot;-2^10&amp;quot;,0,1.17549e-38],[&amp;quot;-2^9&amp;quot;,0,1.17549e-38],[&amp;quot;-2^8&amp;quot;,0,1.17549e-38],[&amp;quot;-2^7&amp;quot;,0,1.17549e-38],[&amp;quot;-2^6&amp;quot;,0,1.17549e-38],[&amp;quot;-2^5&amp;quot;,0,1.17549e-38],[&amp;quot;-2^4&amp;quot;,2.53131e-14,4.24683e-07],[&amp;quot;-2^3&amp;quot;,5.95888e-08,0.999734],[&amp;quot;-2^2&amp;quot;,8.86696e-08,1.48763],[&amp;quot;-2^1&amp;quot;,8.91609e-08,1.49587],[&amp;quot;-2^0&amp;quot;,8.73743e-08,1.4659],[&amp;quot;-2^-1&amp;quot;,8.65382e-08,2.13222],[&amp;quot;-2^-2&amp;quot;,6.18481e-08,2.61428],[&amp;quot;-2^-3&amp;quot;,3.88071e-08,3.08594],[&amp;quot;-2^-4&amp;quot;,2.6234e-08,4.23594],[&amp;quot;-2^-5&amp;quot;,2.02799e-08,8.19726],[&amp;quot;-2^-6&amp;quot;,1.75183e-08,12.4495],[&amp;quot;-2^-7&amp;quot;,1.61937e-08,17.3878],[&amp;quot;-2^-8&amp;quot;,1.5566e-08,33.4278],[&amp;quot;-2^-9&amp;quot;,1.51688e-08,65.1495],[&amp;quot;-2^-10&amp;quot;,1.50281e-08,129.09],[&amp;quot;-2^-11&amp;quot;,1.49613e-08,257.033],[&amp;quot;-2^-12&amp;quot;,1.49296e-08,512.977],[&amp;quot;-2^-13&amp;quot;,1.49174e-08,1025.12],[&amp;quot;-2^-14&amp;quot;,1.4909e-08,2049.08],[&amp;quot;-2^-15&amp;quot;,1.49049e-08,4097.02],[&amp;quot;-2^-16&amp;quot;,1.4903e-08,8193],[&amp;quot;-2^-17&amp;quot;,1.49021e-08,16385],[&amp;quot;-2^-18&amp;quot;,1.48862e-08,32735],[&amp;quot;-2^-19&amp;quot;,1.48973e-08,65519],[&amp;quot;-2^-20&amp;quot;,1.49001e-08,131063],[&amp;quot;-2^-21&amp;quot;,1.49009e-08,262139],[&amp;quot;-2^-22&amp;quot;,1.49011e-08,524285],[&amp;quot;-2^-23&amp;quot;,1.49011e-08,1.04857e+06],[&amp;quot;-2^-24&amp;quot;,1.49011e-08,2.09715e+06],[&amp;quot;-2^-25&amp;quot;,1.49012e-08,4.1943e+06],[&amp;quot;-2^-26&amp;quot;,1.49007e-08,8.38835e+06],[&amp;quot;-2^-27&amp;quot;,1.49012e-08,8.47249e+08],[&amp;quot;-2^-28&amp;quot;,7.45058e-09,8.38861e+08],[&amp;quot;-2^-29&amp;quot;,3.72529e-09,8.30472e+08],[&amp;quot;-2^-30&amp;quot;,1.86265e-09,8.22084e+08],[&amp;quot;-2^-31&amp;quot;,9.31323e-10,8.13695e+08],[&amp;quot;-2^-32&amp;quot;,4.65661e-10,8.05306e+08],[&amp;quot;-2^-33&amp;quot;,2.32831e-10,7.96918e+08],[&amp;quot;-2^-34&amp;quot;,1.16415e-10,7.88529e+08],[&amp;quot;-2^-35&amp;quot;,5.82077e-11,7.80141e+08],[&amp;quot;-2^-36&amp;quot;,2.91038e-11,7.71752e+08],[&amp;quot;-2^-37&amp;quot;,1.45519e-11,7.63363e+08],[&amp;quot;-2^-38&amp;quot;,7.27596e-12,7.54975e+08],[&amp;quot;-2^-39&amp;quot;,3.63798e-12,7.46586e+08],[&amp;quot;-2^-40&amp;quot;,1.81899e-12,7.38198e+08],[&amp;quot;-2^-41&amp;quot;,9.09495e-13,7.29809e+08],[&amp;quot;-2^-42&amp;quot;,4.54747e-13,7.2142e+08],[&amp;quot;-2^-43&amp;quot;,2.27374e-13,7.13032e+08],[&amp;quot;-2^-44&amp;quot;,1.13687e-13,7.04643e+08],[&amp;quot;-2^-45&amp;quot;,5.68434e-14,6.96254e+08],[&amp;quot;-2^-46&amp;quot;,2.84217e-14,6.87866e+08],[&amp;quot;-2^-47&amp;quot;,1.42109e-14,6.79477e+08],[&amp;quot;-2^-48&amp;quot;,7.10543e-15,6.71089e+08],[&amp;quot;-2^-49&amp;quot;,3.55271e-15,6.627e+08],[&amp;quot;-2^-50&amp;quot;,1.77636e-15,6.54311e+08],[&amp;quot;-2^-51&amp;quot;,8.88178e-16,6.45923e+08],[&amp;quot;-2^-52&amp;quot;,4.44089e-16,6.37534e+08],[&amp;quot;-2^-53&amp;quot;,2.22045e-16,6.29146e+08],[&amp;quot;-2^-54&amp;quot;,1.11022e-16,6.20757e+08],[&amp;quot;-2^-55&amp;quot;,5.55111e-17,6.12368e+08],[&amp;quot;-2^-56&amp;quot;,2.77556e-17,6.0398e+08],[&amp;quot;-2^-57&amp;quot;,1.38778e-17,5.95591e+08],[&amp;quot;-2^-58&amp;quot;,6.93889e-18,5.87203e+08],[&amp;quot;-2^-59&amp;quot;,3.46945e-18,5.78814e+08],[&amp;quot;-2^-60&amp;quot;,1.73472e-18,5.70425e+08],[&amp;quot;-2^-61&amp;quot;,8.67362e-19,5.62037e+08],[&amp;quot;-2^-62&amp;quot;,4.33681e-19,5.53648e+08],[&amp;quot;-2^-63&amp;quot;,2.1684e-19,5.4526e+08],[&amp;quot;-2^-64&amp;quot;,1.0842e-19,5.36871e+08],[&amp;quot;-2^-65&amp;quot;,5.42101e-20,5.28482e+08],[&amp;quot;-2^-66&amp;quot;,2.71051e-20,5.20094e+08],[&amp;quot;-2^-67&amp;quot;,1.35525e-20,5.11705e+08],[&amp;quot;-2^-68&amp;quot;,6.77626e-21,5.03316e+08],[&amp;quot;-2^-69&amp;quot;,3.38813e-21,4.94928e+08],[&amp;quot;-2^-70&amp;quot;,1.69407e-21,4.86539e+08],[&amp;quot;-2^-71&amp;quot;,8.47033e-22,4.78151e+08],[&amp;quot;-2^-72&amp;quot;,4.23516e-22,4.69762e+08],[&amp;quot;-2^-73&amp;quot;,2.11758e-22,4.61373e+08],[&amp;quot;-2^-74&amp;quot;,1.05879e-22,4.52985e+08],[&amp;quot;-2^-75&amp;quot;,5.29396e-23,4.44596e+08],[&amp;quot;-2^-76&amp;quot;,2.64698e-23,4.36208e+08],[&amp;quot;-2^-77&amp;quot;,1.32349e-23,4.27819e+08],[&amp;quot;-2^-78&amp;quot;,6.61744e-24,4.1943e+08],[&amp;quot;-2^-79&amp;quot;,3.30872e-24,4.11042e+08],[&amp;quot;-2^-80&amp;quot;,1.65436e-24,4.02653e+08],[&amp;quot;-2^-81&amp;quot;,8.27181e-25,3.94265e+08],[&amp;quot;-2^-82&amp;quot;,4.1359e-25,3.85876e+08],[&amp;quot;-2^-83&amp;quot;,2.06795e-25,3.77487e+08],[&amp;quot;-2^-84&amp;quot;,1.03398e-25,3.69099e+08],[&amp;quot;-2^-85&amp;quot;,5.16988e-26,3.6071e+08],[&amp;quot;-2^-86&amp;quot;,2.58494e-26,3.52322e+08],[&amp;quot;-2^-87&amp;quot;,1.29247e-26,3.43933e+08],[&amp;quot;-2^-88&amp;quot;,6.46235e-27,3.35544e+08],[&amp;quot;-2^-89&amp;quot;,3.23117e-27,3.27156e+08],[&amp;quot;-2^-90&amp;quot;,1.61559e-27,3.18767e+08],[&amp;quot;-2^-91&amp;quot;,8.07794e-28,3.10378e+08],[&amp;quot;-2^-92&amp;quot;,4.03897e-28,3.0199e+08],[&amp;quot;-2^-93&amp;quot;,2.01948e-28,2.93601e+08],[&amp;quot;-2^-94&amp;quot;,1.00974e-28,2.85213e+08],[&amp;quot;-2^-95&amp;quot;,5.04871e-29,2.76824e+08],[&amp;quot;-2^-96&amp;quot;,2.52435e-29,2.68435e+08],[&amp;quot;-2^-97&amp;quot;,1.26218e-29,2.60047e+08],[&amp;quot;-2^-98&amp;quot;,6.31089e-30,2.51658e+08],[&amp;quot;-2^-99&amp;quot;,3.15544e-30,2.4327e+08],[&amp;quot;-2^-100&amp;quot;,1.57772e-30,2.34881e+08],[&amp;quot;-2^-101&amp;quot;,7.88861e-31,2.26492e+08],[&amp;quot;-2^-102&amp;quot;,3.9443e-31,2.18104e+08],[&amp;quot;-2^-103&amp;quot;,1.97215e-31,2.09715e+08],[&amp;quot;-2^-104&amp;quot;,9.86076e-32,2.01327e+08],[&amp;quot;-2^-105&amp;quot;,4.93038e-32,1.92938e+08],[&amp;quot;-2^-106&amp;quot;,2.46519e-32,1.84549e+08],[&amp;quot;-2^-107&amp;quot;,1.2326e-32,1.76161e+08],[&amp;quot;-2^-108&amp;quot;,6.16298e-33,1.67772e+08],[&amp;quot;-2^-109&amp;quot;,3.08149e-33,1.59384e+08],[&amp;quot;-2^-110&amp;quot;,1.54074e-33,1.50995e+08],[&amp;quot;-2^-111&amp;quot;,7.70372e-34,1.42606e+08],[&amp;quot;-2^-112&amp;quot;,3.85186e-34,1.34218e+08],[&amp;quot;-2^-113&amp;quot;,1.92593e-34,1.25829e+08],[&amp;quot;-2^-114&amp;quot;,9.62965e-35,1.17441e+08],[&amp;quot;-2^-115&amp;quot;,4.81482e-35,1.09052e+08],[&amp;quot;-2^-116&amp;quot;,2.40741e-35,1.00663e+08],[&amp;quot;-2^-117&amp;quot;,1.20371e-35,9.22747e+07],[&amp;quot;-2^-118&amp;quot;,6.01853e-36,8.38861e+07],[&amp;quot;-2^-119&amp;quot;,3.00927e-36,7.54975e+07],[&amp;quot;-2^-120&amp;quot;,1.50463e-36,6.71089e+07],[&amp;quot;-2^-121&amp;quot;,7.52316e-37,5.87203e+07],[&amp;quot;-2^-122&amp;quot;,3.76158e-37,5.03316e+07],[&amp;quot;-2^-123&amp;quot;,1.88079e-37,4.1943e+07],[&amp;quot;-2^-124&amp;quot;,9.40395e-38,3.35544e+07],[&amp;quot;-2^-125&amp;quot;,4.70198e-38,2.51658e+07],[&amp;quot;-2^-126&amp;quot;,2.35099e-38,1.67772e+07],[&amp;quot;-SN&amp;quot;,1.17546e-38,8.38835e+06],[&amp;quot;0&amp;quot;,0,1.17549e-38],[&amp;quot;+SN&amp;quot;,1.17546e-38,8.38835e+06],[&amp;quot;+2^-126&amp;quot;,2.35095e-38,1.6777e+07],[&amp;quot;+2^-125&amp;quot;,4.70191e-38,2.51656e+07],[&amp;quot;+2^-124&amp;quot;,9.40381e-38,3.35542e+07],[&amp;quot;+2^-123&amp;quot;,1.88076e-37,4.19428e+07],[&amp;quot;+2^-122&amp;quot;,3.76152e-37,5.03314e+07],[&amp;quot;+2^-121&amp;quot;,7.52305e-37,5.872e+07],[&amp;quot;+2^-120&amp;quot;,1.50461e-36,6.71086e+07],[&amp;quot;+2^-119&amp;quot;,3.00922e-36,7.54972e+07],[&amp;quot;+2^-118&amp;quot;,6.01844e-36,8.38858e+07],[&amp;quot;+2^-117&amp;quot;,1.20369e-35,9.22744e+07],[&amp;quot;+2^-116&amp;quot;,2.40738e-35,1.00663e+08],[&amp;quot;+2^-115&amp;quot;,4.81475e-35,1.09052e+08],[&amp;quot;+2^-114&amp;quot;,9.6295e-35,1.1744e+08],[&amp;quot;+2^-113&amp;quot;,1.9259e-34,1.25829e+08],[&amp;quot;+2^-112&amp;quot;,3.8518e-34,1.34217e+08],[&amp;quot;+2^-111&amp;quot;,7.7036e-34,1.42606e+08],[&amp;quot;+2^-110&amp;quot;,1.54072e-33,1.50995e+08],[&amp;quot;+2^-109&amp;quot;,3.08144e-33,1.59383e+08],[&amp;quot;+2^-108&amp;quot;,6.16288e-33,1.67772e+08],[&amp;quot;+2^-107&amp;quot;,1.23258e-32,1.76161e+08],[&amp;quot;+2^-106&amp;quot;,2.46515e-32,1.84549e+08],[&amp;quot;+2^-105&amp;quot;,4.93031e-32,1.92938e+08],[&amp;quot;+2^-104&amp;quot;,9.86061e-32,2.01326e+08],[&amp;quot;+2^-103&amp;quot;,1.97212e-31,2.09715e+08],[&amp;quot;+2^-102&amp;quot;,3.94424e-31,2.18104e+08],[&amp;quot;+2^-101&amp;quot;,7.88849e-31,2.26492e+08],[&amp;quot;+2^-100&amp;quot;,1.5777e-30,2.34881e+08],[&amp;quot;+2^-99&amp;quot;,3.1554e-30,2.43269e+08],[&amp;quot;+2^-98&amp;quot;,6.31079e-30,2.51658e+08],[&amp;quot;+2^-97&amp;quot;,1.26216e-29,2.60047e+08],[&amp;quot;+2^-96&amp;quot;,2.52432e-29,2.68435e+08],[&amp;quot;+2^-95&amp;quot;,5.04863e-29,2.76824e+08],[&amp;quot;+2^-94&amp;quot;,1.00973e-28,2.85212e+08],[&amp;quot;+2^-93&amp;quot;,2.01945e-28,2.93601e+08],[&amp;quot;+2^-92&amp;quot;,4.03891e-28,3.0199e+08],[&amp;quot;+2^-91&amp;quot;,8.07781e-28,3.10378e+08],[&amp;quot;+2^-90&amp;quot;,1.61556e-27,3.18767e+08],[&amp;quot;+2^-89&amp;quot;,3.23112e-27,3.27155e+08],[&amp;quot;+2^-88&amp;quot;,6.46225e-27,3.35544e+08],[&amp;quot;+2^-87&amp;quot;,1.29245e-26,3.43933e+08],[&amp;quot;+2^-86&amp;quot;,2.5849e-26,3.52321e+08],[&amp;quot;+2^-85&amp;quot;,5.1698e-26,3.6071e+08],[&amp;quot;+2^-84&amp;quot;,1.03396e-25,3.69098e+08],[&amp;quot;+2^-83&amp;quot;,2.06792e-25,3.77487e+08],[&amp;quot;+2^-82&amp;quot;,4.13584e-25,3.85876e+08],[&amp;quot;+2^-81&amp;quot;,8.27168e-25,3.94264e+08],[&amp;quot;+2^-80&amp;quot;,1.65434e-24,4.02653e+08],[&amp;quot;+2^-79&amp;quot;,3.30867e-24,4.11042e+08],[&amp;quot;+2^-78&amp;quot;,6.61734e-24,4.1943e+08],[&amp;quot;+2^-77&amp;quot;,1.32347e-23,4.27819e+08],[&amp;quot;+2^-76&amp;quot;,2.64694e-23,4.36207e+08],[&amp;quot;+2^-75&amp;quot;,5.29388e-23,4.44596e+08],[&amp;quot;+2^-74&amp;quot;,1.05878e-22,4.52985e+08],[&amp;quot;+2^-73&amp;quot;,2.11755e-22,4.61373e+08],[&amp;quot;+2^-72&amp;quot;,4.2351e-22,4.69762e+08],[&amp;quot;+2^-71&amp;quot;,8.4702e-22,4.7815e+08],[&amp;quot;+2^-70&amp;quot;,1.69404e-21,4.86539e+08],[&amp;quot;+2^-69&amp;quot;,3.38808e-21,4.94928e+08],[&amp;quot;+2^-68&amp;quot;,6.77616e-21,5.03316e+08],[&amp;quot;+2^-67&amp;quot;,1.35523e-20,5.11705e+08],[&amp;quot;+2^-66&amp;quot;,2.71046e-20,5.20093e+08],[&amp;quot;+2^-65&amp;quot;,5.42093e-20,5.28482e+08],[&amp;quot;+2^-64&amp;quot;,1.08419e-19,5.36871e+08],[&amp;quot;+2^-63&amp;quot;,2.16837e-19,5.45259e+08],[&amp;quot;+2^-62&amp;quot;,4.33674e-19,5.53648e+08],[&amp;quot;+2^-61&amp;quot;,8.67349e-19,5.62036e+08],[&amp;quot;+2^-60&amp;quot;,1.7347e-18,5.70425e+08],[&amp;quot;+2^-59&amp;quot;,3.46939e-18,5.78814e+08],[&amp;quot;+2^-58&amp;quot;,6.93879e-18,5.87202e+08],[&amp;quot;+2^-57&amp;quot;,1.38776e-17,5.95591e+08],[&amp;quot;+2^-56&amp;quot;,2.77552e-17,6.0398e+08],[&amp;quot;+2^-55&amp;quot;,5.55103e-17,6.12368e+08],[&amp;quot;+2^-54&amp;quot;,1.11021e-16,6.20757e+08],[&amp;quot;+2^-53&amp;quot;,2.22041e-16,6.29145e+08],[&amp;quot;+2^-52&amp;quot;,4.44082e-16,6.37534e+08],[&amp;quot;+2^-51&amp;quot;,8.88165e-16,6.45923e+08],[&amp;quot;+2^-50&amp;quot;,1.77633e-15,6.54311e+08],[&amp;quot;+2^-49&amp;quot;,3.55266e-15,6.627e+08],[&amp;quot;+2^-48&amp;quot;,7.10532e-15,6.71088e+08],[&amp;quot;+2^-47&amp;quot;,1.42106e-14,6.79477e+08],[&amp;quot;+2^-46&amp;quot;,2.84213e-14,6.87866e+08],[&amp;quot;+2^-45&amp;quot;,5.68426e-14,6.96254e+08],[&amp;quot;+2^-44&amp;quot;,1.13685e-13,7.04643e+08],[&amp;quot;+2^-43&amp;quot;,2.2737e-13,7.13031e+08],[&amp;quot;+2^-42&amp;quot;,4.5474e-13,7.2142e+08],[&amp;quot;+2^-41&amp;quot;,9.09481e-13,7.29809e+08],[&amp;quot;+2^-40&amp;quot;,1.81896e-12,7.38197e+08],[&amp;quot;+2^-39&amp;quot;,3.63792e-12,7.46586e+08],[&amp;quot;+2^-38&amp;quot;,7.27585e-12,7.54974e+08],[&amp;quot;+2^-37&amp;quot;,1.45517e-11,7.63363e+08],[&amp;quot;+2^-36&amp;quot;,2.91034e-11,7.71752e+08],[&amp;quot;+2^-35&amp;quot;,5.82068e-11,7.8014e+08],[&amp;quot;+2^-34&amp;quot;,1.16414e-10,7.88529e+08],[&amp;quot;+2^-33&amp;quot;,2.32827e-10,7.96918e+08],[&amp;quot;+2^-32&amp;quot;,4.65654e-10,8.05306e+08],[&amp;quot;+2^-31&amp;quot;,9.31308e-10,8.13695e+08],[&amp;quot;+2^-30&amp;quot;,1.86262e-09,8.22083e+08],[&amp;quot;+2^-29&amp;quot;,3.72523e-09,8.30472e+08],[&amp;quot;+2^-28&amp;quot;,7.45047e-09,8.38861e+08],[&amp;quot;+2^-27&amp;quot;,1.49009e-08,8.47249e+08],[&amp;quot;+2^-26&amp;quot;,2.98019e-08,8.55638e+08],[&amp;quot;+2^-25&amp;quot;,2.98023e-08,8.38861e+06],[&amp;quot;+2^-24&amp;quot;,2.98023e-08,4.1943e+06],[&amp;quot;+2^-23&amp;quot;,2.98023e-08,2.09715e+06],[&amp;quot;+2^-22&amp;quot;,2.98023e-08,1.04857e+06],[&amp;quot;+2^-21&amp;quot;,2.98021e-08,524284],[&amp;quot;+2^-20&amp;quot;,2.98014e-08,262136],[&amp;quot;+2^-19&amp;quot;,2.97987e-08,131056],[&amp;quot;+2^-18&amp;quot;,2.97878e-08,65504],[&amp;quot;+2^-17&amp;quot;,2.98014e-08,32767],[&amp;quot;+2^-16&amp;quot;,2.98005e-08,16384],[&amp;quot;+2^-15&amp;quot;,2.98023e-08,8192],[&amp;quot;+2^-14&amp;quot;,2.98025e-08,4351.99],[&amp;quot;+2^-13&amp;quot;,2.98044e-08,2815.96],[&amp;quot;+2^-12&amp;quot;,2.98091e-08,1024.23],[&amp;quot;+2^-11&amp;quot;,2.9812e-08,512.167],[&amp;quot;+2^-10&amp;quot;,2.98154e-08,256.113],[&amp;quot;+2^-9&amp;quot;,2.98591e-08,128.244],[&amp;quot;+2^-8&amp;quot;,3.00204e-08,85.3328],[&amp;quot;+2^-7&amp;quot;,3.01947e-08,43.675],[&amp;quot;+2^-6&amp;quot;,3.05713e-08,30.45],[&amp;quot;+2^-5&amp;quot;,3.14319e-08,8.43743],[&amp;quot;+2^-4&amp;quot;,3.31291e-08,7.77058],[&amp;quot;+2^-3&amp;quot;,3.58597e-08,3.99659],[&amp;quot;+2^-2&amp;quot;,4.14639e-08,2.29246],[&amp;quot;+2^-1&amp;quot;,8.02409e-08,1.34622],[&amp;quot;+2^0&amp;quot;,8.66201e-08,1.45324],[&amp;quot;+2^1&amp;quot;,8.8654e-08,1.48737],[&amp;quot;+2^2&amp;quot;,6.7976e-08,1.14045],[&amp;quot;+2^3&amp;quot;,1.33955e-07,2.2474],[&amp;quot;+2^4&amp;quot;,2.53131e-14,4.24683e-07],[&amp;quot;+2^5&amp;quot;,0,null],[&amp;quot;+2^6&amp;quot;,0,null],[&amp;quot;+2^7&amp;quot;,0,null],[&amp;quot;+2^8&amp;quot;,0,null],[&amp;quot;+2^9&amp;quot;,0,null],[&amp;quot;+2^10&amp;quot;,0,null],[&amp;quot;+2^11&amp;quot;,0,null],[&amp;quot;+2^12&amp;quot;,0,null],[&amp;quot;+2^13&amp;quot;,0,null],[&amp;quot;+2^14&amp;quot;,0,null],[&amp;quot;+2^15&amp;quot;,0,null],[&amp;quot;+2^16&amp;quot;,0,null],[&amp;quot;+2^17&amp;quot;,0,null],[&amp;quot;+2^18&amp;quot;,0,null],[&amp;quot;+2^19&amp;quot;,0,null],[&amp;quot;+2^20&amp;quot;,0,null],[&amp;quot;+2^21&amp;quot;,0,null],[&amp;quot;+2^22&amp;quot;,0,null],[&amp;quot;+2^23&amp;quot;,0,null],[&amp;quot;+2^24&amp;quot;,0,null],[&amp;quot;+2^25&amp;quot;,0,null],[&amp;quot;+2^26&amp;quot;,0,null],[&amp;quot;+2^27&amp;quot;,0,null],[&amp;quot;+2^28&amp;quot;,0,null],[&amp;quot;+2^29&amp;quot;,0,null],[&amp;quot;+2^30&amp;quot;,0,null],[&amp;quot;+2^31&amp;quot;,0,null],[&amp;quot;+2^32&amp;quot;,0,null],[&amp;quot;+2^33&amp;quot;,0,null],[&amp;quot;+2^34&amp;quot;,0,null],[&amp;quot;+2^35&amp;quot;,0,null],[&amp;quot;+2^36&amp;quot;,0,null],[&amp;quot;+2^37&amp;quot;,0,null],[&amp;quot;+2^38&amp;quot;,0,null],[&amp;quot;+2^39&amp;quot;,0,null],[&amp;quot;+2^40&amp;quot;,0,null],[&amp;quot;+2^41&amp;quot;,0,null],[&amp;quot;+2^42&amp;quot;,0,null],[&amp;quot;+2^43&amp;quot;,0,null],[&amp;quot;+2^44&amp;quot;,0,null],[&amp;quot;+2^45&amp;quot;,0,null],[&amp;quot;+2^46&amp;quot;,0,null],[&amp;quot;+2^47&amp;quot;,0,null],[&amp;quot;+2^48&amp;quot;,0,null],[&amp;quot;+2^49&amp;quot;,0,null],[&amp;quot;+2^50&amp;quot;,0,null],[&amp;quot;+2^51&amp;quot;,0,null],[&amp;quot;+2^52&amp;quot;,0,null],[&amp;quot;+2^53&amp;quot;,0,null],[&amp;quot;+2^54&amp;quot;,0,null],[&amp;quot;+2^55&amp;quot;,0,null],[&amp;quot;+2^56&amp;quot;,0,null],[&amp;quot;+2^57&amp;quot;,0,null],[&amp;quot;+2^58&amp;quot;,0,null],[&amp;quot;+2^59&amp;quot;,0,null],[&amp;quot;+2^60&amp;quot;,0,null],[&amp;quot;+2^61&amp;quot;,0,null],[&amp;quot;+2^62&amp;quot;,0,null],[&amp;quot;+2^63&amp;quot;,0,null],[&amp;quot;+2^64&amp;quot;,0,null],[&amp;quot;+2^65&amp;quot;,0,null],[&amp;quot;+2^66&amp;quot;,0,null],[&amp;quot;+2^67&amp;quot;,0,null],[&amp;quot;+2^68&amp;quot;,0,null],[&amp;quot;+2^69&amp;quot;,0,null],[&amp;quot;+2^70&amp;quot;,0,null],[&amp;quot;+2^71&amp;quot;,0,null],[&amp;quot;+2^72&amp;quot;,0,null],[&amp;quot;+2^73&amp;quot;,0,null],[&amp;quot;+2^74&amp;quot;,0,null],[&amp;quot;+2^75&amp;quot;,0,null],[&amp;quot;+2^76&amp;quot;,0,null],[&amp;quot;+2^77&amp;quot;,0,null],[&amp;quot;+2^78&amp;quot;,0,null],[&amp;quot;+2^79&amp;quot;,0,null],[&amp;quot;+2^80&amp;quot;,0,null],[&amp;quot;+2^81&amp;quot;,0,null],[&amp;quot;+2^82&amp;quot;,0,null],[&amp;quot;+2^83&amp;quot;,0,null],[&amp;quot;+2^84&amp;quot;,0,null],[&amp;quot;+2^85&amp;quot;,0,null],[&amp;quot;+2^86&amp;quot;,0,null],[&amp;quot;+2^87&amp;quot;,0,null],[&amp;quot;+2^88&amp;quot;,0,null],[&amp;quot;+2^89&amp;quot;,0,null],[&amp;quot;+2^90&amp;quot;,0,null],[&amp;quot;+2^91&amp;quot;,0,null],[&amp;quot;+2^92&amp;quot;,0,null],[&amp;quot;+2^93&amp;quot;,0,null],[&amp;quot;+2^94&amp;quot;,0,null],[&amp;quot;+2^95&amp;quot;,0,null],[&amp;quot;+2^96&amp;quot;,0,null],[&amp;quot;+2^97&amp;quot;,0,null],[&amp;quot;+2^98&amp;quot;,0,null],[&amp;quot;+2^99&amp;quot;,0,null],[&amp;quot;+2^100&amp;quot;,0,null],[&amp;quot;+2^101&amp;quot;,0,null],[&amp;quot;+2^102&amp;quot;,0,null],[&amp;quot;+2^103&amp;quot;,0,null],[&amp;quot;+2^104&amp;quot;,0,null],[&amp;quot;+2^105&amp;quot;,0,null],[&amp;quot;+2^106&amp;quot;,0,null],[&amp;quot;+2^107&amp;quot;,0,null],[&amp;quot;+2^108&amp;quot;,0,null],[&amp;quot;+2^109&amp;quot;,0,null],[&amp;quot;+2^110&amp;quot;,0,null],[&amp;quot;+2^111&amp;quot;,0,null],[&amp;quot;+2^112&amp;quot;,0,null],[&amp;quot;+2^113&amp;quot;,0,null],[&amp;quot;+2^114&amp;quot;,0,null],[&amp;quot;+2^115&amp;quot;,0,null],[&amp;quot;+2^116&amp;quot;,0,null],[&amp;quot;+2^117&amp;quot;,0,null],[&amp;quot;+2^118&amp;quot;,0,null],[&amp;quot;+2^119&amp;quot;,0,null],[&amp;quot;+2^120&amp;quot;,0,null],[&amp;quot;+2^121&amp;quot;,0,null],[&amp;quot;+2^122&amp;quot;,0,null],[&amp;quot;+2^123&amp;quot;,0,null],[&amp;quot;+2^124&amp;quot;,0,null],[&amp;quot;+2^125&amp;quot;,0,null],[&amp;quot;+2^126&amp;quot;,0,null],[&amp;quot;+2^127&amp;quot;,0,null] ]&lt;br /&gt;
 }}&lt;br /&gt;
=== expm1 ===&lt;br /&gt;
&lt;br /&gt;
{{#widget:FloatErrorGraph|id=tanhf_expm1&lt;br /&gt;
  |data=[ [&amp;quot;-2^127&amp;quot;,0,1.17549e-38],[&amp;quot;-2^126&amp;quot;,0,1.17549e-38],[&amp;quot;-2^125&amp;quot;,0,1.17549e-38],[&amp;quot;-2^124&amp;quot;,0,1.17549e-38],[&amp;quot;-2^123&amp;quot;,0,1.17549e-38],[&amp;quot;-2^122&amp;quot;,0,1.17549e-38],[&amp;quot;-2^121&amp;quot;,0,1.17549e-38],[&amp;quot;-2^120&amp;quot;,0,1.17549e-38],[&amp;quot;-2^119&amp;quot;,0,1.17549e-38],[&amp;quot;-2^118&amp;quot;,0,1.17549e-38],[&amp;quot;-2^117&amp;quot;,0,1.17549e-38],[&amp;quot;-2^116&amp;quot;,0,1.17549e-38],[&amp;quot;-2^115&amp;quot;,0,1.17549e-38],[&amp;quot;-2^114&amp;quot;,0,1.17549e-38],[&amp;quot;-2^113&amp;quot;,0,1.17549e-38],[&amp;quot;-2^112&amp;quot;,0,1.17549e-38],[&amp;quot;-2^111&amp;quot;,0,1.17549e-38],[&amp;quot;-2^110&amp;quot;,0,1.17549e-38],[&amp;quot;-2^109&amp;quot;,0,1.17549e-38],[&amp;quot;-2^108&amp;quot;,0,1.17549e-38],[&amp;quot;-2^107&amp;quot;,0,1.17549e-38],[&amp;quot;-2^106&amp;quot;,0,1.17549e-38],[&amp;quot;-2^105&amp;quot;,0,1.17549e-38],[&amp;quot;-2^104&amp;quot;,0,1.17549e-38],[&amp;quot;-2^103&amp;quot;,0,1.17549e-38],[&amp;quot;-2^102&amp;quot;,0,1.17549e-38],[&amp;quot;-2^101&amp;quot;,0,1.17549e-38],[&amp;quot;-2^100&amp;quot;,0,1.17549e-38],[&amp;quot;-2^99&amp;quot;,0,1.17549e-38],[&amp;quot;-2^98&amp;quot;,0,1.17549e-38],[&amp;quot;-2^97&amp;quot;,0,1.17549e-38],[&amp;quot;-2^96&amp;quot;,0,1.17549e-38],[&amp;quot;-2^95&amp;quot;,0,1.17549e-38],[&amp;quot;-2^94&amp;quot;,0,1.17549e-38],[&amp;quot;-2^93&amp;quot;,0,1.17549e-38],[&amp;quot;-2^92&amp;quot;,0,1.17549e-38],[&amp;quot;-2^91&amp;quot;,0,1.17549e-38],[&amp;quot;-2^90&amp;quot;,0,1.17549e-38],[&amp;quot;-2^89&amp;quot;,0,1.17549e-38],[&amp;quot;-2^88&amp;quot;,0,1.17549e-38],[&amp;quot;-2^87&amp;quot;,0,1.17549e-38],[&amp;quot;-2^86&amp;quot;,0,1.17549e-38],[&amp;quot;-2^85&amp;quot;,0,1.17549e-38],[&amp;quot;-2^84&amp;quot;,0,1.17549e-38],[&amp;quot;-2^83&amp;quot;,0,1.17549e-38],[&amp;quot;-2^82&amp;quot;,0,1.17549e-38],[&amp;quot;-2^81&amp;quot;,0,1.17549e-38],[&amp;quot;-2^80&amp;quot;,0,1.17549e-38],[&amp;quot;-2^79&amp;quot;,0,1.17549e-38],[&amp;quot;-2^78&amp;quot;,0,1.17549e-38],[&amp;quot;-2^77&amp;quot;,0,1.17549e-38],[&amp;quot;-2^76&amp;quot;,0,1.17549e-38],[&amp;quot;-2^75&amp;quot;,0,1.17549e-38],[&amp;quot;-2^74&amp;quot;,0,1.17549e-38],[&amp;quot;-2^73&amp;quot;,0,1.17549e-38],[&amp;quot;-2^72&amp;quot;,0,1.17549e-38],[&amp;quot;-2^71&amp;quot;,0,1.17549e-38],[&amp;quot;-2^70&amp;quot;,0,1.17549e-38],[&amp;quot;-2^69&amp;quot;,0,1.17549e-38],[&amp;quot;-2^68&amp;quot;,0,1.17549e-38],[&amp;quot;-2^67&amp;quot;,0,1.17549e-38],[&amp;quot;-2^66&amp;quot;,0,1.17549e-38],[&amp;quot;-2^65&amp;quot;,0,1.17549e-38],[&amp;quot;-2^64&amp;quot;,0,1.17549e-38],[&amp;quot;-2^63&amp;quot;,0,1.17549e-38],[&amp;quot;-2^62&amp;quot;,0,1.17549e-38],[&amp;quot;-2^61&amp;quot;,0,1.17549e-38],[&amp;quot;-2^60&amp;quot;,0,1.17549e-38],[&amp;quot;-2^59&amp;quot;,0,1.17549e-38],[&amp;quot;-2^58&amp;quot;,0,1.17549e-38],[&amp;quot;-2^57&amp;quot;,0,1.17549e-38],[&amp;quot;-2^56&amp;quot;,0,1.17549e-38],[&amp;quot;-2^55&amp;quot;,0,1.17549e-38],[&amp;quot;-2^54&amp;quot;,0,1.17549e-38],[&amp;quot;-2^53&amp;quot;,0,1.17549e-38],[&amp;quot;-2^52&amp;quot;,0,1.17549e-38],[&amp;quot;-2^51&amp;quot;,0,1.17549e-38],[&amp;quot;-2^50&amp;quot;,0,1.17549e-38],[&amp;quot;-2^49&amp;quot;,0,1.17549e-38],[&amp;quot;-2^48&amp;quot;,0,1.17549e-38],[&amp;quot;-2^47&amp;quot;,0,1.17549e-38],[&amp;quot;-2^46&amp;quot;,0,1.17549e-38],[&amp;quot;-2^45&amp;quot;,0,1.17549e-38],[&amp;quot;-2^44&amp;quot;,0,1.17549e-38],[&amp;quot;-2^43&amp;quot;,0,1.17549e-38],[&amp;quot;-2^42&amp;quot;,0,1.17549e-38],[&amp;quot;-2^41&amp;quot;,0,1.17549e-38],[&amp;quot;-2^40&amp;quot;,0,1.17549e-38],[&amp;quot;-2^39&amp;quot;,0,1.17549e-38],[&amp;quot;-2^38&amp;quot;,0,1.17549e-38],[&amp;quot;-2^37&amp;quot;,0,1.17549e-38],[&amp;quot;-2^36&amp;quot;,0,1.17549e-38],[&amp;quot;-2^35&amp;quot;,0,1.17549e-38],[&amp;quot;-2^34&amp;quot;,0,1.17549e-38],[&amp;quot;-2^33&amp;quot;,0,1.17549e-38],[&amp;quot;-2^32&amp;quot;,0,1.17549e-38],[&amp;quot;-2^31&amp;quot;,0,1.17549e-38],[&amp;quot;-2^30&amp;quot;,0,1.17549e-38],[&amp;quot;-2^29&amp;quot;,0,1.17549e-38],[&amp;quot;-2^28&amp;quot;,0,1.17549e-38],[&amp;quot;-2^27&amp;quot;,0,1.17549e-38],[&amp;quot;-2^26&amp;quot;,0,1.17549e-38],[&amp;quot;-2^25&amp;quot;,0,1.17549e-38],[&amp;quot;-2^24&amp;quot;,0,1.17549e-38],[&amp;quot;-2^23&amp;quot;,0,1.17549e-38],[&amp;quot;-2^22&amp;quot;,0,1.17549e-38],[&amp;quot;-2^21&amp;quot;,0,1.17549e-38],[&amp;quot;-2^20&amp;quot;,0,1.17549e-38],[&amp;quot;-2^19&amp;quot;,0,1.17549e-38],[&amp;quot;-2^18&amp;quot;,0,1.17549e-38],[&amp;quot;-2^17&amp;quot;,0,1.17549e-38],[&amp;quot;-2^16&amp;quot;,0,1.17549e-38],[&amp;quot;-2^15&amp;quot;,0,1.17549e-38],[&amp;quot;-2^14&amp;quot;,0,1.17549e-38],[&amp;quot;-2^13&amp;quot;,0,1.17549e-38],[&amp;quot;-2^12&amp;quot;,0,1.17549e-38],[&amp;quot;-2^11&amp;quot;,0,1.17549e-38],[&amp;quot;-2^10&amp;quot;,0,1.17549e-38],[&amp;quot;-2^9&amp;quot;,0,1.17549e-38],[&amp;quot;-2^8&amp;quot;,0,1.17549e-38],[&amp;quot;-2^7&amp;quot;,0,1.17549e-38],[&amp;quot;-2^6&amp;quot;,0,1.17549e-38],[&amp;quot;-2^5&amp;quot;,0,1.17549e-38],[&amp;quot;-2^4&amp;quot;,2.53131e-14,4.24683e-07],[&amp;quot;-2^3&amp;quot;,1.19138e-07,1.9988],[&amp;quot;-2^2&amp;quot;,1.46791e-07,2.46274],[&amp;quot;-2^1&amp;quot;,1.47723e-07,2.47839],[&amp;quot;-2^0&amp;quot;,1.40758e-07,2.36153],[&amp;quot;-2^-1&amp;quot;,1.13422e-07,2.23445],[&amp;quot;-2^-2&amp;quot;,6.38012e-08,2.14081],[&amp;quot;-2^-3&amp;quot;,2.69538e-08,1.80884],[&amp;quot;-2^-4&amp;quot;,1.21308e-08,1.62817],[&amp;quot;-2^-5&amp;quot;,5.59535e-09,1.50199],[&amp;quot;-2^-6&amp;quot;,2.69177e-09,1.44513],[&amp;quot;-2^-7&amp;quot;,1.31289e-09,1.4097],[&amp;quot;-2^-8&amp;quot;,6.49688e-10,1.3952],[&amp;quot;-2^-9&amp;quot;,3.32043e-10,1.42611],[&amp;quot;-2^-10&amp;quot;,1.66275e-10,1.4283],[&amp;quot;-2^-11&amp;quot;,8.08595e-11,1.38916],[&amp;quot;-2^-12&amp;quot;,3.8939e-11,1.33794],[&amp;quot;-2^-13&amp;quot;,1.9401e-11,1.33323],[&amp;quot;-2^-14&amp;quot;,7.88212e-12,1.08331],[&amp;quot;-2^-15&amp;quot;,3.71372e-12,1.02082],[&amp;quot;-2^-16&amp;quot;,1.82845e-12,1.0052],[&amp;quot;-2^-17&amp;quot;,9.10676e-13,1.0013],[&amp;quot;-2^-18&amp;quot;,4.54895e-13,1.00032],[&amp;quot;-2^-19&amp;quot;,2.27392e-13,1.00008],[&amp;quot;-2^-20&amp;quot;,1.13689e-13,1.00002],[&amp;quot;-2^-21&amp;quot;,5.68437e-14,1],[&amp;quot;-2^-22&amp;quot;,2.84217e-14,1],[&amp;quot;-2^-23&amp;quot;,1.42109e-14,1],[&amp;quot;-2^-24&amp;quot;,7.10543e-15,1],[&amp;quot;-2^-25&amp;quot;,3.55271e-15,1],[&amp;quot;-2^-26&amp;quot;,9.92617e-24,5.58794e-09],[&amp;quot;-2^-27&amp;quot;,1.65436e-24,1.86265e-09],[&amp;quot;-2^-28&amp;quot;,8.27181e-25,1.86265e-09],[&amp;quot;-2^-29&amp;quot;,4.1359e-25,1.86265e-09],[&amp;quot;-2^-30&amp;quot;,2.06795e-25,1.86265e-09],[&amp;quot;-2^-31&amp;quot;,1.03398e-25,1.86265e-09],[&amp;quot;-2^-32&amp;quot;,5.16988e-26,1.86265e-09],[&amp;quot;-2^-33&amp;quot;,2.58494e-26,1.86265e-09],[&amp;quot;-2^-34&amp;quot;,1.29247e-26,1.86265e-09],[&amp;quot;-2^-35&amp;quot;,6.46235e-27,1.86265e-09],[&amp;quot;-2^-36&amp;quot;,3.23117e-27,1.86265e-09],[&amp;quot;-2^-37&amp;quot;,1.61559e-27,1.86265e-09],[&amp;quot;-2^-38&amp;quot;,8.07794e-28,1.86265e-09],[&amp;quot;-2^-39&amp;quot;,4.03897e-28,1.86265e-09],[&amp;quot;-2^-40&amp;quot;,2.01948e-28,1.86265e-09],[&amp;quot;-2^-41&amp;quot;,1.00974e-28,1.86265e-09],[&amp;quot;-2^-42&amp;quot;,5.04871e-29,1.86265e-09],[&amp;quot;-2^-43&amp;quot;,2.52435e-29,1.86265e-09],[&amp;quot;-2^-44&amp;quot;,1.26218e-29,1.86265e-09],[&amp;quot;-2^-45&amp;quot;,6.31089e-30,1.86265e-09],[&amp;quot;-2^-46&amp;quot;,3.15544e-30,1.86265e-09],[&amp;quot;-2^-47&amp;quot;,1.57772e-30,1.86265e-09],[&amp;quot;-2^-48&amp;quot;,7.88861e-31,1.86265e-09],[&amp;quot;-2^-49&amp;quot;,3.9443e-31,1.86265e-09],[&amp;quot;-2^-50&amp;quot;,1.97215e-31,1.86265e-09],[&amp;quot;-2^-51&amp;quot;,9.86076e-32,1.86265e-09],[&amp;quot;-2^-52&amp;quot;,4.93038e-32,1.86265e-09],[&amp;quot;-2^-53&amp;quot;,2.46519e-32,1.86265e-09],[&amp;quot;-2^-54&amp;quot;,1.2326e-32,1.86265e-09],[&amp;quot;-2^-55&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-56&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-57&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-58&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-59&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-60&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-61&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-62&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-63&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-64&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-65&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-66&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-67&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-68&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-69&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-70&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-71&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-72&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-73&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-74&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-75&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-76&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-77&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-78&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-79&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-80&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-81&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-82&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-83&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-84&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-85&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-86&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-87&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-88&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-89&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-90&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-91&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-92&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-93&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-94&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-95&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-96&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-97&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-98&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-99&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-100&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-101&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-102&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-103&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-104&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-105&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-106&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-107&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-108&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-109&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-110&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-111&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-112&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-113&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-114&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-115&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-116&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-117&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-118&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-119&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-120&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-121&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-122&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-123&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-124&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-125&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-126&amp;quot;,0,1.17549e-38],[&amp;quot;-SN&amp;quot;,0,1.17549e-38],[&amp;quot;0&amp;quot;,0,1.17549e-38],[&amp;quot;+SN&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-126&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-125&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-124&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-123&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-122&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-121&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-120&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-119&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-118&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-117&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-116&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-115&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-114&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-113&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-112&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-111&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-110&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-109&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-108&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-107&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-106&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-105&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-104&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-103&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-102&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-101&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-100&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-99&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-98&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-97&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-96&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-95&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-94&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-93&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-92&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-91&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-90&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-89&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-88&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-87&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-86&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-85&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-84&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-83&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-82&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-81&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-80&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-79&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-78&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-77&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-76&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-75&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-74&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-73&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-72&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-71&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-70&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-69&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-68&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-67&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-66&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-65&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-64&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-63&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-62&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-61&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-60&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-59&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-58&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-57&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-56&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-55&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-54&amp;quot;,1.2326e-32,1.86265e-09],[&amp;quot;+2^-53&amp;quot;,2.46519e-32,1.86265e-09],[&amp;quot;+2^-52&amp;quot;,4.93038e-32,1.86265e-09],[&amp;quot;+2^-51&amp;quot;,9.86076e-32,1.86265e-09],[&amp;quot;+2^-50&amp;quot;,1.97215e-31,1.86265e-09],[&amp;quot;+2^-49&amp;quot;,3.9443e-31,1.86265e-09],[&amp;quot;+2^-48&amp;quot;,7.88861e-31,1.86265e-09],[&amp;quot;+2^-47&amp;quot;,1.57772e-30,1.86265e-09],[&amp;quot;+2^-46&amp;quot;,3.15544e-30,1.86265e-09],[&amp;quot;+2^-45&amp;quot;,6.31089e-30,1.86265e-09],[&amp;quot;+2^-44&amp;quot;,1.26218e-29,1.86265e-09],[&amp;quot;+2^-43&amp;quot;,2.52435e-29,1.86265e-09],[&amp;quot;+2^-42&amp;quot;,5.04871e-29,1.86265e-09],[&amp;quot;+2^-41&amp;quot;,1.00974e-28,1.86265e-09],[&amp;quot;+2^-40&amp;quot;,2.01948e-28,1.86265e-09],[&amp;quot;+2^-39&amp;quot;,4.03897e-28,1.86265e-09],[&amp;quot;+2^-38&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-37&amp;quot;,1.61559e-27,1.86265e-09],[&amp;quot;+2^-36&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-35&amp;quot;,6.46235e-27,1.86265e-09],[&amp;quot;+2^-34&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-33&amp;quot;,2.58494e-26,1.86265e-09],[&amp;quot;+2^-32&amp;quot;,5.16988e-26,1.86265e-09],[&amp;quot;+2^-31&amp;quot;,1.03398e-25,1.86265e-09],[&amp;quot;+2^-30&amp;quot;,2.06795e-25,1.86265e-09],[&amp;quot;+2^-29&amp;quot;,4.1359e-25,1.86265e-09],[&amp;quot;+2^-28&amp;quot;,8.27181e-25,1.86265e-09],[&amp;quot;+2^-27&amp;quot;,1.65436e-24,1.86265e-09],[&amp;quot;+2^-26&amp;quot;,9.92617e-24,5.58794e-09],[&amp;quot;+2^-25&amp;quot;,3.55271e-15,1],[&amp;quot;+2^-24&amp;quot;,7.10543e-15,1],[&amp;quot;+2^-23&amp;quot;,1.42109e-14,1],[&amp;quot;+2^-22&amp;quot;,2.84217e-14,1],[&amp;quot;+2^-21&amp;quot;,5.68437e-14,1],[&amp;quot;+2^-20&amp;quot;,1.13689e-13,1.00002],[&amp;quot;+2^-19&amp;quot;,2.27391e-13,1.00008],[&amp;quot;+2^-18&amp;quot;,4.54892e-13,1.00032],[&amp;quot;+2^-17&amp;quot;,9.10665e-13,1.00129],[&amp;quot;+2^-16&amp;quot;,1.82841e-12,1.00518],[&amp;quot;+2^-15&amp;quot;,3.71353e-12,1.02077],[&amp;quot;+2^-14&amp;quot;,1.39798e-11,1.92136],[&amp;quot;+2^-13&amp;quot;,2.63239e-11,1.80896],[&amp;quot;+2^-12&amp;quot;,5.37712e-11,1.84757],[&amp;quot;+2^-11&amp;quot;,1.09932e-10,1.88861],[&amp;quot;+2^-10&amp;quot;,2.24787e-10,1.93091],[&amp;quot;+2^-9&amp;quot;,4.3452e-10,1.86625],[&amp;quot;+2^-8&amp;quot;,1.01783e-09,2.18576],[&amp;quot;+2^-7&amp;quot;,2.10562e-09,2.26089],[&amp;quot;+2^-6&amp;quot;,4.03954e-09,2.16871],[&amp;quot;+2^-5&amp;quot;,8.15746e-09,2.18975],[&amp;quot;+2^-4&amp;quot;,1.5545e-08,2.08641],[&amp;quot;+2^-3&amp;quot;,2.62153e-08,2.05864],[&amp;quot;+2^-2&amp;quot;,4.09295e-08,1.73804],[&amp;quot;+2^-1&amp;quot;,8.02409e-08,1.34622],[&amp;quot;+2^0&amp;quot;,8.66201e-08,1.45324],[&amp;quot;+2^1&amp;quot;,8.8654e-08,1.48737],[&amp;quot;+2^2&amp;quot;,6.7976e-08,1.14045],[&amp;quot;+2^3&amp;quot;,7.44117e-08,1.24842],[&amp;quot;+2^4&amp;quot;,2.53131e-14,4.24683e-07],[&amp;quot;+2^5&amp;quot;,0,null],[&amp;quot;+2^6&amp;quot;,0,null],[&amp;quot;+2^7&amp;quot;,0,null],[&amp;quot;+2^8&amp;quot;,0,null],[&amp;quot;+2^9&amp;quot;,0,null],[&amp;quot;+2^10&amp;quot;,0,null],[&amp;quot;+2^11&amp;quot;,0,null],[&amp;quot;+2^12&amp;quot;,0,null],[&amp;quot;+2^13&amp;quot;,0,null],[&amp;quot;+2^14&amp;quot;,0,null],[&amp;quot;+2^15&amp;quot;,0,null],[&amp;quot;+2^16&amp;quot;,0,null],[&amp;quot;+2^17&amp;quot;,0,null],[&amp;quot;+2^18&amp;quot;,0,null],[&amp;quot;+2^19&amp;quot;,0,null],[&amp;quot;+2^20&amp;quot;,0,null],[&amp;quot;+2^21&amp;quot;,0,null],[&amp;quot;+2^22&amp;quot;,0,null],[&amp;quot;+2^23&amp;quot;,0,null],[&amp;quot;+2^24&amp;quot;,0,null],[&amp;quot;+2^25&amp;quot;,0,null],[&amp;quot;+2^26&amp;quot;,0,null],[&amp;quot;+2^27&amp;quot;,0,null],[&amp;quot;+2^28&amp;quot;,0,null],[&amp;quot;+2^29&amp;quot;,0,null],[&amp;quot;+2^30&amp;quot;,0,null],[&amp;quot;+2^31&amp;quot;,0,null],[&amp;quot;+2^32&amp;quot;,0,null],[&amp;quot;+2^33&amp;quot;,0,null],[&amp;quot;+2^34&amp;quot;,0,null],[&amp;quot;+2^35&amp;quot;,0,null],[&amp;quot;+2^36&amp;quot;,0,null],[&amp;quot;+2^37&amp;quot;,0,null],[&amp;quot;+2^38&amp;quot;,0,null],[&amp;quot;+2^39&amp;quot;,0,null],[&amp;quot;+2^40&amp;quot;,0,null],[&amp;quot;+2^41&amp;quot;,0,null],[&amp;quot;+2^42&amp;quot;,0,null],[&amp;quot;+2^43&amp;quot;,0,null],[&amp;quot;+2^44&amp;quot;,0,null],[&amp;quot;+2^45&amp;quot;,0,null],[&amp;quot;+2^46&amp;quot;,0,null],[&amp;quot;+2^47&amp;quot;,0,null],[&amp;quot;+2^48&amp;quot;,0,null],[&amp;quot;+2^49&amp;quot;,0,null],[&amp;quot;+2^50&amp;quot;,0,null],[&amp;quot;+2^51&amp;quot;,0,null],[&amp;quot;+2^52&amp;quot;,0,null],[&amp;quot;+2^53&amp;quot;,0,null],[&amp;quot;+2^54&amp;quot;,0,null],[&amp;quot;+2^55&amp;quot;,0,null],[&amp;quot;+2^56&amp;quot;,0,null],[&amp;quot;+2^57&amp;quot;,0,null],[&amp;quot;+2^58&amp;quot;,0,null],[&amp;quot;+2^59&amp;quot;,0,null],[&amp;quot;+2^60&amp;quot;,0,null],[&amp;quot;+2^61&amp;quot;,0,null],[&amp;quot;+2^62&amp;quot;,0,null],[&amp;quot;+2^63&amp;quot;,0,null],[&amp;quot;+2^64&amp;quot;,0,null],[&amp;quot;+2^65&amp;quot;,0,null],[&amp;quot;+2^66&amp;quot;,0,null],[&amp;quot;+2^67&amp;quot;,0,null],[&amp;quot;+2^68&amp;quot;,0,null],[&amp;quot;+2^69&amp;quot;,0,null],[&amp;quot;+2^70&amp;quot;,0,null],[&amp;quot;+2^71&amp;quot;,0,null],[&amp;quot;+2^72&amp;quot;,0,null],[&amp;quot;+2^73&amp;quot;,0,null],[&amp;quot;+2^74&amp;quot;,0,null],[&amp;quot;+2^75&amp;quot;,0,null],[&amp;quot;+2^76&amp;quot;,0,null],[&amp;quot;+2^77&amp;quot;,0,null],[&amp;quot;+2^78&amp;quot;,0,null],[&amp;quot;+2^79&amp;quot;,0,null],[&amp;quot;+2^80&amp;quot;,0,null],[&amp;quot;+2^81&amp;quot;,0,null],[&amp;quot;+2^82&amp;quot;,0,null],[&amp;quot;+2^83&amp;quot;,0,null],[&amp;quot;+2^84&amp;quot;,0,null],[&amp;quot;+2^85&amp;quot;,0,null],[&amp;quot;+2^86&amp;quot;,0,null],[&amp;quot;+2^87&amp;quot;,0,null],[&amp;quot;+2^88&amp;quot;,0,null],[&amp;quot;+2^89&amp;quot;,0,null],[&amp;quot;+2^90&amp;quot;,0,null],[&amp;quot;+2^91&amp;quot;,0,null],[&amp;quot;+2^92&amp;quot;,0,null],[&amp;quot;+2^93&amp;quot;,0,null],[&amp;quot;+2^94&amp;quot;,0,null],[&amp;quot;+2^95&amp;quot;,0,null],[&amp;quot;+2^96&amp;quot;,0,null],[&amp;quot;+2^97&amp;quot;,0,null],[&amp;quot;+2^98&amp;quot;,0,null],[&amp;quot;+2^99&amp;quot;,0,null],[&amp;quot;+2^100&amp;quot;,0,null],[&amp;quot;+2^101&amp;quot;,0,null],[&amp;quot;+2^102&amp;quot;,0,null],[&amp;quot;+2^103&amp;quot;,0,null],[&amp;quot;+2^104&amp;quot;,0,null],[&amp;quot;+2^105&amp;quot;,0,null],[&amp;quot;+2^106&amp;quot;,0,null],[&amp;quot;+2^107&amp;quot;,0,null],[&amp;quot;+2^108&amp;quot;,0,null],[&amp;quot;+2^109&amp;quot;,0,null],[&amp;quot;+2^110&amp;quot;,0,null],[&amp;quot;+2^111&amp;quot;,0,null],[&amp;quot;+2^112&amp;quot;,0,null],[&amp;quot;+2^113&amp;quot;,0,null],[&amp;quot;+2^114&amp;quot;,0,null],[&amp;quot;+2^115&amp;quot;,0,null],[&amp;quot;+2^116&amp;quot;,0,null],[&amp;quot;+2^117&amp;quot;,0,null],[&amp;quot;+2^118&amp;quot;,0,null],[&amp;quot;+2^119&amp;quot;,0,null],[&amp;quot;+2^120&amp;quot;,0,null],[&amp;quot;+2^121&amp;quot;,0,null],[&amp;quot;+2^122&amp;quot;,0,null],[&amp;quot;+2^123&amp;quot;,0,null],[&amp;quot;+2^124&amp;quot;,0,null],[&amp;quot;+2^125&amp;quot;,0,null],[&amp;quot;+2^126&amp;quot;,0,null],[&amp;quot;+2^127&amp;quot;,0,null] ]&lt;br /&gt;
 }}&lt;br /&gt;
&lt;br /&gt;
== Numerical Issues and Error Metrics ==&lt;br /&gt;
&lt;br /&gt;
To evaluate and compare different software implementations of &amp;lt;code&amp;gt;tanh&amp;lt;/code&amp;gt;, several numerical error metrics are used throughout this article. These metrics make it possible to reason about accuracy, robustness, and failure modes in floating-point arithmetic.&lt;br /&gt;
&lt;br /&gt;
=== ULP Error ===&lt;br /&gt;
ULP (Unit in the Last Place) measures the distance between two adjacent representable floating-point numbers. The ULP error of a computed result is defined as the difference, measured in ULPs, between the computed value and the correctly rounded reference value.&lt;br /&gt;
&lt;br /&gt;
An error of &amp;lt;code&amp;gt;ULP ≤ 2&amp;lt;/code&amp;gt; is commonly considered acceptable for single-precision transcendental functions, as it indicates that the result is very close to the correctly rounded value.&lt;br /&gt;
&lt;br /&gt;
Large ULP errors typically indicate catastrophic cancellation, loss of significant bits, or severe rounding effects.&lt;br /&gt;
&lt;br /&gt;
=== NaN and Overflow ===&lt;br /&gt;
NaN (Not a Number) results occur when undefined floating-point operations are performed, such as &amp;lt;code&amp;gt;inf / inf&amp;lt;/code&amp;gt;. In &amp;lt;code&amp;gt;tanh&amp;lt;/code&amp;gt; implementations, this often happens when both the numerator and denominator of a formulation overflow to infinity.&lt;br /&gt;
&lt;br /&gt;
Tracking NaN generation is critical, as NaNs can silently propagate through numerical pipelines and invalidate downstream results.&lt;br /&gt;
&lt;br /&gt;
=== Error Graphs ===&lt;br /&gt;
The error graphs shown in the following sections visualize ULP error across the full &amp;lt;code&amp;gt;float32&amp;lt;/code&amp;gt; input domain. They make it possible to identify:&lt;br /&gt;
* regions with catastrophic cancellation,&lt;br /&gt;
* input ranges that trigger overflow or NaNs,&lt;br /&gt;
* and numerical stability differences between formulations.&lt;br /&gt;
&lt;br /&gt;
These graphs are a key tool for understanding not only the maximum error, but also how errors are distributed across different input magnitudes.&lt;br /&gt;
&lt;br /&gt;
==Taylor==&lt;br /&gt;
&lt;br /&gt;
{{#widget:FloatErrorGraph|id=taylor&lt;br /&gt;
  |data=[ [&amp;quot;-2^127&amp;quot;,0,0],[&amp;quot;-2^126&amp;quot;,0,0],[&amp;quot;-2^125&amp;quot;,0,0],[&amp;quot;-2^124&amp;quot;,0,0],[&amp;quot;-2^123&amp;quot;,0,0],[&amp;quot;-2^122&amp;quot;,0,0],[&amp;quot;-2^121&amp;quot;,0,0],[&amp;quot;-2^120&amp;quot;,0,0],[&amp;quot;-2^119&amp;quot;,0,0],[&amp;quot;-2^118&amp;quot;,0,0],[&amp;quot;-2^117&amp;quot;,0,0],[&amp;quot;-2^116&amp;quot;,0,0],[&amp;quot;-2^115&amp;quot;,0,0],[&amp;quot;-2^114&amp;quot;,0,0],[&amp;quot;-2^113&amp;quot;,0,0],[&amp;quot;-2^112&amp;quot;,0,0],[&amp;quot;-2^111&amp;quot;,0,0],[&amp;quot;-2^110&amp;quot;,0,0],[&amp;quot;-2^109&amp;quot;,0,0],[&amp;quot;-2^108&amp;quot;,0,0],[&amp;quot;-2^107&amp;quot;,0,0],[&amp;quot;-2^106&amp;quot;,0,0],[&amp;quot;-2^105&amp;quot;,0,0],[&amp;quot;-2^104&amp;quot;,0,0],[&amp;quot;-2^103&amp;quot;,0,0],[&amp;quot;-2^102&amp;quot;,0,0],[&amp;quot;-2^101&amp;quot;,0,0],[&amp;quot;-2^100&amp;quot;,0,0],[&amp;quot;-2^99&amp;quot;,0,0],[&amp;quot;-2^98&amp;quot;,0,0],[&amp;quot;-2^97&amp;quot;,0,0],[&amp;quot;-2^96&amp;quot;,0,0],[&amp;quot;-2^95&amp;quot;,0,0],[&amp;quot;-2^94&amp;quot;,0,0],[&amp;quot;-2^93&amp;quot;,0,0],[&amp;quot;-2^92&amp;quot;,0,0],[&amp;quot;-2^91&amp;quot;,0,0],[&amp;quot;-2^90&amp;quot;,0,0],[&amp;quot;-2^89&amp;quot;,0,0],[&amp;quot;-2^88&amp;quot;,0,0],[&amp;quot;-2^87&amp;quot;,0,0],[&amp;quot;-2^86&amp;quot;,0,0],[&amp;quot;-2^85&amp;quot;,0,0],[&amp;quot;-2^84&amp;quot;,0,0],[&amp;quot;-2^83&amp;quot;,0,0],[&amp;quot;-2^82&amp;quot;,0,0],[&amp;quot;-2^81&amp;quot;,0,0],[&amp;quot;-2^80&amp;quot;,0,0],[&amp;quot;-2^79&amp;quot;,0,0],[&amp;quot;-2^78&amp;quot;,0,0],[&amp;quot;-2^77&amp;quot;,0,0],[&amp;quot;-2^76&amp;quot;,0,0],[&amp;quot;-2^75&amp;quot;,0,0],[&amp;quot;-2^74&amp;quot;,0,0],[&amp;quot;-2^73&amp;quot;,0,0],[&amp;quot;-2^72&amp;quot;,0,0],[&amp;quot;-2^71&amp;quot;,0,0],[&amp;quot;-2^70&amp;quot;,0,0],[&amp;quot;-2^69&amp;quot;,0,0],[&amp;quot;-2^68&amp;quot;,0,0],[&amp;quot;-2^67&amp;quot;,0,0],[&amp;quot;-2^66&amp;quot;,0,0],[&amp;quot;-2^65&amp;quot;,0,0],[&amp;quot;-2^64&amp;quot;,0,0],[&amp;quot;-2^63&amp;quot;,1.84467e+19,3.09485e+26],[&amp;quot;-2^62&amp;quot;,9.22336e+18,1.54742e+26],[&amp;quot;-2^61&amp;quot;,4.61168e+18,7.73712e+25],[&amp;quot;-2^60&amp;quot;,2.30584e+18,3.86856e+25],[&amp;quot;-2^59&amp;quot;,1.15292e+18,1.93428e+25],[&amp;quot;-2^58&amp;quot;,5.7646e+17,9.6714e+24],[&amp;quot;-2^57&amp;quot;,2.8823e+17,4.8357e+24],[&amp;quot;-2^56&amp;quot;,1.44115e+17,2.41785e+24],[&amp;quot;-2^55&amp;quot;,7.20575e+16,1.20892e+24],[&amp;quot;-2^54&amp;quot;,3.60288e+16,6.04462e+23],[&amp;quot;-2^53&amp;quot;,1.80144e+16,3.02231e+23],[&amp;quot;-2^52&amp;quot;,9.00719e+15,1.51116e+23],[&amp;quot;-2^51&amp;quot;,4.5036e+15,7.55578e+22],[&amp;quot;-2^50&amp;quot;,2.2518e+15,3.77789e+22],[&amp;quot;-2^49&amp;quot;,1.1259e+15,1.88894e+22],[&amp;quot;-2^48&amp;quot;,5.62949e+14,9.44472e+21],[&amp;quot;-2^47&amp;quot;,2.81475e+14,4.72236e+21],[&amp;quot;-2^46&amp;quot;,1.40737e+14,2.36118e+21],[&amp;quot;-2^45&amp;quot;,7.03687e+13,1.18059e+21],[&amp;quot;-2^44&amp;quot;,3.51843e+13,5.90295e+20],[&amp;quot;-2^43&amp;quot;,1.75922e+13,2.95148e+20],[&amp;quot;-2^42&amp;quot;,8.79608e+12,1.47574e+20],[&amp;quot;-2^41&amp;quot;,4.39804e+12,7.37869e+19],[&amp;quot;-2^40&amp;quot;,2.19902e+12,3.68935e+19],[&amp;quot;-2^39&amp;quot;,1.09951e+12,1.84467e+19],[&amp;quot;-2^38&amp;quot;,5.49755e+11,9.22336e+18],[&amp;quot;-2^37&amp;quot;,2.74878e+11,4.61168e+18],[&amp;quot;-2^36&amp;quot;,1.37439e+11,2.30584e+18],[&amp;quot;-2^35&amp;quot;,6.87194e+10,1.15292e+18],[&amp;quot;-2^34&amp;quot;,3.43597e+10,5.7646e+17],[&amp;quot;-2^33&amp;quot;,1.71799e+10,2.8823e+17],[&amp;quot;-2^32&amp;quot;,8.58993e+09,1.44115e+17],[&amp;quot;-2^31&amp;quot;,4.29085e+09,7.19885e+16],[&amp;quot;-2^30&amp;quot;,2.14542e+09,3.59943e+16],[&amp;quot;-2^29&amp;quot;,1.07271e+09,1.79971e+16],[&amp;quot;-2^28&amp;quot;,5.36356e+08,8.99856e+15],[&amp;quot;-2^27&amp;quot;,2.68178e+08,4.49928e+15],[&amp;quot;-2^26&amp;quot;,1.34089e+08,2.24964e+15],[&amp;quot;-2^25&amp;quot;,6.70445e+07,1.12482e+15],[&amp;quot;-2^24&amp;quot;,3.35223e+07,5.6241e+14],[&amp;quot;-2^23&amp;quot;,1.67611e+07,2.81205e+14],[&amp;quot;-2^22&amp;quot;,8.38056e+06,1.40603e+14],[&amp;quot;-2^21&amp;quot;,4.19028e+06,7.03013e+13],[&amp;quot;-2^20&amp;quot;,2.09514e+06,3.51506e+13],[&amp;quot;-2^19&amp;quot;,1.04757e+06,1.75753e+13],[&amp;quot;-2^18&amp;quot;,523784,8.78764e+12],[&amp;quot;-2^17&amp;quot;,261892,4.39381e+12],[&amp;quot;-2^16&amp;quot;,130945,2.1969e+12],[&amp;quot;-2^15&amp;quot;,65472.2,1.09844e+12],[&amp;quot;-2^14&amp;quot;,32735.6,5.49212e+11],[&amp;quot;-2^13&amp;quot;,16367.3,2.74598e+11],[&amp;quot;-2^12&amp;quot;,8183.15,1.3729e+11],[&amp;quot;-2^11&amp;quot;,4091.07,6.86368e+10],[&amp;quot;-2^10&amp;quot;,2045.04,3.431e+10],[&amp;quot;-2^9&amp;quot;,1022.02,1.71466e+10],[&amp;quot;-2^8&amp;quot;,510.509,8.56492e+09],[&amp;quot;-2^7&amp;quot;,254.754,4.27406e+09],[&amp;quot;-2^6&amp;quot;,126.876,2.12862e+09],[&amp;quot;-2^5&amp;quot;,62.9354,1.05588e+09],[&amp;quot;-2^4&amp;quot;,30.9629,5.19471e+08],[&amp;quot;-2^3&amp;quot;,14.9718,2.51185e+08],[&amp;quot;-2^2&amp;quot;,6.96672,1.16882e+08],[&amp;quot;-2^1&amp;quot;,2.94633,4.94312e+07],[&amp;quot;-2^0&amp;quot;,0.940715,1.57826e+07],[&amp;quot;-2^-1&amp;quot;,0.127635,2.14135e+06],[&amp;quot;-2^-2&amp;quot;,0.00318735,106950],[&amp;quot;-2^-3&amp;quot;,3.11475e-05,2090.28],[&amp;quot;-2^-4&amp;quot;,2.57737e-07,34.5929],[&amp;quot;-2^-5&amp;quot;,3.89213e-09,1.04478],[&amp;quot;-2^-6&amp;quot;,9.51472e-10,0.510818],[&amp;quot;-2^-7&amp;quot;,4.66402e-10,0.500795],[&amp;quot;-2^-8&amp;quot;,2.32917e-10,0.500185],[&amp;quot;-2^-9&amp;quot;,1.16423e-10,0.500033],[&amp;quot;-2^-10&amp;quot;,5.82082e-11,0.500004],[&amp;quot;-2^-11&amp;quot;,2.91037e-11,0.499998],[&amp;quot;-2^-12&amp;quot;,1.45519e-11,0.5],[&amp;quot;-2^-13&amp;quot;,4.85062e-12,0.333332],[&amp;quot;-2^-14&amp;quot;,6.06328e-13,0.0833331],[&amp;quot;-2^-15&amp;quot;,7.5791e-14,0.0208333],[&amp;quot;-2^-16&amp;quot;,9.47387e-15,0.00520832],[&amp;quot;-2^-17&amp;quot;,1.18424e-15,0.00130208],[&amp;quot;-2^-18&amp;quot;,1.48029e-16,0.00032552],[&amp;quot;-2^-19&amp;quot;,1.85039e-17,8.13808e-05],[&amp;quot;-2^-20&amp;quot;,2.31304e-18,2.03457e-05],[&amp;quot;-2^-21&amp;quot;,2.89156e-19,5.08688e-06],[&amp;quot;-2^-22&amp;quot;,3.61577e-20,1.27219e-06],[&amp;quot;-2^-23&amp;quot;,4.52633e-21,3.18512e-07],[&amp;quot;-2^-24&amp;quot;,5.691e-22,8.00937e-08],[&amp;quot;-2^-25&amp;quot;,7.27919e-23,2.04891e-08],[&amp;quot;-2^-26&amp;quot;,9.92617e-24,5.58794e-09],[&amp;quot;-2^-27&amp;quot;,1.65436e-24,1.86265e-09],[&amp;quot;-2^-28&amp;quot;,8.27181e-25,1.86265e-09],[&amp;quot;-2^-29&amp;quot;,4.1359e-25,1.86265e-09],[&amp;quot;-2^-30&amp;quot;,2.06795e-25,1.86265e-09],[&amp;quot;-2^-31&amp;quot;,1.03398e-25,1.86265e-09],[&amp;quot;-2^-32&amp;quot;,5.16988e-26,1.86265e-09],[&amp;quot;-2^-33&amp;quot;,2.58494e-26,1.86265e-09],[&amp;quot;-2^-34&amp;quot;,1.29247e-26,1.86265e-09],[&amp;quot;-2^-35&amp;quot;,6.46235e-27,1.86265e-09],[&amp;quot;-2^-36&amp;quot;,3.23117e-27,1.86265e-09],[&amp;quot;-2^-37&amp;quot;,1.61559e-27,1.86265e-09],[&amp;quot;-2^-38&amp;quot;,8.07794e-28,1.86265e-09],[&amp;quot;-2^-39&amp;quot;,4.03897e-28,1.86265e-09],[&amp;quot;-2^-40&amp;quot;,2.01948e-28,1.86265e-09],[&amp;quot;-2^-41&amp;quot;,1.00974e-28,1.86265e-09],[&amp;quot;-2^-42&amp;quot;,5.04871e-29,1.86265e-09],[&amp;quot;-2^-43&amp;quot;,2.52435e-29,1.86265e-09],[&amp;quot;-2^-44&amp;quot;,1.26218e-29,1.86265e-09],[&amp;quot;-2^-45&amp;quot;,6.31089e-30,1.86265e-09],[&amp;quot;-2^-46&amp;quot;,3.15544e-30,1.86265e-09],[&amp;quot;-2^-47&amp;quot;,1.57772e-30,1.86265e-09],[&amp;quot;-2^-48&amp;quot;,7.88861e-31,1.86265e-09],[&amp;quot;-2^-49&amp;quot;,3.9443e-31,1.86265e-09],[&amp;quot;-2^-50&amp;quot;,1.97215e-31,1.86265e-09],[&amp;quot;-2^-51&amp;quot;,9.86076e-32,1.86265e-09],[&amp;quot;-2^-52&amp;quot;,4.93038e-32,1.86265e-09],[&amp;quot;-2^-53&amp;quot;,2.46519e-32,1.86265e-09],[&amp;quot;-2^-54&amp;quot;,1.2326e-32,1.86265e-09],[&amp;quot;-2^-55&amp;quot;,0,0],[&amp;quot;-2^-56&amp;quot;,0,0],[&amp;quot;-2^-57&amp;quot;,0,0],[&amp;quot;-2^-58&amp;quot;,0,0],[&amp;quot;-2^-59&amp;quot;,0,0],[&amp;quot;-2^-60&amp;quot;,0,0],[&amp;quot;-2^-61&amp;quot;,0,0],[&amp;quot;-2^-62&amp;quot;,0,0],[&amp;quot;-2^-63&amp;quot;,0,0],[&amp;quot;-2^-64&amp;quot;,0,0],[&amp;quot;-2^-65&amp;quot;,0,0],[&amp;quot;-2^-66&amp;quot;,0,0],[&amp;quot;-2^-67&amp;quot;,0,0],[&amp;quot;-2^-68&amp;quot;,0,0],[&amp;quot;-2^-69&amp;quot;,0,0],[&amp;quot;-2^-70&amp;quot;,0,0],[&amp;quot;-2^-71&amp;quot;,0,0],[&amp;quot;-2^-72&amp;quot;,0,0],[&amp;quot;-2^-73&amp;quot;,0,0],[&amp;quot;-2^-74&amp;quot;,0,0],[&amp;quot;-2^-75&amp;quot;,0,0],[&amp;quot;-2^-76&amp;quot;,0,0],[&amp;quot;-2^-77&amp;quot;,0,0],[&amp;quot;-2^-78&amp;quot;,0,0],[&amp;quot;-2^-79&amp;quot;,0,0],[&amp;quot;-2^-80&amp;quot;,0,0],[&amp;quot;-2^-81&amp;quot;,0,0],[&amp;quot;-2^-82&amp;quot;,0,0],[&amp;quot;-2^-83&amp;quot;,0,0],[&amp;quot;-2^-84&amp;quot;,0,0],[&amp;quot;-2^-85&amp;quot;,0,0],[&amp;quot;-2^-86&amp;quot;,0,0],[&amp;quot;-2^-87&amp;quot;,0,0],[&amp;quot;-2^-88&amp;quot;,0,0],[&amp;quot;-2^-89&amp;quot;,0,0],[&amp;quot;-2^-90&amp;quot;,0,0],[&amp;quot;-2^-91&amp;quot;,0,0],[&amp;quot;-2^-92&amp;quot;,0,0],[&amp;quot;-2^-93&amp;quot;,0,0],[&amp;quot;-2^-94&amp;quot;,0,0],[&amp;quot;-2^-95&amp;quot;,0,0],[&amp;quot;-2^-96&amp;quot;,0,0],[&amp;quot;-2^-97&amp;quot;,0,0],[&amp;quot;-2^-98&amp;quot;,0,0],[&amp;quot;-2^-99&amp;quot;,0,0],[&amp;quot;-2^-100&amp;quot;,0,0],[&amp;quot;-2^-101&amp;quot;,0,0],[&amp;quot;-2^-102&amp;quot;,0,0],[&amp;quot;-2^-103&amp;quot;,0,0],[&amp;quot;-2^-104&amp;quot;,0,0],[&amp;quot;-2^-105&amp;quot;,0,0],[&amp;quot;-2^-106&amp;quot;,0,0],[&amp;quot;-2^-107&amp;quot;,0,0],[&amp;quot;-2^-108&amp;quot;,0,0],[&amp;quot;-2^-109&amp;quot;,0,0],[&amp;quot;-2^-110&amp;quot;,0,0],[&amp;quot;-2^-111&amp;quot;,0,0],[&amp;quot;-2^-112&amp;quot;,0,0],[&amp;quot;-2^-113&amp;quot;,0,0],[&amp;quot;-2^-114&amp;quot;,0,0],[&amp;quot;-2^-115&amp;quot;,0,0],[&amp;quot;-2^-116&amp;quot;,0,0],[&amp;quot;-2^-117&amp;quot;,0,0],[&amp;quot;-2^-118&amp;quot;,0,0],[&amp;quot;-2^-119&amp;quot;,0,0],[&amp;quot;-2^-120&amp;quot;,0,0],[&amp;quot;-2^-121&amp;quot;,0,0],[&amp;quot;-2^-122&amp;quot;,0,0],[&amp;quot;-2^-123&amp;quot;,0,0],[&amp;quot;-2^-124&amp;quot;,0,0],[&amp;quot;-2^-125&amp;quot;,0,0],[&amp;quot;-2^-126&amp;quot;,0,0],[&amp;quot;-SN&amp;quot;,0,0],[&amp;quot;0&amp;quot;,0,0],[&amp;quot;+SN&amp;quot;,0,0],[&amp;quot;+2^-126&amp;quot;,0,0],[&amp;quot;+2^-125&amp;quot;,0,0],[&amp;quot;+2^-124&amp;quot;,0,0],[&amp;quot;+2^-123&amp;quot;,0,0],[&amp;quot;+2^-122&amp;quot;,0,0],[&amp;quot;+2^-121&amp;quot;,0,0],[&amp;quot;+2^-120&amp;quot;,0,0],[&amp;quot;+2^-119&amp;quot;,0,0],[&amp;quot;+2^-118&amp;quot;,0,0],[&amp;quot;+2^-117&amp;quot;,0,0],[&amp;quot;+2^-116&amp;quot;,0,0],[&amp;quot;+2^-115&amp;quot;,0,0],[&amp;quot;+2^-114&amp;quot;,0,0],[&amp;quot;+2^-113&amp;quot;,0,0],[&amp;quot;+2^-112&amp;quot;,0,0],[&amp;quot;+2^-111&amp;quot;,0,0],[&amp;quot;+2^-110&amp;quot;,0,0],[&amp;quot;+2^-109&amp;quot;,0,0],[&amp;quot;+2^-108&amp;quot;,0,0],[&amp;quot;+2^-107&amp;quot;,0,0],[&amp;quot;+2^-106&amp;quot;,0,0],[&amp;quot;+2^-105&amp;quot;,0,0],[&amp;quot;+2^-104&amp;quot;,0,0],[&amp;quot;+2^-103&amp;quot;,0,0],[&amp;quot;+2^-102&amp;quot;,0,0],[&amp;quot;+2^-101&amp;quot;,0,0],[&amp;quot;+2^-100&amp;quot;,0,0],[&amp;quot;+2^-99&amp;quot;,0,0],[&amp;quot;+2^-98&amp;quot;,0,0],[&amp;quot;+2^-97&amp;quot;,0,0],[&amp;quot;+2^-96&amp;quot;,0,0],[&amp;quot;+2^-95&amp;quot;,0,0],[&amp;quot;+2^-94&amp;quot;,0,0],[&amp;quot;+2^-93&amp;quot;,0,0],[&amp;quot;+2^-92&amp;quot;,0,0],[&amp;quot;+2^-91&amp;quot;,0,0],[&amp;quot;+2^-90&amp;quot;,0,0],[&amp;quot;+2^-89&amp;quot;,0,0],[&amp;quot;+2^-88&amp;quot;,0,0],[&amp;quot;+2^-87&amp;quot;,0,0],[&amp;quot;+2^-86&amp;quot;,0,0],[&amp;quot;+2^-85&amp;quot;,0,0],[&amp;quot;+2^-84&amp;quot;,0,0],[&amp;quot;+2^-83&amp;quot;,0,0],[&amp;quot;+2^-82&amp;quot;,0,0],[&amp;quot;+2^-81&amp;quot;,0,0],[&amp;quot;+2^-80&amp;quot;,0,0],[&amp;quot;+2^-79&amp;quot;,0,0],[&amp;quot;+2^-78&amp;quot;,0,0],[&amp;quot;+2^-77&amp;quot;,0,0],[&amp;quot;+2^-76&amp;quot;,0,0],[&amp;quot;+2^-75&amp;quot;,0,0],[&amp;quot;+2^-74&amp;quot;,0,0],[&amp;quot;+2^-73&amp;quot;,0,0],[&amp;quot;+2^-72&amp;quot;,0,0],[&amp;quot;+2^-71&amp;quot;,0,0],[&amp;quot;+2^-70&amp;quot;,0,0],[&amp;quot;+2^-69&amp;quot;,0,0],[&amp;quot;+2^-68&amp;quot;,0,0],[&amp;quot;+2^-67&amp;quot;,0,0],[&amp;quot;+2^-66&amp;quot;,0,0],[&amp;quot;+2^-65&amp;quot;,0,0],[&amp;quot;+2^-64&amp;quot;,0,0],[&amp;quot;+2^-63&amp;quot;,0,0],[&amp;quot;+2^-62&amp;quot;,0,0],[&amp;quot;+2^-61&amp;quot;,0,0],[&amp;quot;+2^-60&amp;quot;,0,0],[&amp;quot;+2^-59&amp;quot;,0,0],[&amp;quot;+2^-58&amp;quot;,0,0],[&amp;quot;+2^-57&amp;quot;,0,0],[&amp;quot;+2^-56&amp;quot;,0,0],[&amp;quot;+2^-55&amp;quot;,0,0],[&amp;quot;+2^-54&amp;quot;,1.2326e-32,1.86265e-09],[&amp;quot;+2^-53&amp;quot;,2.46519e-32,1.86265e-09],[&amp;quot;+2^-52&amp;quot;,4.93038e-32,1.86265e-09],[&amp;quot;+2^-51&amp;quot;,9.86076e-32,1.86265e-09],[&amp;quot;+2^-50&amp;quot;,1.97215e-31,1.86265e-09],[&amp;quot;+2^-49&amp;quot;,3.9443e-31,1.86265e-09],[&amp;quot;+2^-48&amp;quot;,7.88861e-31,1.86265e-09],[&amp;quot;+2^-47&amp;quot;,1.57772e-30,1.86265e-09],[&amp;quot;+2^-46&amp;quot;,3.15544e-30,1.86265e-09],[&amp;quot;+2^-45&amp;quot;,6.31089e-30,1.86265e-09],[&amp;quot;+2^-44&amp;quot;,1.26218e-29,1.86265e-09],[&amp;quot;+2^-43&amp;quot;,2.52435e-29,1.86265e-09],[&amp;quot;+2^-42&amp;quot;,5.04871e-29,1.86265e-09],[&amp;quot;+2^-41&amp;quot;,1.00974e-28,1.86265e-09],[&amp;quot;+2^-40&amp;quot;,2.01948e-28,1.86265e-09],[&amp;quot;+2^-39&amp;quot;,4.03897e-28,1.86265e-09],[&amp;quot;+2^-38&amp;quot;,8.07794e-28,1.86265e-09],[&amp;quot;+2^-37&amp;quot;,1.61559e-27,1.86265e-09],[&amp;quot;+2^-36&amp;quot;,3.23117e-27,1.86265e-09],[&amp;quot;+2^-35&amp;quot;,6.46235e-27,1.86265e-09],[&amp;quot;+2^-34&amp;quot;,1.29247e-26,1.86265e-09],[&amp;quot;+2^-33&amp;quot;,2.58494e-26,1.86265e-09],[&amp;quot;+2^-32&amp;quot;,5.16988e-26,1.86265e-09],[&amp;quot;+2^-31&amp;quot;,1.03398e-25,1.86265e-09],[&amp;quot;+2^-30&amp;quot;,2.06795e-25,1.86265e-09],[&amp;quot;+2^-29&amp;quot;,4.1359e-25,1.86265e-09],[&amp;quot;+2^-28&amp;quot;,8.27181e-25,1.86265e-09],[&amp;quot;+2^-27&amp;quot;,1.65436e-24,1.86265e-09],[&amp;quot;+2^-26&amp;quot;,9.92617e-24,5.58794e-09],[&amp;quot;+2^-25&amp;quot;,7.27919e-23,2.04891e-08],[&amp;quot;+2^-24&amp;quot;,5.691e-22,8.00937e-08],[&amp;quot;+2^-23&amp;quot;,4.52633e-21,3.18512e-07],[&amp;quot;+2^-22&amp;quot;,3.61577e-20,1.27219e-06],[&amp;quot;+2^-21&amp;quot;,2.89156e-19,5.08688e-06],[&amp;quot;+2^-20&amp;quot;,2.31304e-18,2.03457e-05],[&amp;quot;+2^-19&amp;quot;,1.85039e-17,8.13808e-05],[&amp;quot;+2^-18&amp;quot;,1.48029e-16,0.00032552],[&amp;quot;+2^-17&amp;quot;,1.18424e-15,0.00130208],[&amp;quot;+2^-16&amp;quot;,9.47387e-15,0.00520832],[&amp;quot;+2^-15&amp;quot;,7.5791e-14,0.0208333],[&amp;quot;+2^-14&amp;quot;,6.06328e-13,0.0833331],[&amp;quot;+2^-13&amp;quot;,4.85062e-12,0.333332],[&amp;quot;+2^-12&amp;quot;,1.45519e-11,0.5],[&amp;quot;+2^-11&amp;quot;,2.91037e-11,0.499998],[&amp;quot;+2^-10&amp;quot;,5.82082e-11,0.500004],[&amp;quot;+2^-9&amp;quot;,1.16423e-10,0.500033],[&amp;quot;+2^-8&amp;quot;,2.32917e-10,0.500185],[&amp;quot;+2^-7&amp;quot;,4.66402e-10,0.500795],[&amp;quot;+2^-6&amp;quot;,9.51472e-10,0.510818],[&amp;quot;+2^-5&amp;quot;,3.89213e-09,1.04478],[&amp;quot;+2^-4&amp;quot;,2.57737e-07,34.5929],[&amp;quot;+2^-3&amp;quot;,3.11475e-05,2090.28],[&amp;quot;+2^-2&amp;quot;,0.00318735,106950],[&amp;quot;+2^-1&amp;quot;,0.127635,2.14135e+06],[&amp;quot;+2^0&amp;quot;,0.940715,1.57826e+07],[&amp;quot;+2^1&amp;quot;,2.94633,4.94312e+07],[&amp;quot;+2^2&amp;quot;,6.96672,1.16882e+08],[&amp;quot;+2^3&amp;quot;,14.9718,1.33874e+08],[&amp;quot;+2^4&amp;quot;,30.9629,2.59736e+08],[&amp;quot;+2^5&amp;quot;,62.9354,5.27941e+08],[&amp;quot;+2^6&amp;quot;,126.876,1.06431e+09],[&amp;quot;+2^7&amp;quot;,254.754,2.13703e+09],[&amp;quot;+2^8&amp;quot;,510.509,4.28246e+09],[&amp;quot;+2^9&amp;quot;,1022.02,8.57331e+09],[&amp;quot;+2^10&amp;quot;,2045.04,1.7155e+10],[&amp;quot;+2^11&amp;quot;,4091.07,3.43184e+10],[&amp;quot;+2^12&amp;quot;,8183.15,6.86452e+10],[&amp;quot;+2^13&amp;quot;,16367.3,1.37299e+11],[&amp;quot;+2^14&amp;quot;,32735.6,2.74606e+11],[&amp;quot;+2^15&amp;quot;,65472.2,5.4922e+11],[&amp;quot;+2^16&amp;quot;,130945,1.09845e+12],[&amp;quot;+2^17&amp;quot;,261892,2.19691e+12],[&amp;quot;+2^18&amp;quot;,523784,4.39382e+12],[&amp;quot;+2^19&amp;quot;,1.04757e+06,8.78765e+12],[&amp;quot;+2^20&amp;quot;,2.09514e+06,1.75753e+13],[&amp;quot;+2^21&amp;quot;,4.19028e+06,3.51506e+13],[&amp;quot;+2^22&amp;quot;,8.38056e+06,7.03013e+13],[&amp;quot;+2^23&amp;quot;,1.67611e+07,1.40603e+14],[&amp;quot;+2^24&amp;quot;,3.35223e+07,2.81205e+14],[&amp;quot;+2^25&amp;quot;,6.70445e+07,5.6241e+14],[&amp;quot;+2^26&amp;quot;,1.34089e+08,1.12482e+15],[&amp;quot;+2^27&amp;quot;,2.68178e+08,2.24964e+15],[&amp;quot;+2^28&amp;quot;,5.36356e+08,4.49928e+15],[&amp;quot;+2^29&amp;quot;,1.07271e+09,8.99856e+15],[&amp;quot;+2^30&amp;quot;,2.14542e+09,1.79971e+16],[&amp;quot;+2^31&amp;quot;,4.29085e+09,3.59943e+16],[&amp;quot;+2^32&amp;quot;,8.58993e+09,7.20575e+16],[&amp;quot;+2^33&amp;quot;,1.71799e+10,1.44115e+17],[&amp;quot;+2^34&amp;quot;,3.43597e+10,2.8823e+17],[&amp;quot;+2^35&amp;quot;,6.87194e+10,5.7646e+17],[&amp;quot;+2^36&amp;quot;,1.37439e+11,1.15292e+18],[&amp;quot;+2^37&amp;quot;,2.74878e+11,2.30584e+18],[&amp;quot;+2^38&amp;quot;,5.49755e+11,4.61168e+18],[&amp;quot;+2^39&amp;quot;,1.09951e+12,9.22336e+18],[&amp;quot;+2^40&amp;quot;,2.19902e+12,1.84467e+19],[&amp;quot;+2^41&amp;quot;,4.39804e+12,3.68935e+19],[&amp;quot;+2^42&amp;quot;,8.79608e+12,7.37869e+19],[&amp;quot;+2^43&amp;quot;,1.75922e+13,1.47574e+20],[&amp;quot;+2^44&amp;quot;,3.51843e+13,2.95148e+20],[&amp;quot;+2^45&amp;quot;,7.03687e+13,5.90295e+20],[&amp;quot;+2^46&amp;quot;,1.40737e+14,1.18059e+21],[&amp;quot;+2^47&amp;quot;,2.81475e+14,2.36118e+21],[&amp;quot;+2^48&amp;quot;,5.62949e+14,4.72236e+21],[&amp;quot;+2^49&amp;quot;,1.1259e+15,9.44472e+21],[&amp;quot;+2^50&amp;quot;,2.2518e+15,1.88894e+22],[&amp;quot;+2^51&amp;quot;,4.5036e+15,3.77789e+22],[&amp;quot;+2^52&amp;quot;,9.00719e+15,7.55578e+22],[&amp;quot;+2^53&amp;quot;,1.80144e+16,1.51116e+23],[&amp;quot;+2^54&amp;quot;,3.60288e+16,3.02231e+23],[&amp;quot;+2^55&amp;quot;,7.20575e+16,6.04462e+23],[&amp;quot;+2^56&amp;quot;,1.44115e+17,1.20892e+24],[&amp;quot;+2^57&amp;quot;,2.8823e+17,2.41785e+24],[&amp;quot;+2^58&amp;quot;,5.7646e+17,4.8357e+24],[&amp;quot;+2^59&amp;quot;,1.15292e+18,9.6714e+24],[&amp;quot;+2^60&amp;quot;,2.30584e+18,1.93428e+25],[&amp;quot;+2^61&amp;quot;,4.61168e+18,3.86856e+25],[&amp;quot;+2^62&amp;quot;,9.22336e+18,7.73712e+25],[&amp;quot;+2^63&amp;quot;,1.84467e+19,1.54742e+26],[&amp;quot;+2^64&amp;quot;,0,0],[&amp;quot;+2^65&amp;quot;,0,0],[&amp;quot;+2^66&amp;quot;,0,0],[&amp;quot;+2^67&amp;quot;,0,0],[&amp;quot;+2^68&amp;quot;,0,0],[&amp;quot;+2^69&amp;quot;,0,0],[&amp;quot;+2^70&amp;quot;,0,0],[&amp;quot;+2^71&amp;quot;,0,0],[&amp;quot;+2^72&amp;quot;,0,0],[&amp;quot;+2^73&amp;quot;,0,0],[&amp;quot;+2^74&amp;quot;,0,0],[&amp;quot;+2^75&amp;quot;,0,0],[&amp;quot;+2^76&amp;quot;,0,0],[&amp;quot;+2^77&amp;quot;,0,0],[&amp;quot;+2^78&amp;quot;,0,0],[&amp;quot;+2^79&amp;quot;,0,0],[&amp;quot;+2^80&amp;quot;,0,0],[&amp;quot;+2^81&amp;quot;,0,0],[&amp;quot;+2^82&amp;quot;,0,0],[&amp;quot;+2^83&amp;quot;,0,0],[&amp;quot;+2^84&amp;quot;,0,0],[&amp;quot;+2^85&amp;quot;,0,0],[&amp;quot;+2^86&amp;quot;,0,0],[&amp;quot;+2^87&amp;quot;,0,0],[&amp;quot;+2^88&amp;quot;,0,0],[&amp;quot;+2^89&amp;quot;,0,0],[&amp;quot;+2^90&amp;quot;,0,0],[&amp;quot;+2^91&amp;quot;,0,0],[&amp;quot;+2^92&amp;quot;,0,0],[&amp;quot;+2^93&amp;quot;,0,0],[&amp;quot;+2^94&amp;quot;,0,0],[&amp;quot;+2^95&amp;quot;,0,0],[&amp;quot;+2^96&amp;quot;,0,0],[&amp;quot;+2^97&amp;quot;,0,0],[&amp;quot;+2^98&amp;quot;,0,0],[&amp;quot;+2^99&amp;quot;,0,0],[&amp;quot;+2^100&amp;quot;,0,0],[&amp;quot;+2^101&amp;quot;,0,0],[&amp;quot;+2^102&amp;quot;,0,0],[&amp;quot;+2^103&amp;quot;,0,0],[&amp;quot;+2^104&amp;quot;,0,0],[&amp;quot;+2^105&amp;quot;,0,0],[&amp;quot;+2^106&amp;quot;,0,0],[&amp;quot;+2^107&amp;quot;,0,0],[&amp;quot;+2^108&amp;quot;,0,0],[&amp;quot;+2^109&amp;quot;,0,0],[&amp;quot;+2^110&amp;quot;,0,0],[&amp;quot;+2^111&amp;quot;,0,0],[&amp;quot;+2^112&amp;quot;,0,0],[&amp;quot;+2^113&amp;quot;,0,0],[&amp;quot;+2^114&amp;quot;,0,0],[&amp;quot;+2^115&amp;quot;,0,0],[&amp;quot;+2^116&amp;quot;,0,0],[&amp;quot;+2^117&amp;quot;,0,0],[&amp;quot;+2^118&amp;quot;,0,0],[&amp;quot;+2^119&amp;quot;,0,0],[&amp;quot;+2^120&amp;quot;,0,0],[&amp;quot;+2^121&amp;quot;,0,0],[&amp;quot;+2^122&amp;quot;,0,0],[&amp;quot;+2^123&amp;quot;,0,0],[&amp;quot;+2^124&amp;quot;,0,0],[&amp;quot;+2^125&amp;quot;,0,0],[&amp;quot;+2^126&amp;quot;,0,0],[&amp;quot;+2^127&amp;quot;,0,0] ]&lt;br /&gt;
 }}&lt;br /&gt;
&lt;br /&gt;
== Localized Approximation Functions ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
!Id&lt;br /&gt;
!Formula&lt;br /&gt;
!ULP &amp;lt;= 2.0&lt;br /&gt;
!NAN&lt;br /&gt;
|-&lt;br /&gt;
|signum&lt;br /&gt;
|&amp;lt;math&amp;gt;\sgn{x}&amp;lt;/math&amp;gt;&lt;br /&gt;
| -inf &amp;lt; x &amp;lt; -8.31763&lt;br /&gt;
8.31763 &amp;lt; x &amp;lt; inf&lt;br /&gt;
| -&lt;br /&gt;
|-&lt;br /&gt;
|poly1&lt;br /&gt;
|&amp;lt;math&amp;gt;x&amp;lt;/math&amp;gt;&lt;br /&gt;
| -0.000704229 &amp;lt; x &amp;lt; 0.000704229&lt;br /&gt;
| -&lt;br /&gt;
|-&lt;br /&gt;
|poly3&lt;br /&gt;
|&amp;lt;math&amp;gt;x - \frac{1}{3} x^3&amp;lt;/math&amp;gt;&lt;br /&gt;
| -0.0291781 &amp;lt; x &amp;lt; 0.0291781 &lt;br /&gt;
| -&lt;br /&gt;
|-&lt;br /&gt;
|lampert7&lt;br /&gt;
|&amp;lt;math&amp;gt;\frac{x \cdot (135135 + x^2 \cdot (17325 + x^2 \cdot (378 + x^2)))}{135135 + x^2 \cdot (62370 + x^2 \cdot (3150 + 28 \cdot x^2))}&lt;br /&gt;
&amp;lt;/math&amp;gt;&lt;br /&gt;
| -0.000947416 &amp;lt; x &amp;lt; 0.000947207&lt;br /&gt;
| -inf &amp;lt; x &amp;lt; -1.51629e+06&lt;br /&gt;
1.51629e+06 &amp;lt; x &amp;lt; inf&lt;br /&gt;
|-&lt;br /&gt;
|pade&lt;br /&gt;
|&amp;lt;math&amp;gt;x + x \cdot \frac{x^2 \cdot \left( p_1 \cdot x^2 + p_0 \right)}{\left( x^2 + q_1 \right) \cdot x^2 + q_0} &lt;br /&gt;
&lt;br /&gt;
&amp;lt;/math&amp;gt; &amp;lt;math&amp;gt;\begin{align}&lt;br /&gt;
p_0 &amp;amp;= -0.2059432032 \\&lt;br /&gt;
p_1 &amp;amp;= -0.0009577527 \\&lt;br /&gt;
q_0 &amp;amp;= 0.6178299136 \\ &lt;br /&gt;
q_1 &amp;amp;= 0.25&lt;br /&gt;
\end{align}&lt;br /&gt;
&amp;lt;/math&amp;gt;&lt;br /&gt;
| -0.0016973 &amp;lt; x &amp;lt; 0.0016973 &lt;br /&gt;
| -inf &amp;lt; x &amp;lt; -6.14896e+18&lt;br /&gt;
6.14896e+18 &amp;lt; x &amp;lt; inf&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== External Links ==&lt;br /&gt;
&lt;br /&gt;
* https://yaikhom.com/2020-04-28-localised-approximation-of-hyperbolic-tangents.html#mjx-eqn-eqn7-th%20degree%20lambert%20approximant&lt;br /&gt;
*https://github.com/llvm/llvm-project/blob/main/libc/src/math/generic/tanhf.cpp&lt;br /&gt;
*https://forums.developer.nvidia.com/t/hardware-accelerated-tanh-on-turing/173291&lt;br /&gt;
[[Category:Numerical Precision]]&lt;/div&gt;</summary>
		<author><name>Timo.stripf</name></author>
	</entry>
	<entry>
		<id>https://www.emmtrix.com/w139/index.php?title=tanh_Software_Implementation&amp;diff=2929</id>
		<title>tanh Software Implementation</title>
		<link rel="alternate" type="text/html" href="https://www.emmtrix.com/w139/index.php?title=tanh_Software_Implementation&amp;diff=2929"/>
		<updated>2026-02-04T11:56:11Z</updated>

		<summary type="html">&lt;p&gt;Timo.stripf: /* Numerical Issues and Error Metrics */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;[[File:Hyperbolic Tangent.svg|thumb]]&lt;br /&gt;
The hyperbolic tangent function, commonly referred to as &amp;lt;code&amp;gt;tanh&amp;lt;/code&amp;gt;, is a mathematical function that maps real numbers to the range &amp;lt;math&amp;gt;(-1, 1)&amp;lt;/math&amp;gt;. It is defined as:&lt;br /&gt;
&lt;br /&gt;
:&amp;lt;math&amp;gt;\tanh(x) = \frac{e^x - e^{-x}}{e^x + e^{-x}}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
From a software implementation perspective, &amp;lt;code&amp;gt;tanh&amp;lt;/code&amp;gt; is a numerically challenging function due to the use of exponential functions, which can easily overflow for large inputs and suffer from catastrophic cancellation for very small values of &amp;lt;math&amp;gt;x&amp;lt;/math&amp;gt;. These issues are particularly relevant in single-precision floating-point (&amp;lt;code&amp;gt;float32&amp;lt;/code&amp;gt;) implementations, where both accuracy and performance are critical.&lt;br /&gt;
&lt;br /&gt;
In machine learning and deep learning, &amp;lt;code&amp;gt;tanh&amp;lt;/code&amp;gt; is widely used as an activation function because it introduces non-linearity and produces outputs centered around zero, which can improve gradient-based optimization. As a result, &amp;lt;code&amp;gt;tanh&amp;lt;/code&amp;gt; is often evaluated billions of times during training and inference, making efficient and numerically stable implementations essential.&lt;br /&gt;
&lt;br /&gt;
This article focuses on practical software implementations of &amp;lt;code&amp;gt;tanh&amp;lt;/code&amp;gt;, analyzing different computational formulations with respect to floating-point accuracy, ULP error, overflow behavior, and NaN generation. The goal is to provide a foundation for designing robust &amp;lt;code&amp;gt;tanh&amp;lt;/code&amp;gt; implementations suitable for performance-critical environments such as numerical libraries, embedded systems, and AI workloads.&lt;br /&gt;
&lt;br /&gt;
== Problem Overview ==&lt;br /&gt;
The hyperbolic tangent function poses several challenges when implemented in finite-precision floating-point arithmetic. While the mathematical definition is simple, direct translations into software often suffer from numerical instability and performance issues.&lt;br /&gt;
&lt;br /&gt;
A primary challenge is the use of exponential functions. For large input values, &amp;lt;code&amp;gt;exp(x)&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;exp(-x)&amp;lt;/code&amp;gt; can overflow in single-precision floating-point arithmetic, leading to infinities and, in some formulations, NaN results. Avoiding overflow therefore requires careful reformulation or explicit range handling.&lt;br /&gt;
&lt;br /&gt;
For very small input values, catastrophic cancellation becomes the dominant issue. Expressions such as &amp;lt;math&amp;gt;e^x - e^{-x}&amp;lt;/math&amp;gt; involve the subtraction of nearly equal numbers, causing a severe loss of significant bits and large ULP errors. In these regions, naive implementations may return zero or highly inaccurate results even though &amp;lt;math&amp;gt;\tanh(x) \approx x&amp;lt;/math&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
In addition to accuracy concerns, performance is a critical factor. In many applications, especially machine learning and numerical simulations, &amp;lt;code&amp;gt;tanh&amp;lt;/code&amp;gt; is evaluated extremely frequently. This makes it necessary to balance numerical robustness with the cost of transcendental function calls, branch complexity, and instruction-level efficiency.&lt;br /&gt;
&lt;br /&gt;
==Direct Exponential Formulations==&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+&lt;br /&gt;
!Id&lt;br /&gt;
!Formula&lt;br /&gt;
!Highest ULP Error&lt;br /&gt;
!ULP &amp;lt;= 2.0&lt;br /&gt;
!NaN&lt;br /&gt;
|-&lt;br /&gt;
|exp_v1&lt;br /&gt;
|&amp;lt;math&amp;gt;\frac{e^x - e^{-x}}{e^x + e^{-x}}&amp;lt;/math&amp;gt;&lt;br /&gt;
|x &amp;lt; 0: 8.55638e+08 at -2.98023e-08 with 0x0p+0 instead of -0x1p-25&lt;br /&gt;
&lt;br /&gt;
x &amp;gt; 0: 8.55638e+08 at 2.98023e-08 with 0x0p+0 instead of 0x1p-25&lt;br /&gt;
| -88.7246 &amp;lt; x &amp;lt; -7.14197&lt;br /&gt;
7.14197 &amp;lt; x &amp;lt; 88.7246&lt;br /&gt;
| -inf &amp;lt; x &amp;lt; -88.7246&lt;br /&gt;
88.7246 &amp;lt; x &amp;lt; inf&lt;br /&gt;
|-&lt;br /&gt;
|exp_v2&lt;br /&gt;
|&amp;lt;math&amp;gt;\frac{e^{2x} - 1}{e^{2x} + 1}&amp;lt;/math&amp;gt;&lt;br /&gt;
|x &amp;lt; 0: 8.47249e+08 at -1.49012e-08 with 0x0p+0 instead of -0x1p-26&lt;br /&gt;
x &amp;gt; 0: 8.55638e+08 at 2.98023e-08 with 0x0p+0 instead of 0x1.fffffep-26&lt;br /&gt;
| -inf &amp;lt; x &amp;lt; -0.547821&lt;br /&gt;
0.254822 &amp;lt; x &amp;lt; 8.31787&lt;br /&gt;
&lt;br /&gt;
8.38379 &amp;lt; x &amp;lt; 44.3623&lt;br /&gt;
|44.3623 &amp;lt; x &amp;lt; inf&lt;br /&gt;
|-&lt;br /&gt;
|exp_v3&lt;br /&gt;
|&amp;lt;math&amp;gt;1 - \frac{2}{e^{2x} + 1}&amp;lt;/math&amp;gt;&lt;br /&gt;
|x &amp;lt; 0: 8.59832e+08 at -4.47035e-08 with 0x0p+0 instead of -0x1.8p-25&lt;br /&gt;
x &amp;gt; 0: 8.68221e+08 at 8.9407e-08 with 0x0p+0 instead of 0x1.7ffffcp-24&lt;br /&gt;
| -inf &amp;lt; x &amp;lt; -6.26233&lt;br /&gt;
0.346382 &amp;lt; x &amp;lt; inf&lt;br /&gt;
| -&lt;br /&gt;
|-&lt;br /&gt;
|expm1&lt;br /&gt;
|&amp;lt;math&amp;gt;\frac{expm1(2x)}{expm1(2x) + 2}&amp;lt;/math&amp;gt; &lt;br /&gt;
|x &amp;lt; 0: 2.49585 at -3.95627 with -0x1.ffa00ap-1 instead of -0x1.ffa00ep-1&lt;br /&gt;
x &amp;gt; 0: 2.41657 at 0.0155837 with 0x1.fe9b66p-7 instead of 0x1.fe9b62p-7&lt;br /&gt;
| -inf &amp;lt; x &amp;lt; -5.57227&lt;br /&gt;
-0.42981 &amp;lt; x &amp;lt; 0.00777948&lt;br /&gt;
&lt;br /&gt;
0.125351 &amp;lt; x &amp;lt; 44.3623&lt;br /&gt;
|44.3623 &amp;lt; x &amp;lt; inf&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
The first equation is the definition of the hyperbolic tangent function. This equation requires two exponentials with different values. Since exponentials are expensive to compute, we can use the second equation to reduce the number of exponentials to one.&lt;br /&gt;
&lt;br /&gt;
The second equation has the drawback that both the numerator and the denominator can become infinity for large values of x which results in NaN. That happens for &amp;lt;math&amp;gt;|x| &amp;gt; \log(FLT\_MAX)/2 \approx 44&amp;lt;/math&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
The third equation solves this problem but has a slightly higher error for &amp;lt;math&amp;gt;x \approx 2^{-25}&amp;lt;/math&amp;gt;. In the following graph you can see the error chart for equation 3. The error charts for the other two equations are very similar. All three equations have an error of up to 16 mio ULPs for &amp;lt;math&amp;gt;x &amp;lt; 2^{-5}&amp;lt;/math&amp;gt; which means that up to 24 bits could be wrong.&lt;br /&gt;
&lt;br /&gt;
=== exp_v2 ===&lt;br /&gt;
&lt;br /&gt;
{{#widget:FloatErrorGraph|id=tanhf_exp_v2&lt;br /&gt;
  |data=[ [&amp;quot;-2^127&amp;quot;,0,1.17549e-38],[&amp;quot;-2^126&amp;quot;,0,1.17549e-38],[&amp;quot;-2^125&amp;quot;,0,1.17549e-38],[&amp;quot;-2^124&amp;quot;,0,1.17549e-38],[&amp;quot;-2^123&amp;quot;,0,1.17549e-38],[&amp;quot;-2^122&amp;quot;,0,1.17549e-38],[&amp;quot;-2^121&amp;quot;,0,1.17549e-38],[&amp;quot;-2^120&amp;quot;,0,1.17549e-38],[&amp;quot;-2^119&amp;quot;,0,1.17549e-38],[&amp;quot;-2^118&amp;quot;,0,1.17549e-38],[&amp;quot;-2^117&amp;quot;,0,1.17549e-38],[&amp;quot;-2^116&amp;quot;,0,1.17549e-38],[&amp;quot;-2^115&amp;quot;,0,1.17549e-38],[&amp;quot;-2^114&amp;quot;,0,1.17549e-38],[&amp;quot;-2^113&amp;quot;,0,1.17549e-38],[&amp;quot;-2^112&amp;quot;,0,1.17549e-38],[&amp;quot;-2^111&amp;quot;,0,1.17549e-38],[&amp;quot;-2^110&amp;quot;,0,1.17549e-38],[&amp;quot;-2^109&amp;quot;,0,1.17549e-38],[&amp;quot;-2^108&amp;quot;,0,1.17549e-38],[&amp;quot;-2^107&amp;quot;,0,1.17549e-38],[&amp;quot;-2^106&amp;quot;,0,1.17549e-38],[&amp;quot;-2^105&amp;quot;,0,1.17549e-38],[&amp;quot;-2^104&amp;quot;,0,1.17549e-38],[&amp;quot;-2^103&amp;quot;,0,1.17549e-38],[&amp;quot;-2^102&amp;quot;,0,1.17549e-38],[&amp;quot;-2^101&amp;quot;,0,1.17549e-38],[&amp;quot;-2^100&amp;quot;,0,1.17549e-38],[&amp;quot;-2^99&amp;quot;,0,1.17549e-38],[&amp;quot;-2^98&amp;quot;,0,1.17549e-38],[&amp;quot;-2^97&amp;quot;,0,1.17549e-38],[&amp;quot;-2^96&amp;quot;,0,1.17549e-38],[&amp;quot;-2^95&amp;quot;,0,1.17549e-38],[&amp;quot;-2^94&amp;quot;,0,1.17549e-38],[&amp;quot;-2^93&amp;quot;,0,1.17549e-38],[&amp;quot;-2^92&amp;quot;,0,1.17549e-38],[&amp;quot;-2^91&amp;quot;,0,1.17549e-38],[&amp;quot;-2^90&amp;quot;,0,1.17549e-38],[&amp;quot;-2^89&amp;quot;,0,1.17549e-38],[&amp;quot;-2^88&amp;quot;,0,1.17549e-38],[&amp;quot;-2^87&amp;quot;,0,1.17549e-38],[&amp;quot;-2^86&amp;quot;,0,1.17549e-38],[&amp;quot;-2^85&amp;quot;,0,1.17549e-38],[&amp;quot;-2^84&amp;quot;,0,1.17549e-38],[&amp;quot;-2^83&amp;quot;,0,1.17549e-38],[&amp;quot;-2^82&amp;quot;,0,1.17549e-38],[&amp;quot;-2^81&amp;quot;,0,1.17549e-38],[&amp;quot;-2^80&amp;quot;,0,1.17549e-38],[&amp;quot;-2^79&amp;quot;,0,1.17549e-38],[&amp;quot;-2^78&amp;quot;,0,1.17549e-38],[&amp;quot;-2^77&amp;quot;,0,1.17549e-38],[&amp;quot;-2^76&amp;quot;,0,1.17549e-38],[&amp;quot;-2^75&amp;quot;,0,1.17549e-38],[&amp;quot;-2^74&amp;quot;,0,1.17549e-38],[&amp;quot;-2^73&amp;quot;,0,1.17549e-38],[&amp;quot;-2^72&amp;quot;,0,1.17549e-38],[&amp;quot;-2^71&amp;quot;,0,1.17549e-38],[&amp;quot;-2^70&amp;quot;,0,1.17549e-38],[&amp;quot;-2^69&amp;quot;,0,1.17549e-38],[&amp;quot;-2^68&amp;quot;,0,1.17549e-38],[&amp;quot;-2^67&amp;quot;,0,1.17549e-38],[&amp;quot;-2^66&amp;quot;,0,1.17549e-38],[&amp;quot;-2^65&amp;quot;,0,1.17549e-38],[&amp;quot;-2^64&amp;quot;,0,1.17549e-38],[&amp;quot;-2^63&amp;quot;,0,1.17549e-38],[&amp;quot;-2^62&amp;quot;,0,1.17549e-38],[&amp;quot;-2^61&amp;quot;,0,1.17549e-38],[&amp;quot;-2^60&amp;quot;,0,1.17549e-38],[&amp;quot;-2^59&amp;quot;,0,1.17549e-38],[&amp;quot;-2^58&amp;quot;,0,1.17549e-38],[&amp;quot;-2^57&amp;quot;,0,1.17549e-38],[&amp;quot;-2^56&amp;quot;,0,1.17549e-38],[&amp;quot;-2^55&amp;quot;,0,1.17549e-38],[&amp;quot;-2^54&amp;quot;,0,1.17549e-38],[&amp;quot;-2^53&amp;quot;,0,1.17549e-38],[&amp;quot;-2^52&amp;quot;,0,1.17549e-38],[&amp;quot;-2^51&amp;quot;,0,1.17549e-38],[&amp;quot;-2^50&amp;quot;,0,1.17549e-38],[&amp;quot;-2^49&amp;quot;,0,1.17549e-38],[&amp;quot;-2^48&amp;quot;,0,1.17549e-38],[&amp;quot;-2^47&amp;quot;,0,1.17549e-38],[&amp;quot;-2^46&amp;quot;,0,1.17549e-38],[&amp;quot;-2^45&amp;quot;,0,1.17549e-38],[&amp;quot;-2^44&amp;quot;,0,1.17549e-38],[&amp;quot;-2^43&amp;quot;,0,1.17549e-38],[&amp;quot;-2^42&amp;quot;,0,1.17549e-38],[&amp;quot;-2^41&amp;quot;,0,1.17549e-38],[&amp;quot;-2^40&amp;quot;,0,1.17549e-38],[&amp;quot;-2^39&amp;quot;,0,1.17549e-38],[&amp;quot;-2^38&amp;quot;,0,1.17549e-38],[&amp;quot;-2^37&amp;quot;,0,1.17549e-38],[&amp;quot;-2^36&amp;quot;,0,1.17549e-38],[&amp;quot;-2^35&amp;quot;,0,1.17549e-38],[&amp;quot;-2^34&amp;quot;,0,1.17549e-38],[&amp;quot;-2^33&amp;quot;,0,1.17549e-38],[&amp;quot;-2^32&amp;quot;,0,1.17549e-38],[&amp;quot;-2^31&amp;quot;,0,1.17549e-38],[&amp;quot;-2^30&amp;quot;,0,1.17549e-38],[&amp;quot;-2^29&amp;quot;,0,1.17549e-38],[&amp;quot;-2^28&amp;quot;,0,1.17549e-38],[&amp;quot;-2^27&amp;quot;,0,1.17549e-38],[&amp;quot;-2^26&amp;quot;,0,1.17549e-38],[&amp;quot;-2^25&amp;quot;,0,1.17549e-38],[&amp;quot;-2^24&amp;quot;,0,1.17549e-38],[&amp;quot;-2^23&amp;quot;,0,1.17549e-38],[&amp;quot;-2^22&amp;quot;,0,1.17549e-38],[&amp;quot;-2^21&amp;quot;,0,1.17549e-38],[&amp;quot;-2^20&amp;quot;,0,1.17549e-38],[&amp;quot;-2^19&amp;quot;,0,1.17549e-38],[&amp;quot;-2^18&amp;quot;,0,1.17549e-38],[&amp;quot;-2^17&amp;quot;,0,1.17549e-38],[&amp;quot;-2^16&amp;quot;,0,1.17549e-38],[&amp;quot;-2^15&amp;quot;,0,1.17549e-38],[&amp;quot;-2^14&amp;quot;,0,1.17549e-38],[&amp;quot;-2^13&amp;quot;,0,1.17549e-38],[&amp;quot;-2^12&amp;quot;,0,1.17549e-38],[&amp;quot;-2^11&amp;quot;,0,1.17549e-38],[&amp;quot;-2^10&amp;quot;,0,1.17549e-38],[&amp;quot;-2^9&amp;quot;,0,1.17549e-38],[&amp;quot;-2^8&amp;quot;,0,1.17549e-38],[&amp;quot;-2^7&amp;quot;,0,1.17549e-38],[&amp;quot;-2^6&amp;quot;,0,1.17549e-38],[&amp;quot;-2^5&amp;quot;,0,1.17549e-38],[&amp;quot;-2^4&amp;quot;,2.53131e-14,4.24683e-07],[&amp;quot;-2^3&amp;quot;,5.95888e-08,0.999734],[&amp;quot;-2^2&amp;quot;,8.86696e-08,1.48763],[&amp;quot;-2^1&amp;quot;,8.91609e-08,1.49587],[&amp;quot;-2^0&amp;quot;,8.73743e-08,1.4659],[&amp;quot;-2^-1&amp;quot;,8.65382e-08,2.13222],[&amp;quot;-2^-2&amp;quot;,6.18481e-08,2.61428],[&amp;quot;-2^-3&amp;quot;,3.88071e-08,3.08594],[&amp;quot;-2^-4&amp;quot;,2.6234e-08,4.23594],[&amp;quot;-2^-5&amp;quot;,2.02799e-08,8.19726],[&amp;quot;-2^-6&amp;quot;,1.75183e-08,12.4495],[&amp;quot;-2^-7&amp;quot;,1.61937e-08,17.3878],[&amp;quot;-2^-8&amp;quot;,1.5566e-08,33.4278],[&amp;quot;-2^-9&amp;quot;,1.51688e-08,65.1495],[&amp;quot;-2^-10&amp;quot;,1.50281e-08,129.09],[&amp;quot;-2^-11&amp;quot;,1.49613e-08,257.033],[&amp;quot;-2^-12&amp;quot;,1.49296e-08,512.977],[&amp;quot;-2^-13&amp;quot;,1.49174e-08,1025.12],[&amp;quot;-2^-14&amp;quot;,1.4909e-08,2049.08],[&amp;quot;-2^-15&amp;quot;,1.49049e-08,4097.02],[&amp;quot;-2^-16&amp;quot;,1.4903e-08,8193],[&amp;quot;-2^-17&amp;quot;,1.49021e-08,16385],[&amp;quot;-2^-18&amp;quot;,1.48862e-08,32735],[&amp;quot;-2^-19&amp;quot;,1.48973e-08,65519],[&amp;quot;-2^-20&amp;quot;,1.49001e-08,131063],[&amp;quot;-2^-21&amp;quot;,1.49009e-08,262139],[&amp;quot;-2^-22&amp;quot;,1.49011e-08,524285],[&amp;quot;-2^-23&amp;quot;,1.49011e-08,1.04857e+06],[&amp;quot;-2^-24&amp;quot;,1.49011e-08,2.09715e+06],[&amp;quot;-2^-25&amp;quot;,1.49012e-08,4.1943e+06],[&amp;quot;-2^-26&amp;quot;,1.49007e-08,8.38835e+06],[&amp;quot;-2^-27&amp;quot;,1.49012e-08,8.47249e+08],[&amp;quot;-2^-28&amp;quot;,7.45058e-09,8.38861e+08],[&amp;quot;-2^-29&amp;quot;,3.72529e-09,8.30472e+08],[&amp;quot;-2^-30&amp;quot;,1.86265e-09,8.22084e+08],[&amp;quot;-2^-31&amp;quot;,9.31323e-10,8.13695e+08],[&amp;quot;-2^-32&amp;quot;,4.65661e-10,8.05306e+08],[&amp;quot;-2^-33&amp;quot;,2.32831e-10,7.96918e+08],[&amp;quot;-2^-34&amp;quot;,1.16415e-10,7.88529e+08],[&amp;quot;-2^-35&amp;quot;,5.82077e-11,7.80141e+08],[&amp;quot;-2^-36&amp;quot;,2.91038e-11,7.71752e+08],[&amp;quot;-2^-37&amp;quot;,1.45519e-11,7.63363e+08],[&amp;quot;-2^-38&amp;quot;,7.27596e-12,7.54975e+08],[&amp;quot;-2^-39&amp;quot;,3.63798e-12,7.46586e+08],[&amp;quot;-2^-40&amp;quot;,1.81899e-12,7.38198e+08],[&amp;quot;-2^-41&amp;quot;,9.09495e-13,7.29809e+08],[&amp;quot;-2^-42&amp;quot;,4.54747e-13,7.2142e+08],[&amp;quot;-2^-43&amp;quot;,2.27374e-13,7.13032e+08],[&amp;quot;-2^-44&amp;quot;,1.13687e-13,7.04643e+08],[&amp;quot;-2^-45&amp;quot;,5.68434e-14,6.96254e+08],[&amp;quot;-2^-46&amp;quot;,2.84217e-14,6.87866e+08],[&amp;quot;-2^-47&amp;quot;,1.42109e-14,6.79477e+08],[&amp;quot;-2^-48&amp;quot;,7.10543e-15,6.71089e+08],[&amp;quot;-2^-49&amp;quot;,3.55271e-15,6.627e+08],[&amp;quot;-2^-50&amp;quot;,1.77636e-15,6.54311e+08],[&amp;quot;-2^-51&amp;quot;,8.88178e-16,6.45923e+08],[&amp;quot;-2^-52&amp;quot;,4.44089e-16,6.37534e+08],[&amp;quot;-2^-53&amp;quot;,2.22045e-16,6.29146e+08],[&amp;quot;-2^-54&amp;quot;,1.11022e-16,6.20757e+08],[&amp;quot;-2^-55&amp;quot;,5.55111e-17,6.12368e+08],[&amp;quot;-2^-56&amp;quot;,2.77556e-17,6.0398e+08],[&amp;quot;-2^-57&amp;quot;,1.38778e-17,5.95591e+08],[&amp;quot;-2^-58&amp;quot;,6.93889e-18,5.87203e+08],[&amp;quot;-2^-59&amp;quot;,3.46945e-18,5.78814e+08],[&amp;quot;-2^-60&amp;quot;,1.73472e-18,5.70425e+08],[&amp;quot;-2^-61&amp;quot;,8.67362e-19,5.62037e+08],[&amp;quot;-2^-62&amp;quot;,4.33681e-19,5.53648e+08],[&amp;quot;-2^-63&amp;quot;,2.1684e-19,5.4526e+08],[&amp;quot;-2^-64&amp;quot;,1.0842e-19,5.36871e+08],[&amp;quot;-2^-65&amp;quot;,5.42101e-20,5.28482e+08],[&amp;quot;-2^-66&amp;quot;,2.71051e-20,5.20094e+08],[&amp;quot;-2^-67&amp;quot;,1.35525e-20,5.11705e+08],[&amp;quot;-2^-68&amp;quot;,6.77626e-21,5.03316e+08],[&amp;quot;-2^-69&amp;quot;,3.38813e-21,4.94928e+08],[&amp;quot;-2^-70&amp;quot;,1.69407e-21,4.86539e+08],[&amp;quot;-2^-71&amp;quot;,8.47033e-22,4.78151e+08],[&amp;quot;-2^-72&amp;quot;,4.23516e-22,4.69762e+08],[&amp;quot;-2^-73&amp;quot;,2.11758e-22,4.61373e+08],[&amp;quot;-2^-74&amp;quot;,1.05879e-22,4.52985e+08],[&amp;quot;-2^-75&amp;quot;,5.29396e-23,4.44596e+08],[&amp;quot;-2^-76&amp;quot;,2.64698e-23,4.36208e+08],[&amp;quot;-2^-77&amp;quot;,1.32349e-23,4.27819e+08],[&amp;quot;-2^-78&amp;quot;,6.61744e-24,4.1943e+08],[&amp;quot;-2^-79&amp;quot;,3.30872e-24,4.11042e+08],[&amp;quot;-2^-80&amp;quot;,1.65436e-24,4.02653e+08],[&amp;quot;-2^-81&amp;quot;,8.27181e-25,3.94265e+08],[&amp;quot;-2^-82&amp;quot;,4.1359e-25,3.85876e+08],[&amp;quot;-2^-83&amp;quot;,2.06795e-25,3.77487e+08],[&amp;quot;-2^-84&amp;quot;,1.03398e-25,3.69099e+08],[&amp;quot;-2^-85&amp;quot;,5.16988e-26,3.6071e+08],[&amp;quot;-2^-86&amp;quot;,2.58494e-26,3.52322e+08],[&amp;quot;-2^-87&amp;quot;,1.29247e-26,3.43933e+08],[&amp;quot;-2^-88&amp;quot;,6.46235e-27,3.35544e+08],[&amp;quot;-2^-89&amp;quot;,3.23117e-27,3.27156e+08],[&amp;quot;-2^-90&amp;quot;,1.61559e-27,3.18767e+08],[&amp;quot;-2^-91&amp;quot;,8.07794e-28,3.10378e+08],[&amp;quot;-2^-92&amp;quot;,4.03897e-28,3.0199e+08],[&amp;quot;-2^-93&amp;quot;,2.01948e-28,2.93601e+08],[&amp;quot;-2^-94&amp;quot;,1.00974e-28,2.85213e+08],[&amp;quot;-2^-95&amp;quot;,5.04871e-29,2.76824e+08],[&amp;quot;-2^-96&amp;quot;,2.52435e-29,2.68435e+08],[&amp;quot;-2^-97&amp;quot;,1.26218e-29,2.60047e+08],[&amp;quot;-2^-98&amp;quot;,6.31089e-30,2.51658e+08],[&amp;quot;-2^-99&amp;quot;,3.15544e-30,2.4327e+08],[&amp;quot;-2^-100&amp;quot;,1.57772e-30,2.34881e+08],[&amp;quot;-2^-101&amp;quot;,7.88861e-31,2.26492e+08],[&amp;quot;-2^-102&amp;quot;,3.9443e-31,2.18104e+08],[&amp;quot;-2^-103&amp;quot;,1.97215e-31,2.09715e+08],[&amp;quot;-2^-104&amp;quot;,9.86076e-32,2.01327e+08],[&amp;quot;-2^-105&amp;quot;,4.93038e-32,1.92938e+08],[&amp;quot;-2^-106&amp;quot;,2.46519e-32,1.84549e+08],[&amp;quot;-2^-107&amp;quot;,1.2326e-32,1.76161e+08],[&amp;quot;-2^-108&amp;quot;,6.16298e-33,1.67772e+08],[&amp;quot;-2^-109&amp;quot;,3.08149e-33,1.59384e+08],[&amp;quot;-2^-110&amp;quot;,1.54074e-33,1.50995e+08],[&amp;quot;-2^-111&amp;quot;,7.70372e-34,1.42606e+08],[&amp;quot;-2^-112&amp;quot;,3.85186e-34,1.34218e+08],[&amp;quot;-2^-113&amp;quot;,1.92593e-34,1.25829e+08],[&amp;quot;-2^-114&amp;quot;,9.62965e-35,1.17441e+08],[&amp;quot;-2^-115&amp;quot;,4.81482e-35,1.09052e+08],[&amp;quot;-2^-116&amp;quot;,2.40741e-35,1.00663e+08],[&amp;quot;-2^-117&amp;quot;,1.20371e-35,9.22747e+07],[&amp;quot;-2^-118&amp;quot;,6.01853e-36,8.38861e+07],[&amp;quot;-2^-119&amp;quot;,3.00927e-36,7.54975e+07],[&amp;quot;-2^-120&amp;quot;,1.50463e-36,6.71089e+07],[&amp;quot;-2^-121&amp;quot;,7.52316e-37,5.87203e+07],[&amp;quot;-2^-122&amp;quot;,3.76158e-37,5.03316e+07],[&amp;quot;-2^-123&amp;quot;,1.88079e-37,4.1943e+07],[&amp;quot;-2^-124&amp;quot;,9.40395e-38,3.35544e+07],[&amp;quot;-2^-125&amp;quot;,4.70198e-38,2.51658e+07],[&amp;quot;-2^-126&amp;quot;,2.35099e-38,1.67772e+07],[&amp;quot;-SN&amp;quot;,1.17546e-38,8.38835e+06],[&amp;quot;0&amp;quot;,0,1.17549e-38],[&amp;quot;+SN&amp;quot;,1.17546e-38,8.38835e+06],[&amp;quot;+2^-126&amp;quot;,2.35095e-38,1.6777e+07],[&amp;quot;+2^-125&amp;quot;,4.70191e-38,2.51656e+07],[&amp;quot;+2^-124&amp;quot;,9.40381e-38,3.35542e+07],[&amp;quot;+2^-123&amp;quot;,1.88076e-37,4.19428e+07],[&amp;quot;+2^-122&amp;quot;,3.76152e-37,5.03314e+07],[&amp;quot;+2^-121&amp;quot;,7.52305e-37,5.872e+07],[&amp;quot;+2^-120&amp;quot;,1.50461e-36,6.71086e+07],[&amp;quot;+2^-119&amp;quot;,3.00922e-36,7.54972e+07],[&amp;quot;+2^-118&amp;quot;,6.01844e-36,8.38858e+07],[&amp;quot;+2^-117&amp;quot;,1.20369e-35,9.22744e+07],[&amp;quot;+2^-116&amp;quot;,2.40738e-35,1.00663e+08],[&amp;quot;+2^-115&amp;quot;,4.81475e-35,1.09052e+08],[&amp;quot;+2^-114&amp;quot;,9.6295e-35,1.1744e+08],[&amp;quot;+2^-113&amp;quot;,1.9259e-34,1.25829e+08],[&amp;quot;+2^-112&amp;quot;,3.8518e-34,1.34217e+08],[&amp;quot;+2^-111&amp;quot;,7.7036e-34,1.42606e+08],[&amp;quot;+2^-110&amp;quot;,1.54072e-33,1.50995e+08],[&amp;quot;+2^-109&amp;quot;,3.08144e-33,1.59383e+08],[&amp;quot;+2^-108&amp;quot;,6.16288e-33,1.67772e+08],[&amp;quot;+2^-107&amp;quot;,1.23258e-32,1.76161e+08],[&amp;quot;+2^-106&amp;quot;,2.46515e-32,1.84549e+08],[&amp;quot;+2^-105&amp;quot;,4.93031e-32,1.92938e+08],[&amp;quot;+2^-104&amp;quot;,9.86061e-32,2.01326e+08],[&amp;quot;+2^-103&amp;quot;,1.97212e-31,2.09715e+08],[&amp;quot;+2^-102&amp;quot;,3.94424e-31,2.18104e+08],[&amp;quot;+2^-101&amp;quot;,7.88849e-31,2.26492e+08],[&amp;quot;+2^-100&amp;quot;,1.5777e-30,2.34881e+08],[&amp;quot;+2^-99&amp;quot;,3.1554e-30,2.43269e+08],[&amp;quot;+2^-98&amp;quot;,6.31079e-30,2.51658e+08],[&amp;quot;+2^-97&amp;quot;,1.26216e-29,2.60047e+08],[&amp;quot;+2^-96&amp;quot;,2.52432e-29,2.68435e+08],[&amp;quot;+2^-95&amp;quot;,5.04863e-29,2.76824e+08],[&amp;quot;+2^-94&amp;quot;,1.00973e-28,2.85212e+08],[&amp;quot;+2^-93&amp;quot;,2.01945e-28,2.93601e+08],[&amp;quot;+2^-92&amp;quot;,4.03891e-28,3.0199e+08],[&amp;quot;+2^-91&amp;quot;,8.07781e-28,3.10378e+08],[&amp;quot;+2^-90&amp;quot;,1.61556e-27,3.18767e+08],[&amp;quot;+2^-89&amp;quot;,3.23112e-27,3.27155e+08],[&amp;quot;+2^-88&amp;quot;,6.46225e-27,3.35544e+08],[&amp;quot;+2^-87&amp;quot;,1.29245e-26,3.43933e+08],[&amp;quot;+2^-86&amp;quot;,2.5849e-26,3.52321e+08],[&amp;quot;+2^-85&amp;quot;,5.1698e-26,3.6071e+08],[&amp;quot;+2^-84&amp;quot;,1.03396e-25,3.69098e+08],[&amp;quot;+2^-83&amp;quot;,2.06792e-25,3.77487e+08],[&amp;quot;+2^-82&amp;quot;,4.13584e-25,3.85876e+08],[&amp;quot;+2^-81&amp;quot;,8.27168e-25,3.94264e+08],[&amp;quot;+2^-80&amp;quot;,1.65434e-24,4.02653e+08],[&amp;quot;+2^-79&amp;quot;,3.30867e-24,4.11042e+08],[&amp;quot;+2^-78&amp;quot;,6.61734e-24,4.1943e+08],[&amp;quot;+2^-77&amp;quot;,1.32347e-23,4.27819e+08],[&amp;quot;+2^-76&amp;quot;,2.64694e-23,4.36207e+08],[&amp;quot;+2^-75&amp;quot;,5.29388e-23,4.44596e+08],[&amp;quot;+2^-74&amp;quot;,1.05878e-22,4.52985e+08],[&amp;quot;+2^-73&amp;quot;,2.11755e-22,4.61373e+08],[&amp;quot;+2^-72&amp;quot;,4.2351e-22,4.69762e+08],[&amp;quot;+2^-71&amp;quot;,8.4702e-22,4.7815e+08],[&amp;quot;+2^-70&amp;quot;,1.69404e-21,4.86539e+08],[&amp;quot;+2^-69&amp;quot;,3.38808e-21,4.94928e+08],[&amp;quot;+2^-68&amp;quot;,6.77616e-21,5.03316e+08],[&amp;quot;+2^-67&amp;quot;,1.35523e-20,5.11705e+08],[&amp;quot;+2^-66&amp;quot;,2.71046e-20,5.20093e+08],[&amp;quot;+2^-65&amp;quot;,5.42093e-20,5.28482e+08],[&amp;quot;+2^-64&amp;quot;,1.08419e-19,5.36871e+08],[&amp;quot;+2^-63&amp;quot;,2.16837e-19,5.45259e+08],[&amp;quot;+2^-62&amp;quot;,4.33674e-19,5.53648e+08],[&amp;quot;+2^-61&amp;quot;,8.67349e-19,5.62036e+08],[&amp;quot;+2^-60&amp;quot;,1.7347e-18,5.70425e+08],[&amp;quot;+2^-59&amp;quot;,3.46939e-18,5.78814e+08],[&amp;quot;+2^-58&amp;quot;,6.93879e-18,5.87202e+08],[&amp;quot;+2^-57&amp;quot;,1.38776e-17,5.95591e+08],[&amp;quot;+2^-56&amp;quot;,2.77552e-17,6.0398e+08],[&amp;quot;+2^-55&amp;quot;,5.55103e-17,6.12368e+08],[&amp;quot;+2^-54&amp;quot;,1.11021e-16,6.20757e+08],[&amp;quot;+2^-53&amp;quot;,2.22041e-16,6.29145e+08],[&amp;quot;+2^-52&amp;quot;,4.44082e-16,6.37534e+08],[&amp;quot;+2^-51&amp;quot;,8.88165e-16,6.45923e+08],[&amp;quot;+2^-50&amp;quot;,1.77633e-15,6.54311e+08],[&amp;quot;+2^-49&amp;quot;,3.55266e-15,6.627e+08],[&amp;quot;+2^-48&amp;quot;,7.10532e-15,6.71088e+08],[&amp;quot;+2^-47&amp;quot;,1.42106e-14,6.79477e+08],[&amp;quot;+2^-46&amp;quot;,2.84213e-14,6.87866e+08],[&amp;quot;+2^-45&amp;quot;,5.68426e-14,6.96254e+08],[&amp;quot;+2^-44&amp;quot;,1.13685e-13,7.04643e+08],[&amp;quot;+2^-43&amp;quot;,2.2737e-13,7.13031e+08],[&amp;quot;+2^-42&amp;quot;,4.5474e-13,7.2142e+08],[&amp;quot;+2^-41&amp;quot;,9.09481e-13,7.29809e+08],[&amp;quot;+2^-40&amp;quot;,1.81896e-12,7.38197e+08],[&amp;quot;+2^-39&amp;quot;,3.63792e-12,7.46586e+08],[&amp;quot;+2^-38&amp;quot;,7.27585e-12,7.54974e+08],[&amp;quot;+2^-37&amp;quot;,1.45517e-11,7.63363e+08],[&amp;quot;+2^-36&amp;quot;,2.91034e-11,7.71752e+08],[&amp;quot;+2^-35&amp;quot;,5.82068e-11,7.8014e+08],[&amp;quot;+2^-34&amp;quot;,1.16414e-10,7.88529e+08],[&amp;quot;+2^-33&amp;quot;,2.32827e-10,7.96918e+08],[&amp;quot;+2^-32&amp;quot;,4.65654e-10,8.05306e+08],[&amp;quot;+2^-31&amp;quot;,9.31308e-10,8.13695e+08],[&amp;quot;+2^-30&amp;quot;,1.86262e-09,8.22083e+08],[&amp;quot;+2^-29&amp;quot;,3.72523e-09,8.30472e+08],[&amp;quot;+2^-28&amp;quot;,7.45047e-09,8.38861e+08],[&amp;quot;+2^-27&amp;quot;,1.49009e-08,8.47249e+08],[&amp;quot;+2^-26&amp;quot;,2.98019e-08,8.55638e+08],[&amp;quot;+2^-25&amp;quot;,2.98023e-08,8.38861e+06],[&amp;quot;+2^-24&amp;quot;,2.98023e-08,4.1943e+06],[&amp;quot;+2^-23&amp;quot;,2.98023e-08,2.09715e+06],[&amp;quot;+2^-22&amp;quot;,2.98023e-08,1.04857e+06],[&amp;quot;+2^-21&amp;quot;,2.98021e-08,524284],[&amp;quot;+2^-20&amp;quot;,2.98014e-08,262136],[&amp;quot;+2^-19&amp;quot;,2.97987e-08,131056],[&amp;quot;+2^-18&amp;quot;,2.97878e-08,65504],[&amp;quot;+2^-17&amp;quot;,2.98014e-08,32767],[&amp;quot;+2^-16&amp;quot;,2.98005e-08,16384],[&amp;quot;+2^-15&amp;quot;,2.98023e-08,8192],[&amp;quot;+2^-14&amp;quot;,2.98025e-08,4351.99],[&amp;quot;+2^-13&amp;quot;,2.98044e-08,2815.96],[&amp;quot;+2^-12&amp;quot;,2.98091e-08,1024.23],[&amp;quot;+2^-11&amp;quot;,2.9812e-08,512.167],[&amp;quot;+2^-10&amp;quot;,2.98154e-08,256.113],[&amp;quot;+2^-9&amp;quot;,2.98591e-08,128.244],[&amp;quot;+2^-8&amp;quot;,3.00204e-08,85.3328],[&amp;quot;+2^-7&amp;quot;,3.01947e-08,43.675],[&amp;quot;+2^-6&amp;quot;,3.05713e-08,30.45],[&amp;quot;+2^-5&amp;quot;,3.14319e-08,8.43743],[&amp;quot;+2^-4&amp;quot;,3.31291e-08,7.77058],[&amp;quot;+2^-3&amp;quot;,3.58597e-08,3.99659],[&amp;quot;+2^-2&amp;quot;,4.14639e-08,2.29246],[&amp;quot;+2^-1&amp;quot;,8.02409e-08,1.34622],[&amp;quot;+2^0&amp;quot;,8.66201e-08,1.45324],[&amp;quot;+2^1&amp;quot;,8.8654e-08,1.48737],[&amp;quot;+2^2&amp;quot;,6.7976e-08,1.14045],[&amp;quot;+2^3&amp;quot;,1.33955e-07,2.2474],[&amp;quot;+2^4&amp;quot;,2.53131e-14,4.24683e-07],[&amp;quot;+2^5&amp;quot;,0,null],[&amp;quot;+2^6&amp;quot;,0,null],[&amp;quot;+2^7&amp;quot;,0,null],[&amp;quot;+2^8&amp;quot;,0,null],[&amp;quot;+2^9&amp;quot;,0,null],[&amp;quot;+2^10&amp;quot;,0,null],[&amp;quot;+2^11&amp;quot;,0,null],[&amp;quot;+2^12&amp;quot;,0,null],[&amp;quot;+2^13&amp;quot;,0,null],[&amp;quot;+2^14&amp;quot;,0,null],[&amp;quot;+2^15&amp;quot;,0,null],[&amp;quot;+2^16&amp;quot;,0,null],[&amp;quot;+2^17&amp;quot;,0,null],[&amp;quot;+2^18&amp;quot;,0,null],[&amp;quot;+2^19&amp;quot;,0,null],[&amp;quot;+2^20&amp;quot;,0,null],[&amp;quot;+2^21&amp;quot;,0,null],[&amp;quot;+2^22&amp;quot;,0,null],[&amp;quot;+2^23&amp;quot;,0,null],[&amp;quot;+2^24&amp;quot;,0,null],[&amp;quot;+2^25&amp;quot;,0,null],[&amp;quot;+2^26&amp;quot;,0,null],[&amp;quot;+2^27&amp;quot;,0,null],[&amp;quot;+2^28&amp;quot;,0,null],[&amp;quot;+2^29&amp;quot;,0,null],[&amp;quot;+2^30&amp;quot;,0,null],[&amp;quot;+2^31&amp;quot;,0,null],[&amp;quot;+2^32&amp;quot;,0,null],[&amp;quot;+2^33&amp;quot;,0,null],[&amp;quot;+2^34&amp;quot;,0,null],[&amp;quot;+2^35&amp;quot;,0,null],[&amp;quot;+2^36&amp;quot;,0,null],[&amp;quot;+2^37&amp;quot;,0,null],[&amp;quot;+2^38&amp;quot;,0,null],[&amp;quot;+2^39&amp;quot;,0,null],[&amp;quot;+2^40&amp;quot;,0,null],[&amp;quot;+2^41&amp;quot;,0,null],[&amp;quot;+2^42&amp;quot;,0,null],[&amp;quot;+2^43&amp;quot;,0,null],[&amp;quot;+2^44&amp;quot;,0,null],[&amp;quot;+2^45&amp;quot;,0,null],[&amp;quot;+2^46&amp;quot;,0,null],[&amp;quot;+2^47&amp;quot;,0,null],[&amp;quot;+2^48&amp;quot;,0,null],[&amp;quot;+2^49&amp;quot;,0,null],[&amp;quot;+2^50&amp;quot;,0,null],[&amp;quot;+2^51&amp;quot;,0,null],[&amp;quot;+2^52&amp;quot;,0,null],[&amp;quot;+2^53&amp;quot;,0,null],[&amp;quot;+2^54&amp;quot;,0,null],[&amp;quot;+2^55&amp;quot;,0,null],[&amp;quot;+2^56&amp;quot;,0,null],[&amp;quot;+2^57&amp;quot;,0,null],[&amp;quot;+2^58&amp;quot;,0,null],[&amp;quot;+2^59&amp;quot;,0,null],[&amp;quot;+2^60&amp;quot;,0,null],[&amp;quot;+2^61&amp;quot;,0,null],[&amp;quot;+2^62&amp;quot;,0,null],[&amp;quot;+2^63&amp;quot;,0,null],[&amp;quot;+2^64&amp;quot;,0,null],[&amp;quot;+2^65&amp;quot;,0,null],[&amp;quot;+2^66&amp;quot;,0,null],[&amp;quot;+2^67&amp;quot;,0,null],[&amp;quot;+2^68&amp;quot;,0,null],[&amp;quot;+2^69&amp;quot;,0,null],[&amp;quot;+2^70&amp;quot;,0,null],[&amp;quot;+2^71&amp;quot;,0,null],[&amp;quot;+2^72&amp;quot;,0,null],[&amp;quot;+2^73&amp;quot;,0,null],[&amp;quot;+2^74&amp;quot;,0,null],[&amp;quot;+2^75&amp;quot;,0,null],[&amp;quot;+2^76&amp;quot;,0,null],[&amp;quot;+2^77&amp;quot;,0,null],[&amp;quot;+2^78&amp;quot;,0,null],[&amp;quot;+2^79&amp;quot;,0,null],[&amp;quot;+2^80&amp;quot;,0,null],[&amp;quot;+2^81&amp;quot;,0,null],[&amp;quot;+2^82&amp;quot;,0,null],[&amp;quot;+2^83&amp;quot;,0,null],[&amp;quot;+2^84&amp;quot;,0,null],[&amp;quot;+2^85&amp;quot;,0,null],[&amp;quot;+2^86&amp;quot;,0,null],[&amp;quot;+2^87&amp;quot;,0,null],[&amp;quot;+2^88&amp;quot;,0,null],[&amp;quot;+2^89&amp;quot;,0,null],[&amp;quot;+2^90&amp;quot;,0,null],[&amp;quot;+2^91&amp;quot;,0,null],[&amp;quot;+2^92&amp;quot;,0,null],[&amp;quot;+2^93&amp;quot;,0,null],[&amp;quot;+2^94&amp;quot;,0,null],[&amp;quot;+2^95&amp;quot;,0,null],[&amp;quot;+2^96&amp;quot;,0,null],[&amp;quot;+2^97&amp;quot;,0,null],[&amp;quot;+2^98&amp;quot;,0,null],[&amp;quot;+2^99&amp;quot;,0,null],[&amp;quot;+2^100&amp;quot;,0,null],[&amp;quot;+2^101&amp;quot;,0,null],[&amp;quot;+2^102&amp;quot;,0,null],[&amp;quot;+2^103&amp;quot;,0,null],[&amp;quot;+2^104&amp;quot;,0,null],[&amp;quot;+2^105&amp;quot;,0,null],[&amp;quot;+2^106&amp;quot;,0,null],[&amp;quot;+2^107&amp;quot;,0,null],[&amp;quot;+2^108&amp;quot;,0,null],[&amp;quot;+2^109&amp;quot;,0,null],[&amp;quot;+2^110&amp;quot;,0,null],[&amp;quot;+2^111&amp;quot;,0,null],[&amp;quot;+2^112&amp;quot;,0,null],[&amp;quot;+2^113&amp;quot;,0,null],[&amp;quot;+2^114&amp;quot;,0,null],[&amp;quot;+2^115&amp;quot;,0,null],[&amp;quot;+2^116&amp;quot;,0,null],[&amp;quot;+2^117&amp;quot;,0,null],[&amp;quot;+2^118&amp;quot;,0,null],[&amp;quot;+2^119&amp;quot;,0,null],[&amp;quot;+2^120&amp;quot;,0,null],[&amp;quot;+2^121&amp;quot;,0,null],[&amp;quot;+2^122&amp;quot;,0,null],[&amp;quot;+2^123&amp;quot;,0,null],[&amp;quot;+2^124&amp;quot;,0,null],[&amp;quot;+2^125&amp;quot;,0,null],[&amp;quot;+2^126&amp;quot;,0,null],[&amp;quot;+2^127&amp;quot;,0,null] ]&lt;br /&gt;
 }}&lt;br /&gt;
=== expm1 ===&lt;br /&gt;
&lt;br /&gt;
{{#widget:FloatErrorGraph|id=tanhf_expm1&lt;br /&gt;
  |data=[ [&amp;quot;-2^127&amp;quot;,0,1.17549e-38],[&amp;quot;-2^126&amp;quot;,0,1.17549e-38],[&amp;quot;-2^125&amp;quot;,0,1.17549e-38],[&amp;quot;-2^124&amp;quot;,0,1.17549e-38],[&amp;quot;-2^123&amp;quot;,0,1.17549e-38],[&amp;quot;-2^122&amp;quot;,0,1.17549e-38],[&amp;quot;-2^121&amp;quot;,0,1.17549e-38],[&amp;quot;-2^120&amp;quot;,0,1.17549e-38],[&amp;quot;-2^119&amp;quot;,0,1.17549e-38],[&amp;quot;-2^118&amp;quot;,0,1.17549e-38],[&amp;quot;-2^117&amp;quot;,0,1.17549e-38],[&amp;quot;-2^116&amp;quot;,0,1.17549e-38],[&amp;quot;-2^115&amp;quot;,0,1.17549e-38],[&amp;quot;-2^114&amp;quot;,0,1.17549e-38],[&amp;quot;-2^113&amp;quot;,0,1.17549e-38],[&amp;quot;-2^112&amp;quot;,0,1.17549e-38],[&amp;quot;-2^111&amp;quot;,0,1.17549e-38],[&amp;quot;-2^110&amp;quot;,0,1.17549e-38],[&amp;quot;-2^109&amp;quot;,0,1.17549e-38],[&amp;quot;-2^108&amp;quot;,0,1.17549e-38],[&amp;quot;-2^107&amp;quot;,0,1.17549e-38],[&amp;quot;-2^106&amp;quot;,0,1.17549e-38],[&amp;quot;-2^105&amp;quot;,0,1.17549e-38],[&amp;quot;-2^104&amp;quot;,0,1.17549e-38],[&amp;quot;-2^103&amp;quot;,0,1.17549e-38],[&amp;quot;-2^102&amp;quot;,0,1.17549e-38],[&amp;quot;-2^101&amp;quot;,0,1.17549e-38],[&amp;quot;-2^100&amp;quot;,0,1.17549e-38],[&amp;quot;-2^99&amp;quot;,0,1.17549e-38],[&amp;quot;-2^98&amp;quot;,0,1.17549e-38],[&amp;quot;-2^97&amp;quot;,0,1.17549e-38],[&amp;quot;-2^96&amp;quot;,0,1.17549e-38],[&amp;quot;-2^95&amp;quot;,0,1.17549e-38],[&amp;quot;-2^94&amp;quot;,0,1.17549e-38],[&amp;quot;-2^93&amp;quot;,0,1.17549e-38],[&amp;quot;-2^92&amp;quot;,0,1.17549e-38],[&amp;quot;-2^91&amp;quot;,0,1.17549e-38],[&amp;quot;-2^90&amp;quot;,0,1.17549e-38],[&amp;quot;-2^89&amp;quot;,0,1.17549e-38],[&amp;quot;-2^88&amp;quot;,0,1.17549e-38],[&amp;quot;-2^87&amp;quot;,0,1.17549e-38],[&amp;quot;-2^86&amp;quot;,0,1.17549e-38],[&amp;quot;-2^85&amp;quot;,0,1.17549e-38],[&amp;quot;-2^84&amp;quot;,0,1.17549e-38],[&amp;quot;-2^83&amp;quot;,0,1.17549e-38],[&amp;quot;-2^82&amp;quot;,0,1.17549e-38],[&amp;quot;-2^81&amp;quot;,0,1.17549e-38],[&amp;quot;-2^80&amp;quot;,0,1.17549e-38],[&amp;quot;-2^79&amp;quot;,0,1.17549e-38],[&amp;quot;-2^78&amp;quot;,0,1.17549e-38],[&amp;quot;-2^77&amp;quot;,0,1.17549e-38],[&amp;quot;-2^76&amp;quot;,0,1.17549e-38],[&amp;quot;-2^75&amp;quot;,0,1.17549e-38],[&amp;quot;-2^74&amp;quot;,0,1.17549e-38],[&amp;quot;-2^73&amp;quot;,0,1.17549e-38],[&amp;quot;-2^72&amp;quot;,0,1.17549e-38],[&amp;quot;-2^71&amp;quot;,0,1.17549e-38],[&amp;quot;-2^70&amp;quot;,0,1.17549e-38],[&amp;quot;-2^69&amp;quot;,0,1.17549e-38],[&amp;quot;-2^68&amp;quot;,0,1.17549e-38],[&amp;quot;-2^67&amp;quot;,0,1.17549e-38],[&amp;quot;-2^66&amp;quot;,0,1.17549e-38],[&amp;quot;-2^65&amp;quot;,0,1.17549e-38],[&amp;quot;-2^64&amp;quot;,0,1.17549e-38],[&amp;quot;-2^63&amp;quot;,0,1.17549e-38],[&amp;quot;-2^62&amp;quot;,0,1.17549e-38],[&amp;quot;-2^61&amp;quot;,0,1.17549e-38],[&amp;quot;-2^60&amp;quot;,0,1.17549e-38],[&amp;quot;-2^59&amp;quot;,0,1.17549e-38],[&amp;quot;-2^58&amp;quot;,0,1.17549e-38],[&amp;quot;-2^57&amp;quot;,0,1.17549e-38],[&amp;quot;-2^56&amp;quot;,0,1.17549e-38],[&amp;quot;-2^55&amp;quot;,0,1.17549e-38],[&amp;quot;-2^54&amp;quot;,0,1.17549e-38],[&amp;quot;-2^53&amp;quot;,0,1.17549e-38],[&amp;quot;-2^52&amp;quot;,0,1.17549e-38],[&amp;quot;-2^51&amp;quot;,0,1.17549e-38],[&amp;quot;-2^50&amp;quot;,0,1.17549e-38],[&amp;quot;-2^49&amp;quot;,0,1.17549e-38],[&amp;quot;-2^48&amp;quot;,0,1.17549e-38],[&amp;quot;-2^47&amp;quot;,0,1.17549e-38],[&amp;quot;-2^46&amp;quot;,0,1.17549e-38],[&amp;quot;-2^45&amp;quot;,0,1.17549e-38],[&amp;quot;-2^44&amp;quot;,0,1.17549e-38],[&amp;quot;-2^43&amp;quot;,0,1.17549e-38],[&amp;quot;-2^42&amp;quot;,0,1.17549e-38],[&amp;quot;-2^41&amp;quot;,0,1.17549e-38],[&amp;quot;-2^40&amp;quot;,0,1.17549e-38],[&amp;quot;-2^39&amp;quot;,0,1.17549e-38],[&amp;quot;-2^38&amp;quot;,0,1.17549e-38],[&amp;quot;-2^37&amp;quot;,0,1.17549e-38],[&amp;quot;-2^36&amp;quot;,0,1.17549e-38],[&amp;quot;-2^35&amp;quot;,0,1.17549e-38],[&amp;quot;-2^34&amp;quot;,0,1.17549e-38],[&amp;quot;-2^33&amp;quot;,0,1.17549e-38],[&amp;quot;-2^32&amp;quot;,0,1.17549e-38],[&amp;quot;-2^31&amp;quot;,0,1.17549e-38],[&amp;quot;-2^30&amp;quot;,0,1.17549e-38],[&amp;quot;-2^29&amp;quot;,0,1.17549e-38],[&amp;quot;-2^28&amp;quot;,0,1.17549e-38],[&amp;quot;-2^27&amp;quot;,0,1.17549e-38],[&amp;quot;-2^26&amp;quot;,0,1.17549e-38],[&amp;quot;-2^25&amp;quot;,0,1.17549e-38],[&amp;quot;-2^24&amp;quot;,0,1.17549e-38],[&amp;quot;-2^23&amp;quot;,0,1.17549e-38],[&amp;quot;-2^22&amp;quot;,0,1.17549e-38],[&amp;quot;-2^21&amp;quot;,0,1.17549e-38],[&amp;quot;-2^20&amp;quot;,0,1.17549e-38],[&amp;quot;-2^19&amp;quot;,0,1.17549e-38],[&amp;quot;-2^18&amp;quot;,0,1.17549e-38],[&amp;quot;-2^17&amp;quot;,0,1.17549e-38],[&amp;quot;-2^16&amp;quot;,0,1.17549e-38],[&amp;quot;-2^15&amp;quot;,0,1.17549e-38],[&amp;quot;-2^14&amp;quot;,0,1.17549e-38],[&amp;quot;-2^13&amp;quot;,0,1.17549e-38],[&amp;quot;-2^12&amp;quot;,0,1.17549e-38],[&amp;quot;-2^11&amp;quot;,0,1.17549e-38],[&amp;quot;-2^10&amp;quot;,0,1.17549e-38],[&amp;quot;-2^9&amp;quot;,0,1.17549e-38],[&amp;quot;-2^8&amp;quot;,0,1.17549e-38],[&amp;quot;-2^7&amp;quot;,0,1.17549e-38],[&amp;quot;-2^6&amp;quot;,0,1.17549e-38],[&amp;quot;-2^5&amp;quot;,0,1.17549e-38],[&amp;quot;-2^4&amp;quot;,2.53131e-14,4.24683e-07],[&amp;quot;-2^3&amp;quot;,1.19138e-07,1.9988],[&amp;quot;-2^2&amp;quot;,1.46791e-07,2.46274],[&amp;quot;-2^1&amp;quot;,1.47723e-07,2.47839],[&amp;quot;-2^0&amp;quot;,1.40758e-07,2.36153],[&amp;quot;-2^-1&amp;quot;,1.13422e-07,2.23445],[&amp;quot;-2^-2&amp;quot;,6.38012e-08,2.14081],[&amp;quot;-2^-3&amp;quot;,2.69538e-08,1.80884],[&amp;quot;-2^-4&amp;quot;,1.21308e-08,1.62817],[&amp;quot;-2^-5&amp;quot;,5.59535e-09,1.50199],[&amp;quot;-2^-6&amp;quot;,2.69177e-09,1.44513],[&amp;quot;-2^-7&amp;quot;,1.31289e-09,1.4097],[&amp;quot;-2^-8&amp;quot;,6.49688e-10,1.3952],[&amp;quot;-2^-9&amp;quot;,3.32043e-10,1.42611],[&amp;quot;-2^-10&amp;quot;,1.66275e-10,1.4283],[&amp;quot;-2^-11&amp;quot;,8.08595e-11,1.38916],[&amp;quot;-2^-12&amp;quot;,3.8939e-11,1.33794],[&amp;quot;-2^-13&amp;quot;,1.9401e-11,1.33323],[&amp;quot;-2^-14&amp;quot;,7.88212e-12,1.08331],[&amp;quot;-2^-15&amp;quot;,3.71372e-12,1.02082],[&amp;quot;-2^-16&amp;quot;,1.82845e-12,1.0052],[&amp;quot;-2^-17&amp;quot;,9.10676e-13,1.0013],[&amp;quot;-2^-18&amp;quot;,4.54895e-13,1.00032],[&amp;quot;-2^-19&amp;quot;,2.27392e-13,1.00008],[&amp;quot;-2^-20&amp;quot;,1.13689e-13,1.00002],[&amp;quot;-2^-21&amp;quot;,5.68437e-14,1],[&amp;quot;-2^-22&amp;quot;,2.84217e-14,1],[&amp;quot;-2^-23&amp;quot;,1.42109e-14,1],[&amp;quot;-2^-24&amp;quot;,7.10543e-15,1],[&amp;quot;-2^-25&amp;quot;,3.55271e-15,1],[&amp;quot;-2^-26&amp;quot;,9.92617e-24,5.58794e-09],[&amp;quot;-2^-27&amp;quot;,1.65436e-24,1.86265e-09],[&amp;quot;-2^-28&amp;quot;,8.27181e-25,1.86265e-09],[&amp;quot;-2^-29&amp;quot;,4.1359e-25,1.86265e-09],[&amp;quot;-2^-30&amp;quot;,2.06795e-25,1.86265e-09],[&amp;quot;-2^-31&amp;quot;,1.03398e-25,1.86265e-09],[&amp;quot;-2^-32&amp;quot;,5.16988e-26,1.86265e-09],[&amp;quot;-2^-33&amp;quot;,2.58494e-26,1.86265e-09],[&amp;quot;-2^-34&amp;quot;,1.29247e-26,1.86265e-09],[&amp;quot;-2^-35&amp;quot;,6.46235e-27,1.86265e-09],[&amp;quot;-2^-36&amp;quot;,3.23117e-27,1.86265e-09],[&amp;quot;-2^-37&amp;quot;,1.61559e-27,1.86265e-09],[&amp;quot;-2^-38&amp;quot;,8.07794e-28,1.86265e-09],[&amp;quot;-2^-39&amp;quot;,4.03897e-28,1.86265e-09],[&amp;quot;-2^-40&amp;quot;,2.01948e-28,1.86265e-09],[&amp;quot;-2^-41&amp;quot;,1.00974e-28,1.86265e-09],[&amp;quot;-2^-42&amp;quot;,5.04871e-29,1.86265e-09],[&amp;quot;-2^-43&amp;quot;,2.52435e-29,1.86265e-09],[&amp;quot;-2^-44&amp;quot;,1.26218e-29,1.86265e-09],[&amp;quot;-2^-45&amp;quot;,6.31089e-30,1.86265e-09],[&amp;quot;-2^-46&amp;quot;,3.15544e-30,1.86265e-09],[&amp;quot;-2^-47&amp;quot;,1.57772e-30,1.86265e-09],[&amp;quot;-2^-48&amp;quot;,7.88861e-31,1.86265e-09],[&amp;quot;-2^-49&amp;quot;,3.9443e-31,1.86265e-09],[&amp;quot;-2^-50&amp;quot;,1.97215e-31,1.86265e-09],[&amp;quot;-2^-51&amp;quot;,9.86076e-32,1.86265e-09],[&amp;quot;-2^-52&amp;quot;,4.93038e-32,1.86265e-09],[&amp;quot;-2^-53&amp;quot;,2.46519e-32,1.86265e-09],[&amp;quot;-2^-54&amp;quot;,1.2326e-32,1.86265e-09],[&amp;quot;-2^-55&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-56&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-57&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-58&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-59&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-60&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-61&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-62&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-63&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-64&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-65&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-66&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-67&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-68&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-69&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-70&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-71&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-72&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-73&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-74&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-75&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-76&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-77&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-78&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-79&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-80&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-81&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-82&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-83&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-84&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-85&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-86&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-87&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-88&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-89&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-90&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-91&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-92&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-93&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-94&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-95&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-96&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-97&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-98&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-99&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-100&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-101&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-102&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-103&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-104&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-105&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-106&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-107&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-108&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-109&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-110&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-111&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-112&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-113&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-114&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-115&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-116&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-117&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-118&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-119&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-120&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-121&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-122&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-123&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-124&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-125&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-126&amp;quot;,0,1.17549e-38],[&amp;quot;-SN&amp;quot;,0,1.17549e-38],[&amp;quot;0&amp;quot;,0,1.17549e-38],[&amp;quot;+SN&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-126&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-125&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-124&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-123&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-122&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-121&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-120&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-119&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-118&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-117&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-116&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-115&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-114&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-113&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-112&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-111&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-110&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-109&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-108&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-107&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-106&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-105&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-104&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-103&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-102&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-101&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-100&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-99&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-98&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-97&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-96&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-95&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-94&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-93&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-92&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-91&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-90&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-89&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-88&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-87&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-86&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-85&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-84&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-83&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-82&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-81&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-80&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-79&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-78&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-77&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-76&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-75&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-74&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-73&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-72&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-71&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-70&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-69&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-68&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-67&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-66&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-65&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-64&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-63&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-62&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-61&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-60&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-59&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-58&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-57&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-56&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-55&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-54&amp;quot;,1.2326e-32,1.86265e-09],[&amp;quot;+2^-53&amp;quot;,2.46519e-32,1.86265e-09],[&amp;quot;+2^-52&amp;quot;,4.93038e-32,1.86265e-09],[&amp;quot;+2^-51&amp;quot;,9.86076e-32,1.86265e-09],[&amp;quot;+2^-50&amp;quot;,1.97215e-31,1.86265e-09],[&amp;quot;+2^-49&amp;quot;,3.9443e-31,1.86265e-09],[&amp;quot;+2^-48&amp;quot;,7.88861e-31,1.86265e-09],[&amp;quot;+2^-47&amp;quot;,1.57772e-30,1.86265e-09],[&amp;quot;+2^-46&amp;quot;,3.15544e-30,1.86265e-09],[&amp;quot;+2^-45&amp;quot;,6.31089e-30,1.86265e-09],[&amp;quot;+2^-44&amp;quot;,1.26218e-29,1.86265e-09],[&amp;quot;+2^-43&amp;quot;,2.52435e-29,1.86265e-09],[&amp;quot;+2^-42&amp;quot;,5.04871e-29,1.86265e-09],[&amp;quot;+2^-41&amp;quot;,1.00974e-28,1.86265e-09],[&amp;quot;+2^-40&amp;quot;,2.01948e-28,1.86265e-09],[&amp;quot;+2^-39&amp;quot;,4.03897e-28,1.86265e-09],[&amp;quot;+2^-38&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-37&amp;quot;,1.61559e-27,1.86265e-09],[&amp;quot;+2^-36&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-35&amp;quot;,6.46235e-27,1.86265e-09],[&amp;quot;+2^-34&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-33&amp;quot;,2.58494e-26,1.86265e-09],[&amp;quot;+2^-32&amp;quot;,5.16988e-26,1.86265e-09],[&amp;quot;+2^-31&amp;quot;,1.03398e-25,1.86265e-09],[&amp;quot;+2^-30&amp;quot;,2.06795e-25,1.86265e-09],[&amp;quot;+2^-29&amp;quot;,4.1359e-25,1.86265e-09],[&amp;quot;+2^-28&amp;quot;,8.27181e-25,1.86265e-09],[&amp;quot;+2^-27&amp;quot;,1.65436e-24,1.86265e-09],[&amp;quot;+2^-26&amp;quot;,9.92617e-24,5.58794e-09],[&amp;quot;+2^-25&amp;quot;,3.55271e-15,1],[&amp;quot;+2^-24&amp;quot;,7.10543e-15,1],[&amp;quot;+2^-23&amp;quot;,1.42109e-14,1],[&amp;quot;+2^-22&amp;quot;,2.84217e-14,1],[&amp;quot;+2^-21&amp;quot;,5.68437e-14,1],[&amp;quot;+2^-20&amp;quot;,1.13689e-13,1.00002],[&amp;quot;+2^-19&amp;quot;,2.27391e-13,1.00008],[&amp;quot;+2^-18&amp;quot;,4.54892e-13,1.00032],[&amp;quot;+2^-17&amp;quot;,9.10665e-13,1.00129],[&amp;quot;+2^-16&amp;quot;,1.82841e-12,1.00518],[&amp;quot;+2^-15&amp;quot;,3.71353e-12,1.02077],[&amp;quot;+2^-14&amp;quot;,1.39798e-11,1.92136],[&amp;quot;+2^-13&amp;quot;,2.63239e-11,1.80896],[&amp;quot;+2^-12&amp;quot;,5.37712e-11,1.84757],[&amp;quot;+2^-11&amp;quot;,1.09932e-10,1.88861],[&amp;quot;+2^-10&amp;quot;,2.24787e-10,1.93091],[&amp;quot;+2^-9&amp;quot;,4.3452e-10,1.86625],[&amp;quot;+2^-8&amp;quot;,1.01783e-09,2.18576],[&amp;quot;+2^-7&amp;quot;,2.10562e-09,2.26089],[&amp;quot;+2^-6&amp;quot;,4.03954e-09,2.16871],[&amp;quot;+2^-5&amp;quot;,8.15746e-09,2.18975],[&amp;quot;+2^-4&amp;quot;,1.5545e-08,2.08641],[&amp;quot;+2^-3&amp;quot;,2.62153e-08,2.05864],[&amp;quot;+2^-2&amp;quot;,4.09295e-08,1.73804],[&amp;quot;+2^-1&amp;quot;,8.02409e-08,1.34622],[&amp;quot;+2^0&amp;quot;,8.66201e-08,1.45324],[&amp;quot;+2^1&amp;quot;,8.8654e-08,1.48737],[&amp;quot;+2^2&amp;quot;,6.7976e-08,1.14045],[&amp;quot;+2^3&amp;quot;,7.44117e-08,1.24842],[&amp;quot;+2^4&amp;quot;,2.53131e-14,4.24683e-07],[&amp;quot;+2^5&amp;quot;,0,null],[&amp;quot;+2^6&amp;quot;,0,null],[&amp;quot;+2^7&amp;quot;,0,null],[&amp;quot;+2^8&amp;quot;,0,null],[&amp;quot;+2^9&amp;quot;,0,null],[&amp;quot;+2^10&amp;quot;,0,null],[&amp;quot;+2^11&amp;quot;,0,null],[&amp;quot;+2^12&amp;quot;,0,null],[&amp;quot;+2^13&amp;quot;,0,null],[&amp;quot;+2^14&amp;quot;,0,null],[&amp;quot;+2^15&amp;quot;,0,null],[&amp;quot;+2^16&amp;quot;,0,null],[&amp;quot;+2^17&amp;quot;,0,null],[&amp;quot;+2^18&amp;quot;,0,null],[&amp;quot;+2^19&amp;quot;,0,null],[&amp;quot;+2^20&amp;quot;,0,null],[&amp;quot;+2^21&amp;quot;,0,null],[&amp;quot;+2^22&amp;quot;,0,null],[&amp;quot;+2^23&amp;quot;,0,null],[&amp;quot;+2^24&amp;quot;,0,null],[&amp;quot;+2^25&amp;quot;,0,null],[&amp;quot;+2^26&amp;quot;,0,null],[&amp;quot;+2^27&amp;quot;,0,null],[&amp;quot;+2^28&amp;quot;,0,null],[&amp;quot;+2^29&amp;quot;,0,null],[&amp;quot;+2^30&amp;quot;,0,null],[&amp;quot;+2^31&amp;quot;,0,null],[&amp;quot;+2^32&amp;quot;,0,null],[&amp;quot;+2^33&amp;quot;,0,null],[&amp;quot;+2^34&amp;quot;,0,null],[&amp;quot;+2^35&amp;quot;,0,null],[&amp;quot;+2^36&amp;quot;,0,null],[&amp;quot;+2^37&amp;quot;,0,null],[&amp;quot;+2^38&amp;quot;,0,null],[&amp;quot;+2^39&amp;quot;,0,null],[&amp;quot;+2^40&amp;quot;,0,null],[&amp;quot;+2^41&amp;quot;,0,null],[&amp;quot;+2^42&amp;quot;,0,null],[&amp;quot;+2^43&amp;quot;,0,null],[&amp;quot;+2^44&amp;quot;,0,null],[&amp;quot;+2^45&amp;quot;,0,null],[&amp;quot;+2^46&amp;quot;,0,null],[&amp;quot;+2^47&amp;quot;,0,null],[&amp;quot;+2^48&amp;quot;,0,null],[&amp;quot;+2^49&amp;quot;,0,null],[&amp;quot;+2^50&amp;quot;,0,null],[&amp;quot;+2^51&amp;quot;,0,null],[&amp;quot;+2^52&amp;quot;,0,null],[&amp;quot;+2^53&amp;quot;,0,null],[&amp;quot;+2^54&amp;quot;,0,null],[&amp;quot;+2^55&amp;quot;,0,null],[&amp;quot;+2^56&amp;quot;,0,null],[&amp;quot;+2^57&amp;quot;,0,null],[&amp;quot;+2^58&amp;quot;,0,null],[&amp;quot;+2^59&amp;quot;,0,null],[&amp;quot;+2^60&amp;quot;,0,null],[&amp;quot;+2^61&amp;quot;,0,null],[&amp;quot;+2^62&amp;quot;,0,null],[&amp;quot;+2^63&amp;quot;,0,null],[&amp;quot;+2^64&amp;quot;,0,null],[&amp;quot;+2^65&amp;quot;,0,null],[&amp;quot;+2^66&amp;quot;,0,null],[&amp;quot;+2^67&amp;quot;,0,null],[&amp;quot;+2^68&amp;quot;,0,null],[&amp;quot;+2^69&amp;quot;,0,null],[&amp;quot;+2^70&amp;quot;,0,null],[&amp;quot;+2^71&amp;quot;,0,null],[&amp;quot;+2^72&amp;quot;,0,null],[&amp;quot;+2^73&amp;quot;,0,null],[&amp;quot;+2^74&amp;quot;,0,null],[&amp;quot;+2^75&amp;quot;,0,null],[&amp;quot;+2^76&amp;quot;,0,null],[&amp;quot;+2^77&amp;quot;,0,null],[&amp;quot;+2^78&amp;quot;,0,null],[&amp;quot;+2^79&amp;quot;,0,null],[&amp;quot;+2^80&amp;quot;,0,null],[&amp;quot;+2^81&amp;quot;,0,null],[&amp;quot;+2^82&amp;quot;,0,null],[&amp;quot;+2^83&amp;quot;,0,null],[&amp;quot;+2^84&amp;quot;,0,null],[&amp;quot;+2^85&amp;quot;,0,null],[&amp;quot;+2^86&amp;quot;,0,null],[&amp;quot;+2^87&amp;quot;,0,null],[&amp;quot;+2^88&amp;quot;,0,null],[&amp;quot;+2^89&amp;quot;,0,null],[&amp;quot;+2^90&amp;quot;,0,null],[&amp;quot;+2^91&amp;quot;,0,null],[&amp;quot;+2^92&amp;quot;,0,null],[&amp;quot;+2^93&amp;quot;,0,null],[&amp;quot;+2^94&amp;quot;,0,null],[&amp;quot;+2^95&amp;quot;,0,null],[&amp;quot;+2^96&amp;quot;,0,null],[&amp;quot;+2^97&amp;quot;,0,null],[&amp;quot;+2^98&amp;quot;,0,null],[&amp;quot;+2^99&amp;quot;,0,null],[&amp;quot;+2^100&amp;quot;,0,null],[&amp;quot;+2^101&amp;quot;,0,null],[&amp;quot;+2^102&amp;quot;,0,null],[&amp;quot;+2^103&amp;quot;,0,null],[&amp;quot;+2^104&amp;quot;,0,null],[&amp;quot;+2^105&amp;quot;,0,null],[&amp;quot;+2^106&amp;quot;,0,null],[&amp;quot;+2^107&amp;quot;,0,null],[&amp;quot;+2^108&amp;quot;,0,null],[&amp;quot;+2^109&amp;quot;,0,null],[&amp;quot;+2^110&amp;quot;,0,null],[&amp;quot;+2^111&amp;quot;,0,null],[&amp;quot;+2^112&amp;quot;,0,null],[&amp;quot;+2^113&amp;quot;,0,null],[&amp;quot;+2^114&amp;quot;,0,null],[&amp;quot;+2^115&amp;quot;,0,null],[&amp;quot;+2^116&amp;quot;,0,null],[&amp;quot;+2^117&amp;quot;,0,null],[&amp;quot;+2^118&amp;quot;,0,null],[&amp;quot;+2^119&amp;quot;,0,null],[&amp;quot;+2^120&amp;quot;,0,null],[&amp;quot;+2^121&amp;quot;,0,null],[&amp;quot;+2^122&amp;quot;,0,null],[&amp;quot;+2^123&amp;quot;,0,null],[&amp;quot;+2^124&amp;quot;,0,null],[&amp;quot;+2^125&amp;quot;,0,null],[&amp;quot;+2^126&amp;quot;,0,null],[&amp;quot;+2^127&amp;quot;,0,null] ]&lt;br /&gt;
 }}&lt;br /&gt;
&lt;br /&gt;
==Numerical Issues and Error Metrics==&lt;br /&gt;
&lt;br /&gt;
==Taylor==&lt;br /&gt;
&lt;br /&gt;
{{#widget:FloatErrorGraph|id=taylor&lt;br /&gt;
  |data=[ [&amp;quot;-2^127&amp;quot;,0,0],[&amp;quot;-2^126&amp;quot;,0,0],[&amp;quot;-2^125&amp;quot;,0,0],[&amp;quot;-2^124&amp;quot;,0,0],[&amp;quot;-2^123&amp;quot;,0,0],[&amp;quot;-2^122&amp;quot;,0,0],[&amp;quot;-2^121&amp;quot;,0,0],[&amp;quot;-2^120&amp;quot;,0,0],[&amp;quot;-2^119&amp;quot;,0,0],[&amp;quot;-2^118&amp;quot;,0,0],[&amp;quot;-2^117&amp;quot;,0,0],[&amp;quot;-2^116&amp;quot;,0,0],[&amp;quot;-2^115&amp;quot;,0,0],[&amp;quot;-2^114&amp;quot;,0,0],[&amp;quot;-2^113&amp;quot;,0,0],[&amp;quot;-2^112&amp;quot;,0,0],[&amp;quot;-2^111&amp;quot;,0,0],[&amp;quot;-2^110&amp;quot;,0,0],[&amp;quot;-2^109&amp;quot;,0,0],[&amp;quot;-2^108&amp;quot;,0,0],[&amp;quot;-2^107&amp;quot;,0,0],[&amp;quot;-2^106&amp;quot;,0,0],[&amp;quot;-2^105&amp;quot;,0,0],[&amp;quot;-2^104&amp;quot;,0,0],[&amp;quot;-2^103&amp;quot;,0,0],[&amp;quot;-2^102&amp;quot;,0,0],[&amp;quot;-2^101&amp;quot;,0,0],[&amp;quot;-2^100&amp;quot;,0,0],[&amp;quot;-2^99&amp;quot;,0,0],[&amp;quot;-2^98&amp;quot;,0,0],[&amp;quot;-2^97&amp;quot;,0,0],[&amp;quot;-2^96&amp;quot;,0,0],[&amp;quot;-2^95&amp;quot;,0,0],[&amp;quot;-2^94&amp;quot;,0,0],[&amp;quot;-2^93&amp;quot;,0,0],[&amp;quot;-2^92&amp;quot;,0,0],[&amp;quot;-2^91&amp;quot;,0,0],[&amp;quot;-2^90&amp;quot;,0,0],[&amp;quot;-2^89&amp;quot;,0,0],[&amp;quot;-2^88&amp;quot;,0,0],[&amp;quot;-2^87&amp;quot;,0,0],[&amp;quot;-2^86&amp;quot;,0,0],[&amp;quot;-2^85&amp;quot;,0,0],[&amp;quot;-2^84&amp;quot;,0,0],[&amp;quot;-2^83&amp;quot;,0,0],[&amp;quot;-2^82&amp;quot;,0,0],[&amp;quot;-2^81&amp;quot;,0,0],[&amp;quot;-2^80&amp;quot;,0,0],[&amp;quot;-2^79&amp;quot;,0,0],[&amp;quot;-2^78&amp;quot;,0,0],[&amp;quot;-2^77&amp;quot;,0,0],[&amp;quot;-2^76&amp;quot;,0,0],[&amp;quot;-2^75&amp;quot;,0,0],[&amp;quot;-2^74&amp;quot;,0,0],[&amp;quot;-2^73&amp;quot;,0,0],[&amp;quot;-2^72&amp;quot;,0,0],[&amp;quot;-2^71&amp;quot;,0,0],[&amp;quot;-2^70&amp;quot;,0,0],[&amp;quot;-2^69&amp;quot;,0,0],[&amp;quot;-2^68&amp;quot;,0,0],[&amp;quot;-2^67&amp;quot;,0,0],[&amp;quot;-2^66&amp;quot;,0,0],[&amp;quot;-2^65&amp;quot;,0,0],[&amp;quot;-2^64&amp;quot;,0,0],[&amp;quot;-2^63&amp;quot;,1.84467e+19,3.09485e+26],[&amp;quot;-2^62&amp;quot;,9.22336e+18,1.54742e+26],[&amp;quot;-2^61&amp;quot;,4.61168e+18,7.73712e+25],[&amp;quot;-2^60&amp;quot;,2.30584e+18,3.86856e+25],[&amp;quot;-2^59&amp;quot;,1.15292e+18,1.93428e+25],[&amp;quot;-2^58&amp;quot;,5.7646e+17,9.6714e+24],[&amp;quot;-2^57&amp;quot;,2.8823e+17,4.8357e+24],[&amp;quot;-2^56&amp;quot;,1.44115e+17,2.41785e+24],[&amp;quot;-2^55&amp;quot;,7.20575e+16,1.20892e+24],[&amp;quot;-2^54&amp;quot;,3.60288e+16,6.04462e+23],[&amp;quot;-2^53&amp;quot;,1.80144e+16,3.02231e+23],[&amp;quot;-2^52&amp;quot;,9.00719e+15,1.51116e+23],[&amp;quot;-2^51&amp;quot;,4.5036e+15,7.55578e+22],[&amp;quot;-2^50&amp;quot;,2.2518e+15,3.77789e+22],[&amp;quot;-2^49&amp;quot;,1.1259e+15,1.88894e+22],[&amp;quot;-2^48&amp;quot;,5.62949e+14,9.44472e+21],[&amp;quot;-2^47&amp;quot;,2.81475e+14,4.72236e+21],[&amp;quot;-2^46&amp;quot;,1.40737e+14,2.36118e+21],[&amp;quot;-2^45&amp;quot;,7.03687e+13,1.18059e+21],[&amp;quot;-2^44&amp;quot;,3.51843e+13,5.90295e+20],[&amp;quot;-2^43&amp;quot;,1.75922e+13,2.95148e+20],[&amp;quot;-2^42&amp;quot;,8.79608e+12,1.47574e+20],[&amp;quot;-2^41&amp;quot;,4.39804e+12,7.37869e+19],[&amp;quot;-2^40&amp;quot;,2.19902e+12,3.68935e+19],[&amp;quot;-2^39&amp;quot;,1.09951e+12,1.84467e+19],[&amp;quot;-2^38&amp;quot;,5.49755e+11,9.22336e+18],[&amp;quot;-2^37&amp;quot;,2.74878e+11,4.61168e+18],[&amp;quot;-2^36&amp;quot;,1.37439e+11,2.30584e+18],[&amp;quot;-2^35&amp;quot;,6.87194e+10,1.15292e+18],[&amp;quot;-2^34&amp;quot;,3.43597e+10,5.7646e+17],[&amp;quot;-2^33&amp;quot;,1.71799e+10,2.8823e+17],[&amp;quot;-2^32&amp;quot;,8.58993e+09,1.44115e+17],[&amp;quot;-2^31&amp;quot;,4.29085e+09,7.19885e+16],[&amp;quot;-2^30&amp;quot;,2.14542e+09,3.59943e+16],[&amp;quot;-2^29&amp;quot;,1.07271e+09,1.79971e+16],[&amp;quot;-2^28&amp;quot;,5.36356e+08,8.99856e+15],[&amp;quot;-2^27&amp;quot;,2.68178e+08,4.49928e+15],[&amp;quot;-2^26&amp;quot;,1.34089e+08,2.24964e+15],[&amp;quot;-2^25&amp;quot;,6.70445e+07,1.12482e+15],[&amp;quot;-2^24&amp;quot;,3.35223e+07,5.6241e+14],[&amp;quot;-2^23&amp;quot;,1.67611e+07,2.81205e+14],[&amp;quot;-2^22&amp;quot;,8.38056e+06,1.40603e+14],[&amp;quot;-2^21&amp;quot;,4.19028e+06,7.03013e+13],[&amp;quot;-2^20&amp;quot;,2.09514e+06,3.51506e+13],[&amp;quot;-2^19&amp;quot;,1.04757e+06,1.75753e+13],[&amp;quot;-2^18&amp;quot;,523784,8.78764e+12],[&amp;quot;-2^17&amp;quot;,261892,4.39381e+12],[&amp;quot;-2^16&amp;quot;,130945,2.1969e+12],[&amp;quot;-2^15&amp;quot;,65472.2,1.09844e+12],[&amp;quot;-2^14&amp;quot;,32735.6,5.49212e+11],[&amp;quot;-2^13&amp;quot;,16367.3,2.74598e+11],[&amp;quot;-2^12&amp;quot;,8183.15,1.3729e+11],[&amp;quot;-2^11&amp;quot;,4091.07,6.86368e+10],[&amp;quot;-2^10&amp;quot;,2045.04,3.431e+10],[&amp;quot;-2^9&amp;quot;,1022.02,1.71466e+10],[&amp;quot;-2^8&amp;quot;,510.509,8.56492e+09],[&amp;quot;-2^7&amp;quot;,254.754,4.27406e+09],[&amp;quot;-2^6&amp;quot;,126.876,2.12862e+09],[&amp;quot;-2^5&amp;quot;,62.9354,1.05588e+09],[&amp;quot;-2^4&amp;quot;,30.9629,5.19471e+08],[&amp;quot;-2^3&amp;quot;,14.9718,2.51185e+08],[&amp;quot;-2^2&amp;quot;,6.96672,1.16882e+08],[&amp;quot;-2^1&amp;quot;,2.94633,4.94312e+07],[&amp;quot;-2^0&amp;quot;,0.940715,1.57826e+07],[&amp;quot;-2^-1&amp;quot;,0.127635,2.14135e+06],[&amp;quot;-2^-2&amp;quot;,0.00318735,106950],[&amp;quot;-2^-3&amp;quot;,3.11475e-05,2090.28],[&amp;quot;-2^-4&amp;quot;,2.57737e-07,34.5929],[&amp;quot;-2^-5&amp;quot;,3.89213e-09,1.04478],[&amp;quot;-2^-6&amp;quot;,9.51472e-10,0.510818],[&amp;quot;-2^-7&amp;quot;,4.66402e-10,0.500795],[&amp;quot;-2^-8&amp;quot;,2.32917e-10,0.500185],[&amp;quot;-2^-9&amp;quot;,1.16423e-10,0.500033],[&amp;quot;-2^-10&amp;quot;,5.82082e-11,0.500004],[&amp;quot;-2^-11&amp;quot;,2.91037e-11,0.499998],[&amp;quot;-2^-12&amp;quot;,1.45519e-11,0.5],[&amp;quot;-2^-13&amp;quot;,4.85062e-12,0.333332],[&amp;quot;-2^-14&amp;quot;,6.06328e-13,0.0833331],[&amp;quot;-2^-15&amp;quot;,7.5791e-14,0.0208333],[&amp;quot;-2^-16&amp;quot;,9.47387e-15,0.00520832],[&amp;quot;-2^-17&amp;quot;,1.18424e-15,0.00130208],[&amp;quot;-2^-18&amp;quot;,1.48029e-16,0.00032552],[&amp;quot;-2^-19&amp;quot;,1.85039e-17,8.13808e-05],[&amp;quot;-2^-20&amp;quot;,2.31304e-18,2.03457e-05],[&amp;quot;-2^-21&amp;quot;,2.89156e-19,5.08688e-06],[&amp;quot;-2^-22&amp;quot;,3.61577e-20,1.27219e-06],[&amp;quot;-2^-23&amp;quot;,4.52633e-21,3.18512e-07],[&amp;quot;-2^-24&amp;quot;,5.691e-22,8.00937e-08],[&amp;quot;-2^-25&amp;quot;,7.27919e-23,2.04891e-08],[&amp;quot;-2^-26&amp;quot;,9.92617e-24,5.58794e-09],[&amp;quot;-2^-27&amp;quot;,1.65436e-24,1.86265e-09],[&amp;quot;-2^-28&amp;quot;,8.27181e-25,1.86265e-09],[&amp;quot;-2^-29&amp;quot;,4.1359e-25,1.86265e-09],[&amp;quot;-2^-30&amp;quot;,2.06795e-25,1.86265e-09],[&amp;quot;-2^-31&amp;quot;,1.03398e-25,1.86265e-09],[&amp;quot;-2^-32&amp;quot;,5.16988e-26,1.86265e-09],[&amp;quot;-2^-33&amp;quot;,2.58494e-26,1.86265e-09],[&amp;quot;-2^-34&amp;quot;,1.29247e-26,1.86265e-09],[&amp;quot;-2^-35&amp;quot;,6.46235e-27,1.86265e-09],[&amp;quot;-2^-36&amp;quot;,3.23117e-27,1.86265e-09],[&amp;quot;-2^-37&amp;quot;,1.61559e-27,1.86265e-09],[&amp;quot;-2^-38&amp;quot;,8.07794e-28,1.86265e-09],[&amp;quot;-2^-39&amp;quot;,4.03897e-28,1.86265e-09],[&amp;quot;-2^-40&amp;quot;,2.01948e-28,1.86265e-09],[&amp;quot;-2^-41&amp;quot;,1.00974e-28,1.86265e-09],[&amp;quot;-2^-42&amp;quot;,5.04871e-29,1.86265e-09],[&amp;quot;-2^-43&amp;quot;,2.52435e-29,1.86265e-09],[&amp;quot;-2^-44&amp;quot;,1.26218e-29,1.86265e-09],[&amp;quot;-2^-45&amp;quot;,6.31089e-30,1.86265e-09],[&amp;quot;-2^-46&amp;quot;,3.15544e-30,1.86265e-09],[&amp;quot;-2^-47&amp;quot;,1.57772e-30,1.86265e-09],[&amp;quot;-2^-48&amp;quot;,7.88861e-31,1.86265e-09],[&amp;quot;-2^-49&amp;quot;,3.9443e-31,1.86265e-09],[&amp;quot;-2^-50&amp;quot;,1.97215e-31,1.86265e-09],[&amp;quot;-2^-51&amp;quot;,9.86076e-32,1.86265e-09],[&amp;quot;-2^-52&amp;quot;,4.93038e-32,1.86265e-09],[&amp;quot;-2^-53&amp;quot;,2.46519e-32,1.86265e-09],[&amp;quot;-2^-54&amp;quot;,1.2326e-32,1.86265e-09],[&amp;quot;-2^-55&amp;quot;,0,0],[&amp;quot;-2^-56&amp;quot;,0,0],[&amp;quot;-2^-57&amp;quot;,0,0],[&amp;quot;-2^-58&amp;quot;,0,0],[&amp;quot;-2^-59&amp;quot;,0,0],[&amp;quot;-2^-60&amp;quot;,0,0],[&amp;quot;-2^-61&amp;quot;,0,0],[&amp;quot;-2^-62&amp;quot;,0,0],[&amp;quot;-2^-63&amp;quot;,0,0],[&amp;quot;-2^-64&amp;quot;,0,0],[&amp;quot;-2^-65&amp;quot;,0,0],[&amp;quot;-2^-66&amp;quot;,0,0],[&amp;quot;-2^-67&amp;quot;,0,0],[&amp;quot;-2^-68&amp;quot;,0,0],[&amp;quot;-2^-69&amp;quot;,0,0],[&amp;quot;-2^-70&amp;quot;,0,0],[&amp;quot;-2^-71&amp;quot;,0,0],[&amp;quot;-2^-72&amp;quot;,0,0],[&amp;quot;-2^-73&amp;quot;,0,0],[&amp;quot;-2^-74&amp;quot;,0,0],[&amp;quot;-2^-75&amp;quot;,0,0],[&amp;quot;-2^-76&amp;quot;,0,0],[&amp;quot;-2^-77&amp;quot;,0,0],[&amp;quot;-2^-78&amp;quot;,0,0],[&amp;quot;-2^-79&amp;quot;,0,0],[&amp;quot;-2^-80&amp;quot;,0,0],[&amp;quot;-2^-81&amp;quot;,0,0],[&amp;quot;-2^-82&amp;quot;,0,0],[&amp;quot;-2^-83&amp;quot;,0,0],[&amp;quot;-2^-84&amp;quot;,0,0],[&amp;quot;-2^-85&amp;quot;,0,0],[&amp;quot;-2^-86&amp;quot;,0,0],[&amp;quot;-2^-87&amp;quot;,0,0],[&amp;quot;-2^-88&amp;quot;,0,0],[&amp;quot;-2^-89&amp;quot;,0,0],[&amp;quot;-2^-90&amp;quot;,0,0],[&amp;quot;-2^-91&amp;quot;,0,0],[&amp;quot;-2^-92&amp;quot;,0,0],[&amp;quot;-2^-93&amp;quot;,0,0],[&amp;quot;-2^-94&amp;quot;,0,0],[&amp;quot;-2^-95&amp;quot;,0,0],[&amp;quot;-2^-96&amp;quot;,0,0],[&amp;quot;-2^-97&amp;quot;,0,0],[&amp;quot;-2^-98&amp;quot;,0,0],[&amp;quot;-2^-99&amp;quot;,0,0],[&amp;quot;-2^-100&amp;quot;,0,0],[&amp;quot;-2^-101&amp;quot;,0,0],[&amp;quot;-2^-102&amp;quot;,0,0],[&amp;quot;-2^-103&amp;quot;,0,0],[&amp;quot;-2^-104&amp;quot;,0,0],[&amp;quot;-2^-105&amp;quot;,0,0],[&amp;quot;-2^-106&amp;quot;,0,0],[&amp;quot;-2^-107&amp;quot;,0,0],[&amp;quot;-2^-108&amp;quot;,0,0],[&amp;quot;-2^-109&amp;quot;,0,0],[&amp;quot;-2^-110&amp;quot;,0,0],[&amp;quot;-2^-111&amp;quot;,0,0],[&amp;quot;-2^-112&amp;quot;,0,0],[&amp;quot;-2^-113&amp;quot;,0,0],[&amp;quot;-2^-114&amp;quot;,0,0],[&amp;quot;-2^-115&amp;quot;,0,0],[&amp;quot;-2^-116&amp;quot;,0,0],[&amp;quot;-2^-117&amp;quot;,0,0],[&amp;quot;-2^-118&amp;quot;,0,0],[&amp;quot;-2^-119&amp;quot;,0,0],[&amp;quot;-2^-120&amp;quot;,0,0],[&amp;quot;-2^-121&amp;quot;,0,0],[&amp;quot;-2^-122&amp;quot;,0,0],[&amp;quot;-2^-123&amp;quot;,0,0],[&amp;quot;-2^-124&amp;quot;,0,0],[&amp;quot;-2^-125&amp;quot;,0,0],[&amp;quot;-2^-126&amp;quot;,0,0],[&amp;quot;-SN&amp;quot;,0,0],[&amp;quot;0&amp;quot;,0,0],[&amp;quot;+SN&amp;quot;,0,0],[&amp;quot;+2^-126&amp;quot;,0,0],[&amp;quot;+2^-125&amp;quot;,0,0],[&amp;quot;+2^-124&amp;quot;,0,0],[&amp;quot;+2^-123&amp;quot;,0,0],[&amp;quot;+2^-122&amp;quot;,0,0],[&amp;quot;+2^-121&amp;quot;,0,0],[&amp;quot;+2^-120&amp;quot;,0,0],[&amp;quot;+2^-119&amp;quot;,0,0],[&amp;quot;+2^-118&amp;quot;,0,0],[&amp;quot;+2^-117&amp;quot;,0,0],[&amp;quot;+2^-116&amp;quot;,0,0],[&amp;quot;+2^-115&amp;quot;,0,0],[&amp;quot;+2^-114&amp;quot;,0,0],[&amp;quot;+2^-113&amp;quot;,0,0],[&amp;quot;+2^-112&amp;quot;,0,0],[&amp;quot;+2^-111&amp;quot;,0,0],[&amp;quot;+2^-110&amp;quot;,0,0],[&amp;quot;+2^-109&amp;quot;,0,0],[&amp;quot;+2^-108&amp;quot;,0,0],[&amp;quot;+2^-107&amp;quot;,0,0],[&amp;quot;+2^-106&amp;quot;,0,0],[&amp;quot;+2^-105&amp;quot;,0,0],[&amp;quot;+2^-104&amp;quot;,0,0],[&amp;quot;+2^-103&amp;quot;,0,0],[&amp;quot;+2^-102&amp;quot;,0,0],[&amp;quot;+2^-101&amp;quot;,0,0],[&amp;quot;+2^-100&amp;quot;,0,0],[&amp;quot;+2^-99&amp;quot;,0,0],[&amp;quot;+2^-98&amp;quot;,0,0],[&amp;quot;+2^-97&amp;quot;,0,0],[&amp;quot;+2^-96&amp;quot;,0,0],[&amp;quot;+2^-95&amp;quot;,0,0],[&amp;quot;+2^-94&amp;quot;,0,0],[&amp;quot;+2^-93&amp;quot;,0,0],[&amp;quot;+2^-92&amp;quot;,0,0],[&amp;quot;+2^-91&amp;quot;,0,0],[&amp;quot;+2^-90&amp;quot;,0,0],[&amp;quot;+2^-89&amp;quot;,0,0],[&amp;quot;+2^-88&amp;quot;,0,0],[&amp;quot;+2^-87&amp;quot;,0,0],[&amp;quot;+2^-86&amp;quot;,0,0],[&amp;quot;+2^-85&amp;quot;,0,0],[&amp;quot;+2^-84&amp;quot;,0,0],[&amp;quot;+2^-83&amp;quot;,0,0],[&amp;quot;+2^-82&amp;quot;,0,0],[&amp;quot;+2^-81&amp;quot;,0,0],[&amp;quot;+2^-80&amp;quot;,0,0],[&amp;quot;+2^-79&amp;quot;,0,0],[&amp;quot;+2^-78&amp;quot;,0,0],[&amp;quot;+2^-77&amp;quot;,0,0],[&amp;quot;+2^-76&amp;quot;,0,0],[&amp;quot;+2^-75&amp;quot;,0,0],[&amp;quot;+2^-74&amp;quot;,0,0],[&amp;quot;+2^-73&amp;quot;,0,0],[&amp;quot;+2^-72&amp;quot;,0,0],[&amp;quot;+2^-71&amp;quot;,0,0],[&amp;quot;+2^-70&amp;quot;,0,0],[&amp;quot;+2^-69&amp;quot;,0,0],[&amp;quot;+2^-68&amp;quot;,0,0],[&amp;quot;+2^-67&amp;quot;,0,0],[&amp;quot;+2^-66&amp;quot;,0,0],[&amp;quot;+2^-65&amp;quot;,0,0],[&amp;quot;+2^-64&amp;quot;,0,0],[&amp;quot;+2^-63&amp;quot;,0,0],[&amp;quot;+2^-62&amp;quot;,0,0],[&amp;quot;+2^-61&amp;quot;,0,0],[&amp;quot;+2^-60&amp;quot;,0,0],[&amp;quot;+2^-59&amp;quot;,0,0],[&amp;quot;+2^-58&amp;quot;,0,0],[&amp;quot;+2^-57&amp;quot;,0,0],[&amp;quot;+2^-56&amp;quot;,0,0],[&amp;quot;+2^-55&amp;quot;,0,0],[&amp;quot;+2^-54&amp;quot;,1.2326e-32,1.86265e-09],[&amp;quot;+2^-53&amp;quot;,2.46519e-32,1.86265e-09],[&amp;quot;+2^-52&amp;quot;,4.93038e-32,1.86265e-09],[&amp;quot;+2^-51&amp;quot;,9.86076e-32,1.86265e-09],[&amp;quot;+2^-50&amp;quot;,1.97215e-31,1.86265e-09],[&amp;quot;+2^-49&amp;quot;,3.9443e-31,1.86265e-09],[&amp;quot;+2^-48&amp;quot;,7.88861e-31,1.86265e-09],[&amp;quot;+2^-47&amp;quot;,1.57772e-30,1.86265e-09],[&amp;quot;+2^-46&amp;quot;,3.15544e-30,1.86265e-09],[&amp;quot;+2^-45&amp;quot;,6.31089e-30,1.86265e-09],[&amp;quot;+2^-44&amp;quot;,1.26218e-29,1.86265e-09],[&amp;quot;+2^-43&amp;quot;,2.52435e-29,1.86265e-09],[&amp;quot;+2^-42&amp;quot;,5.04871e-29,1.86265e-09],[&amp;quot;+2^-41&amp;quot;,1.00974e-28,1.86265e-09],[&amp;quot;+2^-40&amp;quot;,2.01948e-28,1.86265e-09],[&amp;quot;+2^-39&amp;quot;,4.03897e-28,1.86265e-09],[&amp;quot;+2^-38&amp;quot;,8.07794e-28,1.86265e-09],[&amp;quot;+2^-37&amp;quot;,1.61559e-27,1.86265e-09],[&amp;quot;+2^-36&amp;quot;,3.23117e-27,1.86265e-09],[&amp;quot;+2^-35&amp;quot;,6.46235e-27,1.86265e-09],[&amp;quot;+2^-34&amp;quot;,1.29247e-26,1.86265e-09],[&amp;quot;+2^-33&amp;quot;,2.58494e-26,1.86265e-09],[&amp;quot;+2^-32&amp;quot;,5.16988e-26,1.86265e-09],[&amp;quot;+2^-31&amp;quot;,1.03398e-25,1.86265e-09],[&amp;quot;+2^-30&amp;quot;,2.06795e-25,1.86265e-09],[&amp;quot;+2^-29&amp;quot;,4.1359e-25,1.86265e-09],[&amp;quot;+2^-28&amp;quot;,8.27181e-25,1.86265e-09],[&amp;quot;+2^-27&amp;quot;,1.65436e-24,1.86265e-09],[&amp;quot;+2^-26&amp;quot;,9.92617e-24,5.58794e-09],[&amp;quot;+2^-25&amp;quot;,7.27919e-23,2.04891e-08],[&amp;quot;+2^-24&amp;quot;,5.691e-22,8.00937e-08],[&amp;quot;+2^-23&amp;quot;,4.52633e-21,3.18512e-07],[&amp;quot;+2^-22&amp;quot;,3.61577e-20,1.27219e-06],[&amp;quot;+2^-21&amp;quot;,2.89156e-19,5.08688e-06],[&amp;quot;+2^-20&amp;quot;,2.31304e-18,2.03457e-05],[&amp;quot;+2^-19&amp;quot;,1.85039e-17,8.13808e-05],[&amp;quot;+2^-18&amp;quot;,1.48029e-16,0.00032552],[&amp;quot;+2^-17&amp;quot;,1.18424e-15,0.00130208],[&amp;quot;+2^-16&amp;quot;,9.47387e-15,0.00520832],[&amp;quot;+2^-15&amp;quot;,7.5791e-14,0.0208333],[&amp;quot;+2^-14&amp;quot;,6.06328e-13,0.0833331],[&amp;quot;+2^-13&amp;quot;,4.85062e-12,0.333332],[&amp;quot;+2^-12&amp;quot;,1.45519e-11,0.5],[&amp;quot;+2^-11&amp;quot;,2.91037e-11,0.499998],[&amp;quot;+2^-10&amp;quot;,5.82082e-11,0.500004],[&amp;quot;+2^-9&amp;quot;,1.16423e-10,0.500033],[&amp;quot;+2^-8&amp;quot;,2.32917e-10,0.500185],[&amp;quot;+2^-7&amp;quot;,4.66402e-10,0.500795],[&amp;quot;+2^-6&amp;quot;,9.51472e-10,0.510818],[&amp;quot;+2^-5&amp;quot;,3.89213e-09,1.04478],[&amp;quot;+2^-4&amp;quot;,2.57737e-07,34.5929],[&amp;quot;+2^-3&amp;quot;,3.11475e-05,2090.28],[&amp;quot;+2^-2&amp;quot;,0.00318735,106950],[&amp;quot;+2^-1&amp;quot;,0.127635,2.14135e+06],[&amp;quot;+2^0&amp;quot;,0.940715,1.57826e+07],[&amp;quot;+2^1&amp;quot;,2.94633,4.94312e+07],[&amp;quot;+2^2&amp;quot;,6.96672,1.16882e+08],[&amp;quot;+2^3&amp;quot;,14.9718,1.33874e+08],[&amp;quot;+2^4&amp;quot;,30.9629,2.59736e+08],[&amp;quot;+2^5&amp;quot;,62.9354,5.27941e+08],[&amp;quot;+2^6&amp;quot;,126.876,1.06431e+09],[&amp;quot;+2^7&amp;quot;,254.754,2.13703e+09],[&amp;quot;+2^8&amp;quot;,510.509,4.28246e+09],[&amp;quot;+2^9&amp;quot;,1022.02,8.57331e+09],[&amp;quot;+2^10&amp;quot;,2045.04,1.7155e+10],[&amp;quot;+2^11&amp;quot;,4091.07,3.43184e+10],[&amp;quot;+2^12&amp;quot;,8183.15,6.86452e+10],[&amp;quot;+2^13&amp;quot;,16367.3,1.37299e+11],[&amp;quot;+2^14&amp;quot;,32735.6,2.74606e+11],[&amp;quot;+2^15&amp;quot;,65472.2,5.4922e+11],[&amp;quot;+2^16&amp;quot;,130945,1.09845e+12],[&amp;quot;+2^17&amp;quot;,261892,2.19691e+12],[&amp;quot;+2^18&amp;quot;,523784,4.39382e+12],[&amp;quot;+2^19&amp;quot;,1.04757e+06,8.78765e+12],[&amp;quot;+2^20&amp;quot;,2.09514e+06,1.75753e+13],[&amp;quot;+2^21&amp;quot;,4.19028e+06,3.51506e+13],[&amp;quot;+2^22&amp;quot;,8.38056e+06,7.03013e+13],[&amp;quot;+2^23&amp;quot;,1.67611e+07,1.40603e+14],[&amp;quot;+2^24&amp;quot;,3.35223e+07,2.81205e+14],[&amp;quot;+2^25&amp;quot;,6.70445e+07,5.6241e+14],[&amp;quot;+2^26&amp;quot;,1.34089e+08,1.12482e+15],[&amp;quot;+2^27&amp;quot;,2.68178e+08,2.24964e+15],[&amp;quot;+2^28&amp;quot;,5.36356e+08,4.49928e+15],[&amp;quot;+2^29&amp;quot;,1.07271e+09,8.99856e+15],[&amp;quot;+2^30&amp;quot;,2.14542e+09,1.79971e+16],[&amp;quot;+2^31&amp;quot;,4.29085e+09,3.59943e+16],[&amp;quot;+2^32&amp;quot;,8.58993e+09,7.20575e+16],[&amp;quot;+2^33&amp;quot;,1.71799e+10,1.44115e+17],[&amp;quot;+2^34&amp;quot;,3.43597e+10,2.8823e+17],[&amp;quot;+2^35&amp;quot;,6.87194e+10,5.7646e+17],[&amp;quot;+2^36&amp;quot;,1.37439e+11,1.15292e+18],[&amp;quot;+2^37&amp;quot;,2.74878e+11,2.30584e+18],[&amp;quot;+2^38&amp;quot;,5.49755e+11,4.61168e+18],[&amp;quot;+2^39&amp;quot;,1.09951e+12,9.22336e+18],[&amp;quot;+2^40&amp;quot;,2.19902e+12,1.84467e+19],[&amp;quot;+2^41&amp;quot;,4.39804e+12,3.68935e+19],[&amp;quot;+2^42&amp;quot;,8.79608e+12,7.37869e+19],[&amp;quot;+2^43&amp;quot;,1.75922e+13,1.47574e+20],[&amp;quot;+2^44&amp;quot;,3.51843e+13,2.95148e+20],[&amp;quot;+2^45&amp;quot;,7.03687e+13,5.90295e+20],[&amp;quot;+2^46&amp;quot;,1.40737e+14,1.18059e+21],[&amp;quot;+2^47&amp;quot;,2.81475e+14,2.36118e+21],[&amp;quot;+2^48&amp;quot;,5.62949e+14,4.72236e+21],[&amp;quot;+2^49&amp;quot;,1.1259e+15,9.44472e+21],[&amp;quot;+2^50&amp;quot;,2.2518e+15,1.88894e+22],[&amp;quot;+2^51&amp;quot;,4.5036e+15,3.77789e+22],[&amp;quot;+2^52&amp;quot;,9.00719e+15,7.55578e+22],[&amp;quot;+2^53&amp;quot;,1.80144e+16,1.51116e+23],[&amp;quot;+2^54&amp;quot;,3.60288e+16,3.02231e+23],[&amp;quot;+2^55&amp;quot;,7.20575e+16,6.04462e+23],[&amp;quot;+2^56&amp;quot;,1.44115e+17,1.20892e+24],[&amp;quot;+2^57&amp;quot;,2.8823e+17,2.41785e+24],[&amp;quot;+2^58&amp;quot;,5.7646e+17,4.8357e+24],[&amp;quot;+2^59&amp;quot;,1.15292e+18,9.6714e+24],[&amp;quot;+2^60&amp;quot;,2.30584e+18,1.93428e+25],[&amp;quot;+2^61&amp;quot;,4.61168e+18,3.86856e+25],[&amp;quot;+2^62&amp;quot;,9.22336e+18,7.73712e+25],[&amp;quot;+2^63&amp;quot;,1.84467e+19,1.54742e+26],[&amp;quot;+2^64&amp;quot;,0,0],[&amp;quot;+2^65&amp;quot;,0,0],[&amp;quot;+2^66&amp;quot;,0,0],[&amp;quot;+2^67&amp;quot;,0,0],[&amp;quot;+2^68&amp;quot;,0,0],[&amp;quot;+2^69&amp;quot;,0,0],[&amp;quot;+2^70&amp;quot;,0,0],[&amp;quot;+2^71&amp;quot;,0,0],[&amp;quot;+2^72&amp;quot;,0,0],[&amp;quot;+2^73&amp;quot;,0,0],[&amp;quot;+2^74&amp;quot;,0,0],[&amp;quot;+2^75&amp;quot;,0,0],[&amp;quot;+2^76&amp;quot;,0,0],[&amp;quot;+2^77&amp;quot;,0,0],[&amp;quot;+2^78&amp;quot;,0,0],[&amp;quot;+2^79&amp;quot;,0,0],[&amp;quot;+2^80&amp;quot;,0,0],[&amp;quot;+2^81&amp;quot;,0,0],[&amp;quot;+2^82&amp;quot;,0,0],[&amp;quot;+2^83&amp;quot;,0,0],[&amp;quot;+2^84&amp;quot;,0,0],[&amp;quot;+2^85&amp;quot;,0,0],[&amp;quot;+2^86&amp;quot;,0,0],[&amp;quot;+2^87&amp;quot;,0,0],[&amp;quot;+2^88&amp;quot;,0,0],[&amp;quot;+2^89&amp;quot;,0,0],[&amp;quot;+2^90&amp;quot;,0,0],[&amp;quot;+2^91&amp;quot;,0,0],[&amp;quot;+2^92&amp;quot;,0,0],[&amp;quot;+2^93&amp;quot;,0,0],[&amp;quot;+2^94&amp;quot;,0,0],[&amp;quot;+2^95&amp;quot;,0,0],[&amp;quot;+2^96&amp;quot;,0,0],[&amp;quot;+2^97&amp;quot;,0,0],[&amp;quot;+2^98&amp;quot;,0,0],[&amp;quot;+2^99&amp;quot;,0,0],[&amp;quot;+2^100&amp;quot;,0,0],[&amp;quot;+2^101&amp;quot;,0,0],[&amp;quot;+2^102&amp;quot;,0,0],[&amp;quot;+2^103&amp;quot;,0,0],[&amp;quot;+2^104&amp;quot;,0,0],[&amp;quot;+2^105&amp;quot;,0,0],[&amp;quot;+2^106&amp;quot;,0,0],[&amp;quot;+2^107&amp;quot;,0,0],[&amp;quot;+2^108&amp;quot;,0,0],[&amp;quot;+2^109&amp;quot;,0,0],[&amp;quot;+2^110&amp;quot;,0,0],[&amp;quot;+2^111&amp;quot;,0,0],[&amp;quot;+2^112&amp;quot;,0,0],[&amp;quot;+2^113&amp;quot;,0,0],[&amp;quot;+2^114&amp;quot;,0,0],[&amp;quot;+2^115&amp;quot;,0,0],[&amp;quot;+2^116&amp;quot;,0,0],[&amp;quot;+2^117&amp;quot;,0,0],[&amp;quot;+2^118&amp;quot;,0,0],[&amp;quot;+2^119&amp;quot;,0,0],[&amp;quot;+2^120&amp;quot;,0,0],[&amp;quot;+2^121&amp;quot;,0,0],[&amp;quot;+2^122&amp;quot;,0,0],[&amp;quot;+2^123&amp;quot;,0,0],[&amp;quot;+2^124&amp;quot;,0,0],[&amp;quot;+2^125&amp;quot;,0,0],[&amp;quot;+2^126&amp;quot;,0,0],[&amp;quot;+2^127&amp;quot;,0,0] ]&lt;br /&gt;
 }}&lt;br /&gt;
&lt;br /&gt;
== Localized Approximation Functions ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
!Id&lt;br /&gt;
!Formula&lt;br /&gt;
!ULP &amp;lt;= 2.0&lt;br /&gt;
!NAN&lt;br /&gt;
|-&lt;br /&gt;
|signum&lt;br /&gt;
|&amp;lt;math&amp;gt;\sgn{x}&amp;lt;/math&amp;gt;&lt;br /&gt;
| -inf &amp;lt; x &amp;lt; -8.31763&lt;br /&gt;
8.31763 &amp;lt; x &amp;lt; inf&lt;br /&gt;
| -&lt;br /&gt;
|-&lt;br /&gt;
|poly1&lt;br /&gt;
|&amp;lt;math&amp;gt;x&amp;lt;/math&amp;gt;&lt;br /&gt;
| -0.000704229 &amp;lt; x &amp;lt; 0.000704229&lt;br /&gt;
| -&lt;br /&gt;
|-&lt;br /&gt;
|poly3&lt;br /&gt;
|&amp;lt;math&amp;gt;x - \frac{1}{3} x^3&amp;lt;/math&amp;gt;&lt;br /&gt;
| -0.0291781 &amp;lt; x &amp;lt; 0.0291781 &lt;br /&gt;
| -&lt;br /&gt;
|-&lt;br /&gt;
|lampert7&lt;br /&gt;
|&amp;lt;math&amp;gt;\frac{x \cdot (135135 + x^2 \cdot (17325 + x^2 \cdot (378 + x^2)))}{135135 + x^2 \cdot (62370 + x^2 \cdot (3150 + 28 \cdot x^2))}&lt;br /&gt;
&amp;lt;/math&amp;gt;&lt;br /&gt;
| -0.000947416 &amp;lt; x &amp;lt; 0.000947207&lt;br /&gt;
| -inf &amp;lt; x &amp;lt; -1.51629e+06&lt;br /&gt;
1.51629e+06 &amp;lt; x &amp;lt; inf&lt;br /&gt;
|-&lt;br /&gt;
|pade&lt;br /&gt;
|&amp;lt;math&amp;gt;x + x \cdot \frac{x^2 \cdot \left( p_1 \cdot x^2 + p_0 \right)}{\left( x^2 + q_1 \right) \cdot x^2 + q_0} &lt;br /&gt;
&lt;br /&gt;
&amp;lt;/math&amp;gt; &amp;lt;math&amp;gt;\begin{align}&lt;br /&gt;
p_0 &amp;amp;= -0.2059432032 \\&lt;br /&gt;
p_1 &amp;amp;= -0.0009577527 \\&lt;br /&gt;
q_0 &amp;amp;= 0.6178299136 \\ &lt;br /&gt;
q_1 &amp;amp;= 0.25&lt;br /&gt;
\end{align}&lt;br /&gt;
&amp;lt;/math&amp;gt;&lt;br /&gt;
| -0.0016973 &amp;lt; x &amp;lt; 0.0016973 &lt;br /&gt;
| -inf &amp;lt; x &amp;lt; -6.14896e+18&lt;br /&gt;
6.14896e+18 &amp;lt; x &amp;lt; inf&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== External Links ==&lt;br /&gt;
&lt;br /&gt;
* https://yaikhom.com/2020-04-28-localised-approximation-of-hyperbolic-tangents.html#mjx-eqn-eqn7-th%20degree%20lambert%20approximant&lt;br /&gt;
*https://github.com/llvm/llvm-project/blob/main/libc/src/math/generic/tanhf.cpp&lt;br /&gt;
*https://forums.developer.nvidia.com/t/hardware-accelerated-tanh-on-turing/173291&lt;br /&gt;
[[Category:Numerical Precision]]&lt;/div&gt;</summary>
		<author><name>Timo.stripf</name></author>
	</entry>
	<entry>
		<id>https://www.emmtrix.com/w139/index.php?title=tanh_Software_Implementation&amp;diff=2928</id>
		<title>tanh Software Implementation</title>
		<link rel="alternate" type="text/html" href="https://www.emmtrix.com/w139/index.php?title=tanh_Software_Implementation&amp;diff=2928"/>
		<updated>2026-02-04T11:53:38Z</updated>

		<summary type="html">&lt;p&gt;Timo.stripf: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;[[File:Hyperbolic Tangent.svg|thumb]]&lt;br /&gt;
The hyperbolic tangent function, commonly referred to as &amp;lt;code&amp;gt;tanh&amp;lt;/code&amp;gt;, is a mathematical function that maps real numbers to the range &amp;lt;math&amp;gt;(-1, 1)&amp;lt;/math&amp;gt;. It is defined as:&lt;br /&gt;
&lt;br /&gt;
:&amp;lt;math&amp;gt;\tanh(x) = \frac{e^x - e^{-x}}{e^x + e^{-x}}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
From a software implementation perspective, &amp;lt;code&amp;gt;tanh&amp;lt;/code&amp;gt; is a numerically challenging function due to the use of exponential functions, which can easily overflow for large inputs and suffer from catastrophic cancellation for very small values of &amp;lt;math&amp;gt;x&amp;lt;/math&amp;gt;. These issues are particularly relevant in single-precision floating-point (&amp;lt;code&amp;gt;float32&amp;lt;/code&amp;gt;) implementations, where both accuracy and performance are critical.&lt;br /&gt;
&lt;br /&gt;
In machine learning and deep learning, &amp;lt;code&amp;gt;tanh&amp;lt;/code&amp;gt; is widely used as an activation function because it introduces non-linearity and produces outputs centered around zero, which can improve gradient-based optimization. As a result, &amp;lt;code&amp;gt;tanh&amp;lt;/code&amp;gt; is often evaluated billions of times during training and inference, making efficient and numerically stable implementations essential.&lt;br /&gt;
&lt;br /&gt;
This article focuses on practical software implementations of &amp;lt;code&amp;gt;tanh&amp;lt;/code&amp;gt;, analyzing different computational formulations with respect to floating-point accuracy, ULP error, overflow behavior, and NaN generation. The goal is to provide a foundation for designing robust &amp;lt;code&amp;gt;tanh&amp;lt;/code&amp;gt; implementations suitable for performance-critical environments such as numerical libraries, embedded systems, and AI workloads.&lt;br /&gt;
&lt;br /&gt;
== Problem Overview ==&lt;br /&gt;
The hyperbolic tangent function poses several challenges when implemented in finite-precision floating-point arithmetic. While the mathematical definition is simple, direct translations into software often suffer from numerical instability and performance issues.&lt;br /&gt;
&lt;br /&gt;
A primary challenge is the use of exponential functions. For large input values, &amp;lt;code&amp;gt;exp(x)&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;exp(-x)&amp;lt;/code&amp;gt; can overflow in single-precision floating-point arithmetic, leading to infinities and, in some formulations, NaN results. Avoiding overflow therefore requires careful reformulation or explicit range handling.&lt;br /&gt;
&lt;br /&gt;
For very small input values, catastrophic cancellation becomes the dominant issue. Expressions such as &amp;lt;math&amp;gt;e^x - e^{-x}&amp;lt;/math&amp;gt; involve the subtraction of nearly equal numbers, causing a severe loss of significant bits and large ULP errors. In these regions, naive implementations may return zero or highly inaccurate results even though &amp;lt;math&amp;gt;\tanh(x) \approx x&amp;lt;/math&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
In addition to accuracy concerns, performance is a critical factor. In many applications, especially machine learning and numerical simulations, &amp;lt;code&amp;gt;tanh&amp;lt;/code&amp;gt; is evaluated extremely frequently. This makes it necessary to balance numerical robustness with the cost of transcendental function calls, branch complexity, and instruction-level efficiency.&lt;br /&gt;
&lt;br /&gt;
==Direct Exponential Formulations==&lt;br /&gt;
&lt;br /&gt;
==Numerical Issues and Error Metrics==&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+&lt;br /&gt;
!Id&lt;br /&gt;
!Formula&lt;br /&gt;
!Highest ULP Error&lt;br /&gt;
!ULP &amp;lt;= 2.0&lt;br /&gt;
!NaN&lt;br /&gt;
|-&lt;br /&gt;
|exp_v1&lt;br /&gt;
|&amp;lt;math&amp;gt;\frac{e^x - e^{-x}}{e^x + e^{-x}}&amp;lt;/math&amp;gt;&lt;br /&gt;
|x &amp;lt; 0: 8.55638e+08 at -2.98023e-08 with 0x0p+0 instead of -0x1p-25&lt;br /&gt;
&lt;br /&gt;
x &amp;gt; 0: 8.55638e+08 at 2.98023e-08 with 0x0p+0 instead of 0x1p-25&lt;br /&gt;
| -88.7246 &amp;lt; x &amp;lt; -7.14197&lt;br /&gt;
7.14197 &amp;lt; x &amp;lt; 88.7246&lt;br /&gt;
| -inf &amp;lt; x &amp;lt; -88.7246&lt;br /&gt;
88.7246 &amp;lt; x &amp;lt; inf&lt;br /&gt;
|-&lt;br /&gt;
|exp_v2&lt;br /&gt;
|&amp;lt;math&amp;gt;\frac{e^{2x} - 1}{e^{2x} + 1}&amp;lt;/math&amp;gt;&lt;br /&gt;
|x &amp;lt; 0: 8.47249e+08 at -1.49012e-08 with 0x0p+0 instead of -0x1p-26&lt;br /&gt;
x &amp;gt; 0: 8.55638e+08 at 2.98023e-08 with 0x0p+0 instead of 0x1.fffffep-26&lt;br /&gt;
| -inf &amp;lt; x &amp;lt; -0.547821&lt;br /&gt;
0.254822 &amp;lt; x &amp;lt; 8.31787&lt;br /&gt;
&lt;br /&gt;
8.38379 &amp;lt; x &amp;lt; 44.3623&lt;br /&gt;
|44.3623 &amp;lt; x &amp;lt; inf&lt;br /&gt;
|-&lt;br /&gt;
|exp_v3&lt;br /&gt;
|&amp;lt;math&amp;gt;1 - \frac{2}{e^{2x} + 1}&amp;lt;/math&amp;gt;&lt;br /&gt;
|x &amp;lt; 0: 8.59832e+08 at -4.47035e-08 with 0x0p+0 instead of -0x1.8p-25&lt;br /&gt;
x &amp;gt; 0: 8.68221e+08 at 8.9407e-08 with 0x0p+0 instead of 0x1.7ffffcp-24&lt;br /&gt;
| -inf &amp;lt; x &amp;lt; -6.26233&lt;br /&gt;
0.346382 &amp;lt; x &amp;lt; inf&lt;br /&gt;
| -&lt;br /&gt;
|-&lt;br /&gt;
|expm1&lt;br /&gt;
|&amp;lt;math&amp;gt;\frac{expm1(2x)}{expm1(2x) + 2}&amp;lt;/math&amp;gt; &lt;br /&gt;
|x &amp;lt; 0: 2.49585 at -3.95627 with -0x1.ffa00ap-1 instead of -0x1.ffa00ep-1&lt;br /&gt;
x &amp;gt; 0: 2.41657 at 0.0155837 with 0x1.fe9b66p-7 instead of 0x1.fe9b62p-7&lt;br /&gt;
| -inf &amp;lt; x &amp;lt; -5.57227&lt;br /&gt;
-0.42981 &amp;lt; x &amp;lt; 0.00777948&lt;br /&gt;
&lt;br /&gt;
0.125351 &amp;lt; x &amp;lt; 44.3623&lt;br /&gt;
|44.3623 &amp;lt; x &amp;lt; inf&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
The first equation is the definition of the hyperbolic tangent function. This equation requires two exponentials with different values. Since exponentials are expensive to compute, we can use the second equation to reduce the number of exponentials to one.&lt;br /&gt;
&lt;br /&gt;
The second equation has the drawback that both the numerator and the denominator can become infinity for large values of x which results in NaN. That happens for &amp;lt;math&amp;gt;|x| &amp;gt; \log(FLT\_MAX)/2 \approx 44&amp;lt;/math&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
The third equation solves this problem but has a slightly higher error for &amp;lt;math&amp;gt;x \approx 2^{-25}&amp;lt;/math&amp;gt;. In the following graph you can see the error chart for equation 3. The error charts for the other two equations are very similar. All three equations have an error of up to 16 mio ULPs for &amp;lt;math&amp;gt;x &amp;lt; 2^{-5}&amp;lt;/math&amp;gt; which means that up to 24 bits could be wrong.&lt;br /&gt;
&lt;br /&gt;
=== exp_v2 ===&lt;br /&gt;
&lt;br /&gt;
{{#widget:FloatErrorGraph|id=tanhf_exp_v2&lt;br /&gt;
  |data=[ [&amp;quot;-2^127&amp;quot;,0,1.17549e-38],[&amp;quot;-2^126&amp;quot;,0,1.17549e-38],[&amp;quot;-2^125&amp;quot;,0,1.17549e-38],[&amp;quot;-2^124&amp;quot;,0,1.17549e-38],[&amp;quot;-2^123&amp;quot;,0,1.17549e-38],[&amp;quot;-2^122&amp;quot;,0,1.17549e-38],[&amp;quot;-2^121&amp;quot;,0,1.17549e-38],[&amp;quot;-2^120&amp;quot;,0,1.17549e-38],[&amp;quot;-2^119&amp;quot;,0,1.17549e-38],[&amp;quot;-2^118&amp;quot;,0,1.17549e-38],[&amp;quot;-2^117&amp;quot;,0,1.17549e-38],[&amp;quot;-2^116&amp;quot;,0,1.17549e-38],[&amp;quot;-2^115&amp;quot;,0,1.17549e-38],[&amp;quot;-2^114&amp;quot;,0,1.17549e-38],[&amp;quot;-2^113&amp;quot;,0,1.17549e-38],[&amp;quot;-2^112&amp;quot;,0,1.17549e-38],[&amp;quot;-2^111&amp;quot;,0,1.17549e-38],[&amp;quot;-2^110&amp;quot;,0,1.17549e-38],[&amp;quot;-2^109&amp;quot;,0,1.17549e-38],[&amp;quot;-2^108&amp;quot;,0,1.17549e-38],[&amp;quot;-2^107&amp;quot;,0,1.17549e-38],[&amp;quot;-2^106&amp;quot;,0,1.17549e-38],[&amp;quot;-2^105&amp;quot;,0,1.17549e-38],[&amp;quot;-2^104&amp;quot;,0,1.17549e-38],[&amp;quot;-2^103&amp;quot;,0,1.17549e-38],[&amp;quot;-2^102&amp;quot;,0,1.17549e-38],[&amp;quot;-2^101&amp;quot;,0,1.17549e-38],[&amp;quot;-2^100&amp;quot;,0,1.17549e-38],[&amp;quot;-2^99&amp;quot;,0,1.17549e-38],[&amp;quot;-2^98&amp;quot;,0,1.17549e-38],[&amp;quot;-2^97&amp;quot;,0,1.17549e-38],[&amp;quot;-2^96&amp;quot;,0,1.17549e-38],[&amp;quot;-2^95&amp;quot;,0,1.17549e-38],[&amp;quot;-2^94&amp;quot;,0,1.17549e-38],[&amp;quot;-2^93&amp;quot;,0,1.17549e-38],[&amp;quot;-2^92&amp;quot;,0,1.17549e-38],[&amp;quot;-2^91&amp;quot;,0,1.17549e-38],[&amp;quot;-2^90&amp;quot;,0,1.17549e-38],[&amp;quot;-2^89&amp;quot;,0,1.17549e-38],[&amp;quot;-2^88&amp;quot;,0,1.17549e-38],[&amp;quot;-2^87&amp;quot;,0,1.17549e-38],[&amp;quot;-2^86&amp;quot;,0,1.17549e-38],[&amp;quot;-2^85&amp;quot;,0,1.17549e-38],[&amp;quot;-2^84&amp;quot;,0,1.17549e-38],[&amp;quot;-2^83&amp;quot;,0,1.17549e-38],[&amp;quot;-2^82&amp;quot;,0,1.17549e-38],[&amp;quot;-2^81&amp;quot;,0,1.17549e-38],[&amp;quot;-2^80&amp;quot;,0,1.17549e-38],[&amp;quot;-2^79&amp;quot;,0,1.17549e-38],[&amp;quot;-2^78&amp;quot;,0,1.17549e-38],[&amp;quot;-2^77&amp;quot;,0,1.17549e-38],[&amp;quot;-2^76&amp;quot;,0,1.17549e-38],[&amp;quot;-2^75&amp;quot;,0,1.17549e-38],[&amp;quot;-2^74&amp;quot;,0,1.17549e-38],[&amp;quot;-2^73&amp;quot;,0,1.17549e-38],[&amp;quot;-2^72&amp;quot;,0,1.17549e-38],[&amp;quot;-2^71&amp;quot;,0,1.17549e-38],[&amp;quot;-2^70&amp;quot;,0,1.17549e-38],[&amp;quot;-2^69&amp;quot;,0,1.17549e-38],[&amp;quot;-2^68&amp;quot;,0,1.17549e-38],[&amp;quot;-2^67&amp;quot;,0,1.17549e-38],[&amp;quot;-2^66&amp;quot;,0,1.17549e-38],[&amp;quot;-2^65&amp;quot;,0,1.17549e-38],[&amp;quot;-2^64&amp;quot;,0,1.17549e-38],[&amp;quot;-2^63&amp;quot;,0,1.17549e-38],[&amp;quot;-2^62&amp;quot;,0,1.17549e-38],[&amp;quot;-2^61&amp;quot;,0,1.17549e-38],[&amp;quot;-2^60&amp;quot;,0,1.17549e-38],[&amp;quot;-2^59&amp;quot;,0,1.17549e-38],[&amp;quot;-2^58&amp;quot;,0,1.17549e-38],[&amp;quot;-2^57&amp;quot;,0,1.17549e-38],[&amp;quot;-2^56&amp;quot;,0,1.17549e-38],[&amp;quot;-2^55&amp;quot;,0,1.17549e-38],[&amp;quot;-2^54&amp;quot;,0,1.17549e-38],[&amp;quot;-2^53&amp;quot;,0,1.17549e-38],[&amp;quot;-2^52&amp;quot;,0,1.17549e-38],[&amp;quot;-2^51&amp;quot;,0,1.17549e-38],[&amp;quot;-2^50&amp;quot;,0,1.17549e-38],[&amp;quot;-2^49&amp;quot;,0,1.17549e-38],[&amp;quot;-2^48&amp;quot;,0,1.17549e-38],[&amp;quot;-2^47&amp;quot;,0,1.17549e-38],[&amp;quot;-2^46&amp;quot;,0,1.17549e-38],[&amp;quot;-2^45&amp;quot;,0,1.17549e-38],[&amp;quot;-2^44&amp;quot;,0,1.17549e-38],[&amp;quot;-2^43&amp;quot;,0,1.17549e-38],[&amp;quot;-2^42&amp;quot;,0,1.17549e-38],[&amp;quot;-2^41&amp;quot;,0,1.17549e-38],[&amp;quot;-2^40&amp;quot;,0,1.17549e-38],[&amp;quot;-2^39&amp;quot;,0,1.17549e-38],[&amp;quot;-2^38&amp;quot;,0,1.17549e-38],[&amp;quot;-2^37&amp;quot;,0,1.17549e-38],[&amp;quot;-2^36&amp;quot;,0,1.17549e-38],[&amp;quot;-2^35&amp;quot;,0,1.17549e-38],[&amp;quot;-2^34&amp;quot;,0,1.17549e-38],[&amp;quot;-2^33&amp;quot;,0,1.17549e-38],[&amp;quot;-2^32&amp;quot;,0,1.17549e-38],[&amp;quot;-2^31&amp;quot;,0,1.17549e-38],[&amp;quot;-2^30&amp;quot;,0,1.17549e-38],[&amp;quot;-2^29&amp;quot;,0,1.17549e-38],[&amp;quot;-2^28&amp;quot;,0,1.17549e-38],[&amp;quot;-2^27&amp;quot;,0,1.17549e-38],[&amp;quot;-2^26&amp;quot;,0,1.17549e-38],[&amp;quot;-2^25&amp;quot;,0,1.17549e-38],[&amp;quot;-2^24&amp;quot;,0,1.17549e-38],[&amp;quot;-2^23&amp;quot;,0,1.17549e-38],[&amp;quot;-2^22&amp;quot;,0,1.17549e-38],[&amp;quot;-2^21&amp;quot;,0,1.17549e-38],[&amp;quot;-2^20&amp;quot;,0,1.17549e-38],[&amp;quot;-2^19&amp;quot;,0,1.17549e-38],[&amp;quot;-2^18&amp;quot;,0,1.17549e-38],[&amp;quot;-2^17&amp;quot;,0,1.17549e-38],[&amp;quot;-2^16&amp;quot;,0,1.17549e-38],[&amp;quot;-2^15&amp;quot;,0,1.17549e-38],[&amp;quot;-2^14&amp;quot;,0,1.17549e-38],[&amp;quot;-2^13&amp;quot;,0,1.17549e-38],[&amp;quot;-2^12&amp;quot;,0,1.17549e-38],[&amp;quot;-2^11&amp;quot;,0,1.17549e-38],[&amp;quot;-2^10&amp;quot;,0,1.17549e-38],[&amp;quot;-2^9&amp;quot;,0,1.17549e-38],[&amp;quot;-2^8&amp;quot;,0,1.17549e-38],[&amp;quot;-2^7&amp;quot;,0,1.17549e-38],[&amp;quot;-2^6&amp;quot;,0,1.17549e-38],[&amp;quot;-2^5&amp;quot;,0,1.17549e-38],[&amp;quot;-2^4&amp;quot;,2.53131e-14,4.24683e-07],[&amp;quot;-2^3&amp;quot;,5.95888e-08,0.999734],[&amp;quot;-2^2&amp;quot;,8.86696e-08,1.48763],[&amp;quot;-2^1&amp;quot;,8.91609e-08,1.49587],[&amp;quot;-2^0&amp;quot;,8.73743e-08,1.4659],[&amp;quot;-2^-1&amp;quot;,8.65382e-08,2.13222],[&amp;quot;-2^-2&amp;quot;,6.18481e-08,2.61428],[&amp;quot;-2^-3&amp;quot;,3.88071e-08,3.08594],[&amp;quot;-2^-4&amp;quot;,2.6234e-08,4.23594],[&amp;quot;-2^-5&amp;quot;,2.02799e-08,8.19726],[&amp;quot;-2^-6&amp;quot;,1.75183e-08,12.4495],[&amp;quot;-2^-7&amp;quot;,1.61937e-08,17.3878],[&amp;quot;-2^-8&amp;quot;,1.5566e-08,33.4278],[&amp;quot;-2^-9&amp;quot;,1.51688e-08,65.1495],[&amp;quot;-2^-10&amp;quot;,1.50281e-08,129.09],[&amp;quot;-2^-11&amp;quot;,1.49613e-08,257.033],[&amp;quot;-2^-12&amp;quot;,1.49296e-08,512.977],[&amp;quot;-2^-13&amp;quot;,1.49174e-08,1025.12],[&amp;quot;-2^-14&amp;quot;,1.4909e-08,2049.08],[&amp;quot;-2^-15&amp;quot;,1.49049e-08,4097.02],[&amp;quot;-2^-16&amp;quot;,1.4903e-08,8193],[&amp;quot;-2^-17&amp;quot;,1.49021e-08,16385],[&amp;quot;-2^-18&amp;quot;,1.48862e-08,32735],[&amp;quot;-2^-19&amp;quot;,1.48973e-08,65519],[&amp;quot;-2^-20&amp;quot;,1.49001e-08,131063],[&amp;quot;-2^-21&amp;quot;,1.49009e-08,262139],[&amp;quot;-2^-22&amp;quot;,1.49011e-08,524285],[&amp;quot;-2^-23&amp;quot;,1.49011e-08,1.04857e+06],[&amp;quot;-2^-24&amp;quot;,1.49011e-08,2.09715e+06],[&amp;quot;-2^-25&amp;quot;,1.49012e-08,4.1943e+06],[&amp;quot;-2^-26&amp;quot;,1.49007e-08,8.38835e+06],[&amp;quot;-2^-27&amp;quot;,1.49012e-08,8.47249e+08],[&amp;quot;-2^-28&amp;quot;,7.45058e-09,8.38861e+08],[&amp;quot;-2^-29&amp;quot;,3.72529e-09,8.30472e+08],[&amp;quot;-2^-30&amp;quot;,1.86265e-09,8.22084e+08],[&amp;quot;-2^-31&amp;quot;,9.31323e-10,8.13695e+08],[&amp;quot;-2^-32&amp;quot;,4.65661e-10,8.05306e+08],[&amp;quot;-2^-33&amp;quot;,2.32831e-10,7.96918e+08],[&amp;quot;-2^-34&amp;quot;,1.16415e-10,7.88529e+08],[&amp;quot;-2^-35&amp;quot;,5.82077e-11,7.80141e+08],[&amp;quot;-2^-36&amp;quot;,2.91038e-11,7.71752e+08],[&amp;quot;-2^-37&amp;quot;,1.45519e-11,7.63363e+08],[&amp;quot;-2^-38&amp;quot;,7.27596e-12,7.54975e+08],[&amp;quot;-2^-39&amp;quot;,3.63798e-12,7.46586e+08],[&amp;quot;-2^-40&amp;quot;,1.81899e-12,7.38198e+08],[&amp;quot;-2^-41&amp;quot;,9.09495e-13,7.29809e+08],[&amp;quot;-2^-42&amp;quot;,4.54747e-13,7.2142e+08],[&amp;quot;-2^-43&amp;quot;,2.27374e-13,7.13032e+08],[&amp;quot;-2^-44&amp;quot;,1.13687e-13,7.04643e+08],[&amp;quot;-2^-45&amp;quot;,5.68434e-14,6.96254e+08],[&amp;quot;-2^-46&amp;quot;,2.84217e-14,6.87866e+08],[&amp;quot;-2^-47&amp;quot;,1.42109e-14,6.79477e+08],[&amp;quot;-2^-48&amp;quot;,7.10543e-15,6.71089e+08],[&amp;quot;-2^-49&amp;quot;,3.55271e-15,6.627e+08],[&amp;quot;-2^-50&amp;quot;,1.77636e-15,6.54311e+08],[&amp;quot;-2^-51&amp;quot;,8.88178e-16,6.45923e+08],[&amp;quot;-2^-52&amp;quot;,4.44089e-16,6.37534e+08],[&amp;quot;-2^-53&amp;quot;,2.22045e-16,6.29146e+08],[&amp;quot;-2^-54&amp;quot;,1.11022e-16,6.20757e+08],[&amp;quot;-2^-55&amp;quot;,5.55111e-17,6.12368e+08],[&amp;quot;-2^-56&amp;quot;,2.77556e-17,6.0398e+08],[&amp;quot;-2^-57&amp;quot;,1.38778e-17,5.95591e+08],[&amp;quot;-2^-58&amp;quot;,6.93889e-18,5.87203e+08],[&amp;quot;-2^-59&amp;quot;,3.46945e-18,5.78814e+08],[&amp;quot;-2^-60&amp;quot;,1.73472e-18,5.70425e+08],[&amp;quot;-2^-61&amp;quot;,8.67362e-19,5.62037e+08],[&amp;quot;-2^-62&amp;quot;,4.33681e-19,5.53648e+08],[&amp;quot;-2^-63&amp;quot;,2.1684e-19,5.4526e+08],[&amp;quot;-2^-64&amp;quot;,1.0842e-19,5.36871e+08],[&amp;quot;-2^-65&amp;quot;,5.42101e-20,5.28482e+08],[&amp;quot;-2^-66&amp;quot;,2.71051e-20,5.20094e+08],[&amp;quot;-2^-67&amp;quot;,1.35525e-20,5.11705e+08],[&amp;quot;-2^-68&amp;quot;,6.77626e-21,5.03316e+08],[&amp;quot;-2^-69&amp;quot;,3.38813e-21,4.94928e+08],[&amp;quot;-2^-70&amp;quot;,1.69407e-21,4.86539e+08],[&amp;quot;-2^-71&amp;quot;,8.47033e-22,4.78151e+08],[&amp;quot;-2^-72&amp;quot;,4.23516e-22,4.69762e+08],[&amp;quot;-2^-73&amp;quot;,2.11758e-22,4.61373e+08],[&amp;quot;-2^-74&amp;quot;,1.05879e-22,4.52985e+08],[&amp;quot;-2^-75&amp;quot;,5.29396e-23,4.44596e+08],[&amp;quot;-2^-76&amp;quot;,2.64698e-23,4.36208e+08],[&amp;quot;-2^-77&amp;quot;,1.32349e-23,4.27819e+08],[&amp;quot;-2^-78&amp;quot;,6.61744e-24,4.1943e+08],[&amp;quot;-2^-79&amp;quot;,3.30872e-24,4.11042e+08],[&amp;quot;-2^-80&amp;quot;,1.65436e-24,4.02653e+08],[&amp;quot;-2^-81&amp;quot;,8.27181e-25,3.94265e+08],[&amp;quot;-2^-82&amp;quot;,4.1359e-25,3.85876e+08],[&amp;quot;-2^-83&amp;quot;,2.06795e-25,3.77487e+08],[&amp;quot;-2^-84&amp;quot;,1.03398e-25,3.69099e+08],[&amp;quot;-2^-85&amp;quot;,5.16988e-26,3.6071e+08],[&amp;quot;-2^-86&amp;quot;,2.58494e-26,3.52322e+08],[&amp;quot;-2^-87&amp;quot;,1.29247e-26,3.43933e+08],[&amp;quot;-2^-88&amp;quot;,6.46235e-27,3.35544e+08],[&amp;quot;-2^-89&amp;quot;,3.23117e-27,3.27156e+08],[&amp;quot;-2^-90&amp;quot;,1.61559e-27,3.18767e+08],[&amp;quot;-2^-91&amp;quot;,8.07794e-28,3.10378e+08],[&amp;quot;-2^-92&amp;quot;,4.03897e-28,3.0199e+08],[&amp;quot;-2^-93&amp;quot;,2.01948e-28,2.93601e+08],[&amp;quot;-2^-94&amp;quot;,1.00974e-28,2.85213e+08],[&amp;quot;-2^-95&amp;quot;,5.04871e-29,2.76824e+08],[&amp;quot;-2^-96&amp;quot;,2.52435e-29,2.68435e+08],[&amp;quot;-2^-97&amp;quot;,1.26218e-29,2.60047e+08],[&amp;quot;-2^-98&amp;quot;,6.31089e-30,2.51658e+08],[&amp;quot;-2^-99&amp;quot;,3.15544e-30,2.4327e+08],[&amp;quot;-2^-100&amp;quot;,1.57772e-30,2.34881e+08],[&amp;quot;-2^-101&amp;quot;,7.88861e-31,2.26492e+08],[&amp;quot;-2^-102&amp;quot;,3.9443e-31,2.18104e+08],[&amp;quot;-2^-103&amp;quot;,1.97215e-31,2.09715e+08],[&amp;quot;-2^-104&amp;quot;,9.86076e-32,2.01327e+08],[&amp;quot;-2^-105&amp;quot;,4.93038e-32,1.92938e+08],[&amp;quot;-2^-106&amp;quot;,2.46519e-32,1.84549e+08],[&amp;quot;-2^-107&amp;quot;,1.2326e-32,1.76161e+08],[&amp;quot;-2^-108&amp;quot;,6.16298e-33,1.67772e+08],[&amp;quot;-2^-109&amp;quot;,3.08149e-33,1.59384e+08],[&amp;quot;-2^-110&amp;quot;,1.54074e-33,1.50995e+08],[&amp;quot;-2^-111&amp;quot;,7.70372e-34,1.42606e+08],[&amp;quot;-2^-112&amp;quot;,3.85186e-34,1.34218e+08],[&amp;quot;-2^-113&amp;quot;,1.92593e-34,1.25829e+08],[&amp;quot;-2^-114&amp;quot;,9.62965e-35,1.17441e+08],[&amp;quot;-2^-115&amp;quot;,4.81482e-35,1.09052e+08],[&amp;quot;-2^-116&amp;quot;,2.40741e-35,1.00663e+08],[&amp;quot;-2^-117&amp;quot;,1.20371e-35,9.22747e+07],[&amp;quot;-2^-118&amp;quot;,6.01853e-36,8.38861e+07],[&amp;quot;-2^-119&amp;quot;,3.00927e-36,7.54975e+07],[&amp;quot;-2^-120&amp;quot;,1.50463e-36,6.71089e+07],[&amp;quot;-2^-121&amp;quot;,7.52316e-37,5.87203e+07],[&amp;quot;-2^-122&amp;quot;,3.76158e-37,5.03316e+07],[&amp;quot;-2^-123&amp;quot;,1.88079e-37,4.1943e+07],[&amp;quot;-2^-124&amp;quot;,9.40395e-38,3.35544e+07],[&amp;quot;-2^-125&amp;quot;,4.70198e-38,2.51658e+07],[&amp;quot;-2^-126&amp;quot;,2.35099e-38,1.67772e+07],[&amp;quot;-SN&amp;quot;,1.17546e-38,8.38835e+06],[&amp;quot;0&amp;quot;,0,1.17549e-38],[&amp;quot;+SN&amp;quot;,1.17546e-38,8.38835e+06],[&amp;quot;+2^-126&amp;quot;,2.35095e-38,1.6777e+07],[&amp;quot;+2^-125&amp;quot;,4.70191e-38,2.51656e+07],[&amp;quot;+2^-124&amp;quot;,9.40381e-38,3.35542e+07],[&amp;quot;+2^-123&amp;quot;,1.88076e-37,4.19428e+07],[&amp;quot;+2^-122&amp;quot;,3.76152e-37,5.03314e+07],[&amp;quot;+2^-121&amp;quot;,7.52305e-37,5.872e+07],[&amp;quot;+2^-120&amp;quot;,1.50461e-36,6.71086e+07],[&amp;quot;+2^-119&amp;quot;,3.00922e-36,7.54972e+07],[&amp;quot;+2^-118&amp;quot;,6.01844e-36,8.38858e+07],[&amp;quot;+2^-117&amp;quot;,1.20369e-35,9.22744e+07],[&amp;quot;+2^-116&amp;quot;,2.40738e-35,1.00663e+08],[&amp;quot;+2^-115&amp;quot;,4.81475e-35,1.09052e+08],[&amp;quot;+2^-114&amp;quot;,9.6295e-35,1.1744e+08],[&amp;quot;+2^-113&amp;quot;,1.9259e-34,1.25829e+08],[&amp;quot;+2^-112&amp;quot;,3.8518e-34,1.34217e+08],[&amp;quot;+2^-111&amp;quot;,7.7036e-34,1.42606e+08],[&amp;quot;+2^-110&amp;quot;,1.54072e-33,1.50995e+08],[&amp;quot;+2^-109&amp;quot;,3.08144e-33,1.59383e+08],[&amp;quot;+2^-108&amp;quot;,6.16288e-33,1.67772e+08],[&amp;quot;+2^-107&amp;quot;,1.23258e-32,1.76161e+08],[&amp;quot;+2^-106&amp;quot;,2.46515e-32,1.84549e+08],[&amp;quot;+2^-105&amp;quot;,4.93031e-32,1.92938e+08],[&amp;quot;+2^-104&amp;quot;,9.86061e-32,2.01326e+08],[&amp;quot;+2^-103&amp;quot;,1.97212e-31,2.09715e+08],[&amp;quot;+2^-102&amp;quot;,3.94424e-31,2.18104e+08],[&amp;quot;+2^-101&amp;quot;,7.88849e-31,2.26492e+08],[&amp;quot;+2^-100&amp;quot;,1.5777e-30,2.34881e+08],[&amp;quot;+2^-99&amp;quot;,3.1554e-30,2.43269e+08],[&amp;quot;+2^-98&amp;quot;,6.31079e-30,2.51658e+08],[&amp;quot;+2^-97&amp;quot;,1.26216e-29,2.60047e+08],[&amp;quot;+2^-96&amp;quot;,2.52432e-29,2.68435e+08],[&amp;quot;+2^-95&amp;quot;,5.04863e-29,2.76824e+08],[&amp;quot;+2^-94&amp;quot;,1.00973e-28,2.85212e+08],[&amp;quot;+2^-93&amp;quot;,2.01945e-28,2.93601e+08],[&amp;quot;+2^-92&amp;quot;,4.03891e-28,3.0199e+08],[&amp;quot;+2^-91&amp;quot;,8.07781e-28,3.10378e+08],[&amp;quot;+2^-90&amp;quot;,1.61556e-27,3.18767e+08],[&amp;quot;+2^-89&amp;quot;,3.23112e-27,3.27155e+08],[&amp;quot;+2^-88&amp;quot;,6.46225e-27,3.35544e+08],[&amp;quot;+2^-87&amp;quot;,1.29245e-26,3.43933e+08],[&amp;quot;+2^-86&amp;quot;,2.5849e-26,3.52321e+08],[&amp;quot;+2^-85&amp;quot;,5.1698e-26,3.6071e+08],[&amp;quot;+2^-84&amp;quot;,1.03396e-25,3.69098e+08],[&amp;quot;+2^-83&amp;quot;,2.06792e-25,3.77487e+08],[&amp;quot;+2^-82&amp;quot;,4.13584e-25,3.85876e+08],[&amp;quot;+2^-81&amp;quot;,8.27168e-25,3.94264e+08],[&amp;quot;+2^-80&amp;quot;,1.65434e-24,4.02653e+08],[&amp;quot;+2^-79&amp;quot;,3.30867e-24,4.11042e+08],[&amp;quot;+2^-78&amp;quot;,6.61734e-24,4.1943e+08],[&amp;quot;+2^-77&amp;quot;,1.32347e-23,4.27819e+08],[&amp;quot;+2^-76&amp;quot;,2.64694e-23,4.36207e+08],[&amp;quot;+2^-75&amp;quot;,5.29388e-23,4.44596e+08],[&amp;quot;+2^-74&amp;quot;,1.05878e-22,4.52985e+08],[&amp;quot;+2^-73&amp;quot;,2.11755e-22,4.61373e+08],[&amp;quot;+2^-72&amp;quot;,4.2351e-22,4.69762e+08],[&amp;quot;+2^-71&amp;quot;,8.4702e-22,4.7815e+08],[&amp;quot;+2^-70&amp;quot;,1.69404e-21,4.86539e+08],[&amp;quot;+2^-69&amp;quot;,3.38808e-21,4.94928e+08],[&amp;quot;+2^-68&amp;quot;,6.77616e-21,5.03316e+08],[&amp;quot;+2^-67&amp;quot;,1.35523e-20,5.11705e+08],[&amp;quot;+2^-66&amp;quot;,2.71046e-20,5.20093e+08],[&amp;quot;+2^-65&amp;quot;,5.42093e-20,5.28482e+08],[&amp;quot;+2^-64&amp;quot;,1.08419e-19,5.36871e+08],[&amp;quot;+2^-63&amp;quot;,2.16837e-19,5.45259e+08],[&amp;quot;+2^-62&amp;quot;,4.33674e-19,5.53648e+08],[&amp;quot;+2^-61&amp;quot;,8.67349e-19,5.62036e+08],[&amp;quot;+2^-60&amp;quot;,1.7347e-18,5.70425e+08],[&amp;quot;+2^-59&amp;quot;,3.46939e-18,5.78814e+08],[&amp;quot;+2^-58&amp;quot;,6.93879e-18,5.87202e+08],[&amp;quot;+2^-57&amp;quot;,1.38776e-17,5.95591e+08],[&amp;quot;+2^-56&amp;quot;,2.77552e-17,6.0398e+08],[&amp;quot;+2^-55&amp;quot;,5.55103e-17,6.12368e+08],[&amp;quot;+2^-54&amp;quot;,1.11021e-16,6.20757e+08],[&amp;quot;+2^-53&amp;quot;,2.22041e-16,6.29145e+08],[&amp;quot;+2^-52&amp;quot;,4.44082e-16,6.37534e+08],[&amp;quot;+2^-51&amp;quot;,8.88165e-16,6.45923e+08],[&amp;quot;+2^-50&amp;quot;,1.77633e-15,6.54311e+08],[&amp;quot;+2^-49&amp;quot;,3.55266e-15,6.627e+08],[&amp;quot;+2^-48&amp;quot;,7.10532e-15,6.71088e+08],[&amp;quot;+2^-47&amp;quot;,1.42106e-14,6.79477e+08],[&amp;quot;+2^-46&amp;quot;,2.84213e-14,6.87866e+08],[&amp;quot;+2^-45&amp;quot;,5.68426e-14,6.96254e+08],[&amp;quot;+2^-44&amp;quot;,1.13685e-13,7.04643e+08],[&amp;quot;+2^-43&amp;quot;,2.2737e-13,7.13031e+08],[&amp;quot;+2^-42&amp;quot;,4.5474e-13,7.2142e+08],[&amp;quot;+2^-41&amp;quot;,9.09481e-13,7.29809e+08],[&amp;quot;+2^-40&amp;quot;,1.81896e-12,7.38197e+08],[&amp;quot;+2^-39&amp;quot;,3.63792e-12,7.46586e+08],[&amp;quot;+2^-38&amp;quot;,7.27585e-12,7.54974e+08],[&amp;quot;+2^-37&amp;quot;,1.45517e-11,7.63363e+08],[&amp;quot;+2^-36&amp;quot;,2.91034e-11,7.71752e+08],[&amp;quot;+2^-35&amp;quot;,5.82068e-11,7.8014e+08],[&amp;quot;+2^-34&amp;quot;,1.16414e-10,7.88529e+08],[&amp;quot;+2^-33&amp;quot;,2.32827e-10,7.96918e+08],[&amp;quot;+2^-32&amp;quot;,4.65654e-10,8.05306e+08],[&amp;quot;+2^-31&amp;quot;,9.31308e-10,8.13695e+08],[&amp;quot;+2^-30&amp;quot;,1.86262e-09,8.22083e+08],[&amp;quot;+2^-29&amp;quot;,3.72523e-09,8.30472e+08],[&amp;quot;+2^-28&amp;quot;,7.45047e-09,8.38861e+08],[&amp;quot;+2^-27&amp;quot;,1.49009e-08,8.47249e+08],[&amp;quot;+2^-26&amp;quot;,2.98019e-08,8.55638e+08],[&amp;quot;+2^-25&amp;quot;,2.98023e-08,8.38861e+06],[&amp;quot;+2^-24&amp;quot;,2.98023e-08,4.1943e+06],[&amp;quot;+2^-23&amp;quot;,2.98023e-08,2.09715e+06],[&amp;quot;+2^-22&amp;quot;,2.98023e-08,1.04857e+06],[&amp;quot;+2^-21&amp;quot;,2.98021e-08,524284],[&amp;quot;+2^-20&amp;quot;,2.98014e-08,262136],[&amp;quot;+2^-19&amp;quot;,2.97987e-08,131056],[&amp;quot;+2^-18&amp;quot;,2.97878e-08,65504],[&amp;quot;+2^-17&amp;quot;,2.98014e-08,32767],[&amp;quot;+2^-16&amp;quot;,2.98005e-08,16384],[&amp;quot;+2^-15&amp;quot;,2.98023e-08,8192],[&amp;quot;+2^-14&amp;quot;,2.98025e-08,4351.99],[&amp;quot;+2^-13&amp;quot;,2.98044e-08,2815.96],[&amp;quot;+2^-12&amp;quot;,2.98091e-08,1024.23],[&amp;quot;+2^-11&amp;quot;,2.9812e-08,512.167],[&amp;quot;+2^-10&amp;quot;,2.98154e-08,256.113],[&amp;quot;+2^-9&amp;quot;,2.98591e-08,128.244],[&amp;quot;+2^-8&amp;quot;,3.00204e-08,85.3328],[&amp;quot;+2^-7&amp;quot;,3.01947e-08,43.675],[&amp;quot;+2^-6&amp;quot;,3.05713e-08,30.45],[&amp;quot;+2^-5&amp;quot;,3.14319e-08,8.43743],[&amp;quot;+2^-4&amp;quot;,3.31291e-08,7.77058],[&amp;quot;+2^-3&amp;quot;,3.58597e-08,3.99659],[&amp;quot;+2^-2&amp;quot;,4.14639e-08,2.29246],[&amp;quot;+2^-1&amp;quot;,8.02409e-08,1.34622],[&amp;quot;+2^0&amp;quot;,8.66201e-08,1.45324],[&amp;quot;+2^1&amp;quot;,8.8654e-08,1.48737],[&amp;quot;+2^2&amp;quot;,6.7976e-08,1.14045],[&amp;quot;+2^3&amp;quot;,1.33955e-07,2.2474],[&amp;quot;+2^4&amp;quot;,2.53131e-14,4.24683e-07],[&amp;quot;+2^5&amp;quot;,0,null],[&amp;quot;+2^6&amp;quot;,0,null],[&amp;quot;+2^7&amp;quot;,0,null],[&amp;quot;+2^8&amp;quot;,0,null],[&amp;quot;+2^9&amp;quot;,0,null],[&amp;quot;+2^10&amp;quot;,0,null],[&amp;quot;+2^11&amp;quot;,0,null],[&amp;quot;+2^12&amp;quot;,0,null],[&amp;quot;+2^13&amp;quot;,0,null],[&amp;quot;+2^14&amp;quot;,0,null],[&amp;quot;+2^15&amp;quot;,0,null],[&amp;quot;+2^16&amp;quot;,0,null],[&amp;quot;+2^17&amp;quot;,0,null],[&amp;quot;+2^18&amp;quot;,0,null],[&amp;quot;+2^19&amp;quot;,0,null],[&amp;quot;+2^20&amp;quot;,0,null],[&amp;quot;+2^21&amp;quot;,0,null],[&amp;quot;+2^22&amp;quot;,0,null],[&amp;quot;+2^23&amp;quot;,0,null],[&amp;quot;+2^24&amp;quot;,0,null],[&amp;quot;+2^25&amp;quot;,0,null],[&amp;quot;+2^26&amp;quot;,0,null],[&amp;quot;+2^27&amp;quot;,0,null],[&amp;quot;+2^28&amp;quot;,0,null],[&amp;quot;+2^29&amp;quot;,0,null],[&amp;quot;+2^30&amp;quot;,0,null],[&amp;quot;+2^31&amp;quot;,0,null],[&amp;quot;+2^32&amp;quot;,0,null],[&amp;quot;+2^33&amp;quot;,0,null],[&amp;quot;+2^34&amp;quot;,0,null],[&amp;quot;+2^35&amp;quot;,0,null],[&amp;quot;+2^36&amp;quot;,0,null],[&amp;quot;+2^37&amp;quot;,0,null],[&amp;quot;+2^38&amp;quot;,0,null],[&amp;quot;+2^39&amp;quot;,0,null],[&amp;quot;+2^40&amp;quot;,0,null],[&amp;quot;+2^41&amp;quot;,0,null],[&amp;quot;+2^42&amp;quot;,0,null],[&amp;quot;+2^43&amp;quot;,0,null],[&amp;quot;+2^44&amp;quot;,0,null],[&amp;quot;+2^45&amp;quot;,0,null],[&amp;quot;+2^46&amp;quot;,0,null],[&amp;quot;+2^47&amp;quot;,0,null],[&amp;quot;+2^48&amp;quot;,0,null],[&amp;quot;+2^49&amp;quot;,0,null],[&amp;quot;+2^50&amp;quot;,0,null],[&amp;quot;+2^51&amp;quot;,0,null],[&amp;quot;+2^52&amp;quot;,0,null],[&amp;quot;+2^53&amp;quot;,0,null],[&amp;quot;+2^54&amp;quot;,0,null],[&amp;quot;+2^55&amp;quot;,0,null],[&amp;quot;+2^56&amp;quot;,0,null],[&amp;quot;+2^57&amp;quot;,0,null],[&amp;quot;+2^58&amp;quot;,0,null],[&amp;quot;+2^59&amp;quot;,0,null],[&amp;quot;+2^60&amp;quot;,0,null],[&amp;quot;+2^61&amp;quot;,0,null],[&amp;quot;+2^62&amp;quot;,0,null],[&amp;quot;+2^63&amp;quot;,0,null],[&amp;quot;+2^64&amp;quot;,0,null],[&amp;quot;+2^65&amp;quot;,0,null],[&amp;quot;+2^66&amp;quot;,0,null],[&amp;quot;+2^67&amp;quot;,0,null],[&amp;quot;+2^68&amp;quot;,0,null],[&amp;quot;+2^69&amp;quot;,0,null],[&amp;quot;+2^70&amp;quot;,0,null],[&amp;quot;+2^71&amp;quot;,0,null],[&amp;quot;+2^72&amp;quot;,0,null],[&amp;quot;+2^73&amp;quot;,0,null],[&amp;quot;+2^74&amp;quot;,0,null],[&amp;quot;+2^75&amp;quot;,0,null],[&amp;quot;+2^76&amp;quot;,0,null],[&amp;quot;+2^77&amp;quot;,0,null],[&amp;quot;+2^78&amp;quot;,0,null],[&amp;quot;+2^79&amp;quot;,0,null],[&amp;quot;+2^80&amp;quot;,0,null],[&amp;quot;+2^81&amp;quot;,0,null],[&amp;quot;+2^82&amp;quot;,0,null],[&amp;quot;+2^83&amp;quot;,0,null],[&amp;quot;+2^84&amp;quot;,0,null],[&amp;quot;+2^85&amp;quot;,0,null],[&amp;quot;+2^86&amp;quot;,0,null],[&amp;quot;+2^87&amp;quot;,0,null],[&amp;quot;+2^88&amp;quot;,0,null],[&amp;quot;+2^89&amp;quot;,0,null],[&amp;quot;+2^90&amp;quot;,0,null],[&amp;quot;+2^91&amp;quot;,0,null],[&amp;quot;+2^92&amp;quot;,0,null],[&amp;quot;+2^93&amp;quot;,0,null],[&amp;quot;+2^94&amp;quot;,0,null],[&amp;quot;+2^95&amp;quot;,0,null],[&amp;quot;+2^96&amp;quot;,0,null],[&amp;quot;+2^97&amp;quot;,0,null],[&amp;quot;+2^98&amp;quot;,0,null],[&amp;quot;+2^99&amp;quot;,0,null],[&amp;quot;+2^100&amp;quot;,0,null],[&amp;quot;+2^101&amp;quot;,0,null],[&amp;quot;+2^102&amp;quot;,0,null],[&amp;quot;+2^103&amp;quot;,0,null],[&amp;quot;+2^104&amp;quot;,0,null],[&amp;quot;+2^105&amp;quot;,0,null],[&amp;quot;+2^106&amp;quot;,0,null],[&amp;quot;+2^107&amp;quot;,0,null],[&amp;quot;+2^108&amp;quot;,0,null],[&amp;quot;+2^109&amp;quot;,0,null],[&amp;quot;+2^110&amp;quot;,0,null],[&amp;quot;+2^111&amp;quot;,0,null],[&amp;quot;+2^112&amp;quot;,0,null],[&amp;quot;+2^113&amp;quot;,0,null],[&amp;quot;+2^114&amp;quot;,0,null],[&amp;quot;+2^115&amp;quot;,0,null],[&amp;quot;+2^116&amp;quot;,0,null],[&amp;quot;+2^117&amp;quot;,0,null],[&amp;quot;+2^118&amp;quot;,0,null],[&amp;quot;+2^119&amp;quot;,0,null],[&amp;quot;+2^120&amp;quot;,0,null],[&amp;quot;+2^121&amp;quot;,0,null],[&amp;quot;+2^122&amp;quot;,0,null],[&amp;quot;+2^123&amp;quot;,0,null],[&amp;quot;+2^124&amp;quot;,0,null],[&amp;quot;+2^125&amp;quot;,0,null],[&amp;quot;+2^126&amp;quot;,0,null],[&amp;quot;+2^127&amp;quot;,0,null] ]&lt;br /&gt;
 }}&lt;br /&gt;
=== expm1 ===&lt;br /&gt;
&lt;br /&gt;
{{#widget:FloatErrorGraph|id=tanhf_expm1&lt;br /&gt;
  |data=[ [&amp;quot;-2^127&amp;quot;,0,1.17549e-38],[&amp;quot;-2^126&amp;quot;,0,1.17549e-38],[&amp;quot;-2^125&amp;quot;,0,1.17549e-38],[&amp;quot;-2^124&amp;quot;,0,1.17549e-38],[&amp;quot;-2^123&amp;quot;,0,1.17549e-38],[&amp;quot;-2^122&amp;quot;,0,1.17549e-38],[&amp;quot;-2^121&amp;quot;,0,1.17549e-38],[&amp;quot;-2^120&amp;quot;,0,1.17549e-38],[&amp;quot;-2^119&amp;quot;,0,1.17549e-38],[&amp;quot;-2^118&amp;quot;,0,1.17549e-38],[&amp;quot;-2^117&amp;quot;,0,1.17549e-38],[&amp;quot;-2^116&amp;quot;,0,1.17549e-38],[&amp;quot;-2^115&amp;quot;,0,1.17549e-38],[&amp;quot;-2^114&amp;quot;,0,1.17549e-38],[&amp;quot;-2^113&amp;quot;,0,1.17549e-38],[&amp;quot;-2^112&amp;quot;,0,1.17549e-38],[&amp;quot;-2^111&amp;quot;,0,1.17549e-38],[&amp;quot;-2^110&amp;quot;,0,1.17549e-38],[&amp;quot;-2^109&amp;quot;,0,1.17549e-38],[&amp;quot;-2^108&amp;quot;,0,1.17549e-38],[&amp;quot;-2^107&amp;quot;,0,1.17549e-38],[&amp;quot;-2^106&amp;quot;,0,1.17549e-38],[&amp;quot;-2^105&amp;quot;,0,1.17549e-38],[&amp;quot;-2^104&amp;quot;,0,1.17549e-38],[&amp;quot;-2^103&amp;quot;,0,1.17549e-38],[&amp;quot;-2^102&amp;quot;,0,1.17549e-38],[&amp;quot;-2^101&amp;quot;,0,1.17549e-38],[&amp;quot;-2^100&amp;quot;,0,1.17549e-38],[&amp;quot;-2^99&amp;quot;,0,1.17549e-38],[&amp;quot;-2^98&amp;quot;,0,1.17549e-38],[&amp;quot;-2^97&amp;quot;,0,1.17549e-38],[&amp;quot;-2^96&amp;quot;,0,1.17549e-38],[&amp;quot;-2^95&amp;quot;,0,1.17549e-38],[&amp;quot;-2^94&amp;quot;,0,1.17549e-38],[&amp;quot;-2^93&amp;quot;,0,1.17549e-38],[&amp;quot;-2^92&amp;quot;,0,1.17549e-38],[&amp;quot;-2^91&amp;quot;,0,1.17549e-38],[&amp;quot;-2^90&amp;quot;,0,1.17549e-38],[&amp;quot;-2^89&amp;quot;,0,1.17549e-38],[&amp;quot;-2^88&amp;quot;,0,1.17549e-38],[&amp;quot;-2^87&amp;quot;,0,1.17549e-38],[&amp;quot;-2^86&amp;quot;,0,1.17549e-38],[&amp;quot;-2^85&amp;quot;,0,1.17549e-38],[&amp;quot;-2^84&amp;quot;,0,1.17549e-38],[&amp;quot;-2^83&amp;quot;,0,1.17549e-38],[&amp;quot;-2^82&amp;quot;,0,1.17549e-38],[&amp;quot;-2^81&amp;quot;,0,1.17549e-38],[&amp;quot;-2^80&amp;quot;,0,1.17549e-38],[&amp;quot;-2^79&amp;quot;,0,1.17549e-38],[&amp;quot;-2^78&amp;quot;,0,1.17549e-38],[&amp;quot;-2^77&amp;quot;,0,1.17549e-38],[&amp;quot;-2^76&amp;quot;,0,1.17549e-38],[&amp;quot;-2^75&amp;quot;,0,1.17549e-38],[&amp;quot;-2^74&amp;quot;,0,1.17549e-38],[&amp;quot;-2^73&amp;quot;,0,1.17549e-38],[&amp;quot;-2^72&amp;quot;,0,1.17549e-38],[&amp;quot;-2^71&amp;quot;,0,1.17549e-38],[&amp;quot;-2^70&amp;quot;,0,1.17549e-38],[&amp;quot;-2^69&amp;quot;,0,1.17549e-38],[&amp;quot;-2^68&amp;quot;,0,1.17549e-38],[&amp;quot;-2^67&amp;quot;,0,1.17549e-38],[&amp;quot;-2^66&amp;quot;,0,1.17549e-38],[&amp;quot;-2^65&amp;quot;,0,1.17549e-38],[&amp;quot;-2^64&amp;quot;,0,1.17549e-38],[&amp;quot;-2^63&amp;quot;,0,1.17549e-38],[&amp;quot;-2^62&amp;quot;,0,1.17549e-38],[&amp;quot;-2^61&amp;quot;,0,1.17549e-38],[&amp;quot;-2^60&amp;quot;,0,1.17549e-38],[&amp;quot;-2^59&amp;quot;,0,1.17549e-38],[&amp;quot;-2^58&amp;quot;,0,1.17549e-38],[&amp;quot;-2^57&amp;quot;,0,1.17549e-38],[&amp;quot;-2^56&amp;quot;,0,1.17549e-38],[&amp;quot;-2^55&amp;quot;,0,1.17549e-38],[&amp;quot;-2^54&amp;quot;,0,1.17549e-38],[&amp;quot;-2^53&amp;quot;,0,1.17549e-38],[&amp;quot;-2^52&amp;quot;,0,1.17549e-38],[&amp;quot;-2^51&amp;quot;,0,1.17549e-38],[&amp;quot;-2^50&amp;quot;,0,1.17549e-38],[&amp;quot;-2^49&amp;quot;,0,1.17549e-38],[&amp;quot;-2^48&amp;quot;,0,1.17549e-38],[&amp;quot;-2^47&amp;quot;,0,1.17549e-38],[&amp;quot;-2^46&amp;quot;,0,1.17549e-38],[&amp;quot;-2^45&amp;quot;,0,1.17549e-38],[&amp;quot;-2^44&amp;quot;,0,1.17549e-38],[&amp;quot;-2^43&amp;quot;,0,1.17549e-38],[&amp;quot;-2^42&amp;quot;,0,1.17549e-38],[&amp;quot;-2^41&amp;quot;,0,1.17549e-38],[&amp;quot;-2^40&amp;quot;,0,1.17549e-38],[&amp;quot;-2^39&amp;quot;,0,1.17549e-38],[&amp;quot;-2^38&amp;quot;,0,1.17549e-38],[&amp;quot;-2^37&amp;quot;,0,1.17549e-38],[&amp;quot;-2^36&amp;quot;,0,1.17549e-38],[&amp;quot;-2^35&amp;quot;,0,1.17549e-38],[&amp;quot;-2^34&amp;quot;,0,1.17549e-38],[&amp;quot;-2^33&amp;quot;,0,1.17549e-38],[&amp;quot;-2^32&amp;quot;,0,1.17549e-38],[&amp;quot;-2^31&amp;quot;,0,1.17549e-38],[&amp;quot;-2^30&amp;quot;,0,1.17549e-38],[&amp;quot;-2^29&amp;quot;,0,1.17549e-38],[&amp;quot;-2^28&amp;quot;,0,1.17549e-38],[&amp;quot;-2^27&amp;quot;,0,1.17549e-38],[&amp;quot;-2^26&amp;quot;,0,1.17549e-38],[&amp;quot;-2^25&amp;quot;,0,1.17549e-38],[&amp;quot;-2^24&amp;quot;,0,1.17549e-38],[&amp;quot;-2^23&amp;quot;,0,1.17549e-38],[&amp;quot;-2^22&amp;quot;,0,1.17549e-38],[&amp;quot;-2^21&amp;quot;,0,1.17549e-38],[&amp;quot;-2^20&amp;quot;,0,1.17549e-38],[&amp;quot;-2^19&amp;quot;,0,1.17549e-38],[&amp;quot;-2^18&amp;quot;,0,1.17549e-38],[&amp;quot;-2^17&amp;quot;,0,1.17549e-38],[&amp;quot;-2^16&amp;quot;,0,1.17549e-38],[&amp;quot;-2^15&amp;quot;,0,1.17549e-38],[&amp;quot;-2^14&amp;quot;,0,1.17549e-38],[&amp;quot;-2^13&amp;quot;,0,1.17549e-38],[&amp;quot;-2^12&amp;quot;,0,1.17549e-38],[&amp;quot;-2^11&amp;quot;,0,1.17549e-38],[&amp;quot;-2^10&amp;quot;,0,1.17549e-38],[&amp;quot;-2^9&amp;quot;,0,1.17549e-38],[&amp;quot;-2^8&amp;quot;,0,1.17549e-38],[&amp;quot;-2^7&amp;quot;,0,1.17549e-38],[&amp;quot;-2^6&amp;quot;,0,1.17549e-38],[&amp;quot;-2^5&amp;quot;,0,1.17549e-38],[&amp;quot;-2^4&amp;quot;,2.53131e-14,4.24683e-07],[&amp;quot;-2^3&amp;quot;,1.19138e-07,1.9988],[&amp;quot;-2^2&amp;quot;,1.46791e-07,2.46274],[&amp;quot;-2^1&amp;quot;,1.47723e-07,2.47839],[&amp;quot;-2^0&amp;quot;,1.40758e-07,2.36153],[&amp;quot;-2^-1&amp;quot;,1.13422e-07,2.23445],[&amp;quot;-2^-2&amp;quot;,6.38012e-08,2.14081],[&amp;quot;-2^-3&amp;quot;,2.69538e-08,1.80884],[&amp;quot;-2^-4&amp;quot;,1.21308e-08,1.62817],[&amp;quot;-2^-5&amp;quot;,5.59535e-09,1.50199],[&amp;quot;-2^-6&amp;quot;,2.69177e-09,1.44513],[&amp;quot;-2^-7&amp;quot;,1.31289e-09,1.4097],[&amp;quot;-2^-8&amp;quot;,6.49688e-10,1.3952],[&amp;quot;-2^-9&amp;quot;,3.32043e-10,1.42611],[&amp;quot;-2^-10&amp;quot;,1.66275e-10,1.4283],[&amp;quot;-2^-11&amp;quot;,8.08595e-11,1.38916],[&amp;quot;-2^-12&amp;quot;,3.8939e-11,1.33794],[&amp;quot;-2^-13&amp;quot;,1.9401e-11,1.33323],[&amp;quot;-2^-14&amp;quot;,7.88212e-12,1.08331],[&amp;quot;-2^-15&amp;quot;,3.71372e-12,1.02082],[&amp;quot;-2^-16&amp;quot;,1.82845e-12,1.0052],[&amp;quot;-2^-17&amp;quot;,9.10676e-13,1.0013],[&amp;quot;-2^-18&amp;quot;,4.54895e-13,1.00032],[&amp;quot;-2^-19&amp;quot;,2.27392e-13,1.00008],[&amp;quot;-2^-20&amp;quot;,1.13689e-13,1.00002],[&amp;quot;-2^-21&amp;quot;,5.68437e-14,1],[&amp;quot;-2^-22&amp;quot;,2.84217e-14,1],[&amp;quot;-2^-23&amp;quot;,1.42109e-14,1],[&amp;quot;-2^-24&amp;quot;,7.10543e-15,1],[&amp;quot;-2^-25&amp;quot;,3.55271e-15,1],[&amp;quot;-2^-26&amp;quot;,9.92617e-24,5.58794e-09],[&amp;quot;-2^-27&amp;quot;,1.65436e-24,1.86265e-09],[&amp;quot;-2^-28&amp;quot;,8.27181e-25,1.86265e-09],[&amp;quot;-2^-29&amp;quot;,4.1359e-25,1.86265e-09],[&amp;quot;-2^-30&amp;quot;,2.06795e-25,1.86265e-09],[&amp;quot;-2^-31&amp;quot;,1.03398e-25,1.86265e-09],[&amp;quot;-2^-32&amp;quot;,5.16988e-26,1.86265e-09],[&amp;quot;-2^-33&amp;quot;,2.58494e-26,1.86265e-09],[&amp;quot;-2^-34&amp;quot;,1.29247e-26,1.86265e-09],[&amp;quot;-2^-35&amp;quot;,6.46235e-27,1.86265e-09],[&amp;quot;-2^-36&amp;quot;,3.23117e-27,1.86265e-09],[&amp;quot;-2^-37&amp;quot;,1.61559e-27,1.86265e-09],[&amp;quot;-2^-38&amp;quot;,8.07794e-28,1.86265e-09],[&amp;quot;-2^-39&amp;quot;,4.03897e-28,1.86265e-09],[&amp;quot;-2^-40&amp;quot;,2.01948e-28,1.86265e-09],[&amp;quot;-2^-41&amp;quot;,1.00974e-28,1.86265e-09],[&amp;quot;-2^-42&amp;quot;,5.04871e-29,1.86265e-09],[&amp;quot;-2^-43&amp;quot;,2.52435e-29,1.86265e-09],[&amp;quot;-2^-44&amp;quot;,1.26218e-29,1.86265e-09],[&amp;quot;-2^-45&amp;quot;,6.31089e-30,1.86265e-09],[&amp;quot;-2^-46&amp;quot;,3.15544e-30,1.86265e-09],[&amp;quot;-2^-47&amp;quot;,1.57772e-30,1.86265e-09],[&amp;quot;-2^-48&amp;quot;,7.88861e-31,1.86265e-09],[&amp;quot;-2^-49&amp;quot;,3.9443e-31,1.86265e-09],[&amp;quot;-2^-50&amp;quot;,1.97215e-31,1.86265e-09],[&amp;quot;-2^-51&amp;quot;,9.86076e-32,1.86265e-09],[&amp;quot;-2^-52&amp;quot;,4.93038e-32,1.86265e-09],[&amp;quot;-2^-53&amp;quot;,2.46519e-32,1.86265e-09],[&amp;quot;-2^-54&amp;quot;,1.2326e-32,1.86265e-09],[&amp;quot;-2^-55&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-56&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-57&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-58&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-59&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-60&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-61&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-62&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-63&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-64&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-65&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-66&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-67&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-68&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-69&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-70&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-71&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-72&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-73&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-74&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-75&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-76&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-77&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-78&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-79&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-80&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-81&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-82&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-83&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-84&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-85&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-86&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-87&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-88&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-89&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-90&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-91&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-92&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-93&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-94&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-95&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-96&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-97&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-98&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-99&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-100&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-101&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-102&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-103&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-104&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-105&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-106&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-107&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-108&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-109&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-110&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-111&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-112&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-113&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-114&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-115&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-116&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-117&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-118&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-119&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-120&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-121&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-122&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-123&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-124&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-125&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-126&amp;quot;,0,1.17549e-38],[&amp;quot;-SN&amp;quot;,0,1.17549e-38],[&amp;quot;0&amp;quot;,0,1.17549e-38],[&amp;quot;+SN&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-126&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-125&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-124&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-123&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-122&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-121&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-120&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-119&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-118&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-117&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-116&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-115&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-114&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-113&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-112&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-111&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-110&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-109&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-108&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-107&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-106&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-105&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-104&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-103&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-102&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-101&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-100&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-99&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-98&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-97&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-96&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-95&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-94&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-93&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-92&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-91&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-90&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-89&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-88&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-87&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-86&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-85&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-84&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-83&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-82&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-81&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-80&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-79&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-78&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-77&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-76&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-75&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-74&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-73&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-72&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-71&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-70&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-69&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-68&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-67&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-66&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-65&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-64&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-63&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-62&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-61&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-60&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-59&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-58&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-57&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-56&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-55&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-54&amp;quot;,1.2326e-32,1.86265e-09],[&amp;quot;+2^-53&amp;quot;,2.46519e-32,1.86265e-09],[&amp;quot;+2^-52&amp;quot;,4.93038e-32,1.86265e-09],[&amp;quot;+2^-51&amp;quot;,9.86076e-32,1.86265e-09],[&amp;quot;+2^-50&amp;quot;,1.97215e-31,1.86265e-09],[&amp;quot;+2^-49&amp;quot;,3.9443e-31,1.86265e-09],[&amp;quot;+2^-48&amp;quot;,7.88861e-31,1.86265e-09],[&amp;quot;+2^-47&amp;quot;,1.57772e-30,1.86265e-09],[&amp;quot;+2^-46&amp;quot;,3.15544e-30,1.86265e-09],[&amp;quot;+2^-45&amp;quot;,6.31089e-30,1.86265e-09],[&amp;quot;+2^-44&amp;quot;,1.26218e-29,1.86265e-09],[&amp;quot;+2^-43&amp;quot;,2.52435e-29,1.86265e-09],[&amp;quot;+2^-42&amp;quot;,5.04871e-29,1.86265e-09],[&amp;quot;+2^-41&amp;quot;,1.00974e-28,1.86265e-09],[&amp;quot;+2^-40&amp;quot;,2.01948e-28,1.86265e-09],[&amp;quot;+2^-39&amp;quot;,4.03897e-28,1.86265e-09],[&amp;quot;+2^-38&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-37&amp;quot;,1.61559e-27,1.86265e-09],[&amp;quot;+2^-36&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-35&amp;quot;,6.46235e-27,1.86265e-09],[&amp;quot;+2^-34&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-33&amp;quot;,2.58494e-26,1.86265e-09],[&amp;quot;+2^-32&amp;quot;,5.16988e-26,1.86265e-09],[&amp;quot;+2^-31&amp;quot;,1.03398e-25,1.86265e-09],[&amp;quot;+2^-30&amp;quot;,2.06795e-25,1.86265e-09],[&amp;quot;+2^-29&amp;quot;,4.1359e-25,1.86265e-09],[&amp;quot;+2^-28&amp;quot;,8.27181e-25,1.86265e-09],[&amp;quot;+2^-27&amp;quot;,1.65436e-24,1.86265e-09],[&amp;quot;+2^-26&amp;quot;,9.92617e-24,5.58794e-09],[&amp;quot;+2^-25&amp;quot;,3.55271e-15,1],[&amp;quot;+2^-24&amp;quot;,7.10543e-15,1],[&amp;quot;+2^-23&amp;quot;,1.42109e-14,1],[&amp;quot;+2^-22&amp;quot;,2.84217e-14,1],[&amp;quot;+2^-21&amp;quot;,5.68437e-14,1],[&amp;quot;+2^-20&amp;quot;,1.13689e-13,1.00002],[&amp;quot;+2^-19&amp;quot;,2.27391e-13,1.00008],[&amp;quot;+2^-18&amp;quot;,4.54892e-13,1.00032],[&amp;quot;+2^-17&amp;quot;,9.10665e-13,1.00129],[&amp;quot;+2^-16&amp;quot;,1.82841e-12,1.00518],[&amp;quot;+2^-15&amp;quot;,3.71353e-12,1.02077],[&amp;quot;+2^-14&amp;quot;,1.39798e-11,1.92136],[&amp;quot;+2^-13&amp;quot;,2.63239e-11,1.80896],[&amp;quot;+2^-12&amp;quot;,5.37712e-11,1.84757],[&amp;quot;+2^-11&amp;quot;,1.09932e-10,1.88861],[&amp;quot;+2^-10&amp;quot;,2.24787e-10,1.93091],[&amp;quot;+2^-9&amp;quot;,4.3452e-10,1.86625],[&amp;quot;+2^-8&amp;quot;,1.01783e-09,2.18576],[&amp;quot;+2^-7&amp;quot;,2.10562e-09,2.26089],[&amp;quot;+2^-6&amp;quot;,4.03954e-09,2.16871],[&amp;quot;+2^-5&amp;quot;,8.15746e-09,2.18975],[&amp;quot;+2^-4&amp;quot;,1.5545e-08,2.08641],[&amp;quot;+2^-3&amp;quot;,2.62153e-08,2.05864],[&amp;quot;+2^-2&amp;quot;,4.09295e-08,1.73804],[&amp;quot;+2^-1&amp;quot;,8.02409e-08,1.34622],[&amp;quot;+2^0&amp;quot;,8.66201e-08,1.45324],[&amp;quot;+2^1&amp;quot;,8.8654e-08,1.48737],[&amp;quot;+2^2&amp;quot;,6.7976e-08,1.14045],[&amp;quot;+2^3&amp;quot;,7.44117e-08,1.24842],[&amp;quot;+2^4&amp;quot;,2.53131e-14,4.24683e-07],[&amp;quot;+2^5&amp;quot;,0,null],[&amp;quot;+2^6&amp;quot;,0,null],[&amp;quot;+2^7&amp;quot;,0,null],[&amp;quot;+2^8&amp;quot;,0,null],[&amp;quot;+2^9&amp;quot;,0,null],[&amp;quot;+2^10&amp;quot;,0,null],[&amp;quot;+2^11&amp;quot;,0,null],[&amp;quot;+2^12&amp;quot;,0,null],[&amp;quot;+2^13&amp;quot;,0,null],[&amp;quot;+2^14&amp;quot;,0,null],[&amp;quot;+2^15&amp;quot;,0,null],[&amp;quot;+2^16&amp;quot;,0,null],[&amp;quot;+2^17&amp;quot;,0,null],[&amp;quot;+2^18&amp;quot;,0,null],[&amp;quot;+2^19&amp;quot;,0,null],[&amp;quot;+2^20&amp;quot;,0,null],[&amp;quot;+2^21&amp;quot;,0,null],[&amp;quot;+2^22&amp;quot;,0,null],[&amp;quot;+2^23&amp;quot;,0,null],[&amp;quot;+2^24&amp;quot;,0,null],[&amp;quot;+2^25&amp;quot;,0,null],[&amp;quot;+2^26&amp;quot;,0,null],[&amp;quot;+2^27&amp;quot;,0,null],[&amp;quot;+2^28&amp;quot;,0,null],[&amp;quot;+2^29&amp;quot;,0,null],[&amp;quot;+2^30&amp;quot;,0,null],[&amp;quot;+2^31&amp;quot;,0,null],[&amp;quot;+2^32&amp;quot;,0,null],[&amp;quot;+2^33&amp;quot;,0,null],[&amp;quot;+2^34&amp;quot;,0,null],[&amp;quot;+2^35&amp;quot;,0,null],[&amp;quot;+2^36&amp;quot;,0,null],[&amp;quot;+2^37&amp;quot;,0,null],[&amp;quot;+2^38&amp;quot;,0,null],[&amp;quot;+2^39&amp;quot;,0,null],[&amp;quot;+2^40&amp;quot;,0,null],[&amp;quot;+2^41&amp;quot;,0,null],[&amp;quot;+2^42&amp;quot;,0,null],[&amp;quot;+2^43&amp;quot;,0,null],[&amp;quot;+2^44&amp;quot;,0,null],[&amp;quot;+2^45&amp;quot;,0,null],[&amp;quot;+2^46&amp;quot;,0,null],[&amp;quot;+2^47&amp;quot;,0,null],[&amp;quot;+2^48&amp;quot;,0,null],[&amp;quot;+2^49&amp;quot;,0,null],[&amp;quot;+2^50&amp;quot;,0,null],[&amp;quot;+2^51&amp;quot;,0,null],[&amp;quot;+2^52&amp;quot;,0,null],[&amp;quot;+2^53&amp;quot;,0,null],[&amp;quot;+2^54&amp;quot;,0,null],[&amp;quot;+2^55&amp;quot;,0,null],[&amp;quot;+2^56&amp;quot;,0,null],[&amp;quot;+2^57&amp;quot;,0,null],[&amp;quot;+2^58&amp;quot;,0,null],[&amp;quot;+2^59&amp;quot;,0,null],[&amp;quot;+2^60&amp;quot;,0,null],[&amp;quot;+2^61&amp;quot;,0,null],[&amp;quot;+2^62&amp;quot;,0,null],[&amp;quot;+2^63&amp;quot;,0,null],[&amp;quot;+2^64&amp;quot;,0,null],[&amp;quot;+2^65&amp;quot;,0,null],[&amp;quot;+2^66&amp;quot;,0,null],[&amp;quot;+2^67&amp;quot;,0,null],[&amp;quot;+2^68&amp;quot;,0,null],[&amp;quot;+2^69&amp;quot;,0,null],[&amp;quot;+2^70&amp;quot;,0,null],[&amp;quot;+2^71&amp;quot;,0,null],[&amp;quot;+2^72&amp;quot;,0,null],[&amp;quot;+2^73&amp;quot;,0,null],[&amp;quot;+2^74&amp;quot;,0,null],[&amp;quot;+2^75&amp;quot;,0,null],[&amp;quot;+2^76&amp;quot;,0,null],[&amp;quot;+2^77&amp;quot;,0,null],[&amp;quot;+2^78&amp;quot;,0,null],[&amp;quot;+2^79&amp;quot;,0,null],[&amp;quot;+2^80&amp;quot;,0,null],[&amp;quot;+2^81&amp;quot;,0,null],[&amp;quot;+2^82&amp;quot;,0,null],[&amp;quot;+2^83&amp;quot;,0,null],[&amp;quot;+2^84&amp;quot;,0,null],[&amp;quot;+2^85&amp;quot;,0,null],[&amp;quot;+2^86&amp;quot;,0,null],[&amp;quot;+2^87&amp;quot;,0,null],[&amp;quot;+2^88&amp;quot;,0,null],[&amp;quot;+2^89&amp;quot;,0,null],[&amp;quot;+2^90&amp;quot;,0,null],[&amp;quot;+2^91&amp;quot;,0,null],[&amp;quot;+2^92&amp;quot;,0,null],[&amp;quot;+2^93&amp;quot;,0,null],[&amp;quot;+2^94&amp;quot;,0,null],[&amp;quot;+2^95&amp;quot;,0,null],[&amp;quot;+2^96&amp;quot;,0,null],[&amp;quot;+2^97&amp;quot;,0,null],[&amp;quot;+2^98&amp;quot;,0,null],[&amp;quot;+2^99&amp;quot;,0,null],[&amp;quot;+2^100&amp;quot;,0,null],[&amp;quot;+2^101&amp;quot;,0,null],[&amp;quot;+2^102&amp;quot;,0,null],[&amp;quot;+2^103&amp;quot;,0,null],[&amp;quot;+2^104&amp;quot;,0,null],[&amp;quot;+2^105&amp;quot;,0,null],[&amp;quot;+2^106&amp;quot;,0,null],[&amp;quot;+2^107&amp;quot;,0,null],[&amp;quot;+2^108&amp;quot;,0,null],[&amp;quot;+2^109&amp;quot;,0,null],[&amp;quot;+2^110&amp;quot;,0,null],[&amp;quot;+2^111&amp;quot;,0,null],[&amp;quot;+2^112&amp;quot;,0,null],[&amp;quot;+2^113&amp;quot;,0,null],[&amp;quot;+2^114&amp;quot;,0,null],[&amp;quot;+2^115&amp;quot;,0,null],[&amp;quot;+2^116&amp;quot;,0,null],[&amp;quot;+2^117&amp;quot;,0,null],[&amp;quot;+2^118&amp;quot;,0,null],[&amp;quot;+2^119&amp;quot;,0,null],[&amp;quot;+2^120&amp;quot;,0,null],[&amp;quot;+2^121&amp;quot;,0,null],[&amp;quot;+2^122&amp;quot;,0,null],[&amp;quot;+2^123&amp;quot;,0,null],[&amp;quot;+2^124&amp;quot;,0,null],[&amp;quot;+2^125&amp;quot;,0,null],[&amp;quot;+2^126&amp;quot;,0,null],[&amp;quot;+2^127&amp;quot;,0,null] ]&lt;br /&gt;
 }}&lt;br /&gt;
&lt;br /&gt;
==Taylor==&lt;br /&gt;
&lt;br /&gt;
{{#widget:FloatErrorGraph|id=taylor&lt;br /&gt;
  |data=[ [&amp;quot;-2^127&amp;quot;,0,0],[&amp;quot;-2^126&amp;quot;,0,0],[&amp;quot;-2^125&amp;quot;,0,0],[&amp;quot;-2^124&amp;quot;,0,0],[&amp;quot;-2^123&amp;quot;,0,0],[&amp;quot;-2^122&amp;quot;,0,0],[&amp;quot;-2^121&amp;quot;,0,0],[&amp;quot;-2^120&amp;quot;,0,0],[&amp;quot;-2^119&amp;quot;,0,0],[&amp;quot;-2^118&amp;quot;,0,0],[&amp;quot;-2^117&amp;quot;,0,0],[&amp;quot;-2^116&amp;quot;,0,0],[&amp;quot;-2^115&amp;quot;,0,0],[&amp;quot;-2^114&amp;quot;,0,0],[&amp;quot;-2^113&amp;quot;,0,0],[&amp;quot;-2^112&amp;quot;,0,0],[&amp;quot;-2^111&amp;quot;,0,0],[&amp;quot;-2^110&amp;quot;,0,0],[&amp;quot;-2^109&amp;quot;,0,0],[&amp;quot;-2^108&amp;quot;,0,0],[&amp;quot;-2^107&amp;quot;,0,0],[&amp;quot;-2^106&amp;quot;,0,0],[&amp;quot;-2^105&amp;quot;,0,0],[&amp;quot;-2^104&amp;quot;,0,0],[&amp;quot;-2^103&amp;quot;,0,0],[&amp;quot;-2^102&amp;quot;,0,0],[&amp;quot;-2^101&amp;quot;,0,0],[&amp;quot;-2^100&amp;quot;,0,0],[&amp;quot;-2^99&amp;quot;,0,0],[&amp;quot;-2^98&amp;quot;,0,0],[&amp;quot;-2^97&amp;quot;,0,0],[&amp;quot;-2^96&amp;quot;,0,0],[&amp;quot;-2^95&amp;quot;,0,0],[&amp;quot;-2^94&amp;quot;,0,0],[&amp;quot;-2^93&amp;quot;,0,0],[&amp;quot;-2^92&amp;quot;,0,0],[&amp;quot;-2^91&amp;quot;,0,0],[&amp;quot;-2^90&amp;quot;,0,0],[&amp;quot;-2^89&amp;quot;,0,0],[&amp;quot;-2^88&amp;quot;,0,0],[&amp;quot;-2^87&amp;quot;,0,0],[&amp;quot;-2^86&amp;quot;,0,0],[&amp;quot;-2^85&amp;quot;,0,0],[&amp;quot;-2^84&amp;quot;,0,0],[&amp;quot;-2^83&amp;quot;,0,0],[&amp;quot;-2^82&amp;quot;,0,0],[&amp;quot;-2^81&amp;quot;,0,0],[&amp;quot;-2^80&amp;quot;,0,0],[&amp;quot;-2^79&amp;quot;,0,0],[&amp;quot;-2^78&amp;quot;,0,0],[&amp;quot;-2^77&amp;quot;,0,0],[&amp;quot;-2^76&amp;quot;,0,0],[&amp;quot;-2^75&amp;quot;,0,0],[&amp;quot;-2^74&amp;quot;,0,0],[&amp;quot;-2^73&amp;quot;,0,0],[&amp;quot;-2^72&amp;quot;,0,0],[&amp;quot;-2^71&amp;quot;,0,0],[&amp;quot;-2^70&amp;quot;,0,0],[&amp;quot;-2^69&amp;quot;,0,0],[&amp;quot;-2^68&amp;quot;,0,0],[&amp;quot;-2^67&amp;quot;,0,0],[&amp;quot;-2^66&amp;quot;,0,0],[&amp;quot;-2^65&amp;quot;,0,0],[&amp;quot;-2^64&amp;quot;,0,0],[&amp;quot;-2^63&amp;quot;,1.84467e+19,3.09485e+26],[&amp;quot;-2^62&amp;quot;,9.22336e+18,1.54742e+26],[&amp;quot;-2^61&amp;quot;,4.61168e+18,7.73712e+25],[&amp;quot;-2^60&amp;quot;,2.30584e+18,3.86856e+25],[&amp;quot;-2^59&amp;quot;,1.15292e+18,1.93428e+25],[&amp;quot;-2^58&amp;quot;,5.7646e+17,9.6714e+24],[&amp;quot;-2^57&amp;quot;,2.8823e+17,4.8357e+24],[&amp;quot;-2^56&amp;quot;,1.44115e+17,2.41785e+24],[&amp;quot;-2^55&amp;quot;,7.20575e+16,1.20892e+24],[&amp;quot;-2^54&amp;quot;,3.60288e+16,6.04462e+23],[&amp;quot;-2^53&amp;quot;,1.80144e+16,3.02231e+23],[&amp;quot;-2^52&amp;quot;,9.00719e+15,1.51116e+23],[&amp;quot;-2^51&amp;quot;,4.5036e+15,7.55578e+22],[&amp;quot;-2^50&amp;quot;,2.2518e+15,3.77789e+22],[&amp;quot;-2^49&amp;quot;,1.1259e+15,1.88894e+22],[&amp;quot;-2^48&amp;quot;,5.62949e+14,9.44472e+21],[&amp;quot;-2^47&amp;quot;,2.81475e+14,4.72236e+21],[&amp;quot;-2^46&amp;quot;,1.40737e+14,2.36118e+21],[&amp;quot;-2^45&amp;quot;,7.03687e+13,1.18059e+21],[&amp;quot;-2^44&amp;quot;,3.51843e+13,5.90295e+20],[&amp;quot;-2^43&amp;quot;,1.75922e+13,2.95148e+20],[&amp;quot;-2^42&amp;quot;,8.79608e+12,1.47574e+20],[&amp;quot;-2^41&amp;quot;,4.39804e+12,7.37869e+19],[&amp;quot;-2^40&amp;quot;,2.19902e+12,3.68935e+19],[&amp;quot;-2^39&amp;quot;,1.09951e+12,1.84467e+19],[&amp;quot;-2^38&amp;quot;,5.49755e+11,9.22336e+18],[&amp;quot;-2^37&amp;quot;,2.74878e+11,4.61168e+18],[&amp;quot;-2^36&amp;quot;,1.37439e+11,2.30584e+18],[&amp;quot;-2^35&amp;quot;,6.87194e+10,1.15292e+18],[&amp;quot;-2^34&amp;quot;,3.43597e+10,5.7646e+17],[&amp;quot;-2^33&amp;quot;,1.71799e+10,2.8823e+17],[&amp;quot;-2^32&amp;quot;,8.58993e+09,1.44115e+17],[&amp;quot;-2^31&amp;quot;,4.29085e+09,7.19885e+16],[&amp;quot;-2^30&amp;quot;,2.14542e+09,3.59943e+16],[&amp;quot;-2^29&amp;quot;,1.07271e+09,1.79971e+16],[&amp;quot;-2^28&amp;quot;,5.36356e+08,8.99856e+15],[&amp;quot;-2^27&amp;quot;,2.68178e+08,4.49928e+15],[&amp;quot;-2^26&amp;quot;,1.34089e+08,2.24964e+15],[&amp;quot;-2^25&amp;quot;,6.70445e+07,1.12482e+15],[&amp;quot;-2^24&amp;quot;,3.35223e+07,5.6241e+14],[&amp;quot;-2^23&amp;quot;,1.67611e+07,2.81205e+14],[&amp;quot;-2^22&amp;quot;,8.38056e+06,1.40603e+14],[&amp;quot;-2^21&amp;quot;,4.19028e+06,7.03013e+13],[&amp;quot;-2^20&amp;quot;,2.09514e+06,3.51506e+13],[&amp;quot;-2^19&amp;quot;,1.04757e+06,1.75753e+13],[&amp;quot;-2^18&amp;quot;,523784,8.78764e+12],[&amp;quot;-2^17&amp;quot;,261892,4.39381e+12],[&amp;quot;-2^16&amp;quot;,130945,2.1969e+12],[&amp;quot;-2^15&amp;quot;,65472.2,1.09844e+12],[&amp;quot;-2^14&amp;quot;,32735.6,5.49212e+11],[&amp;quot;-2^13&amp;quot;,16367.3,2.74598e+11],[&amp;quot;-2^12&amp;quot;,8183.15,1.3729e+11],[&amp;quot;-2^11&amp;quot;,4091.07,6.86368e+10],[&amp;quot;-2^10&amp;quot;,2045.04,3.431e+10],[&amp;quot;-2^9&amp;quot;,1022.02,1.71466e+10],[&amp;quot;-2^8&amp;quot;,510.509,8.56492e+09],[&amp;quot;-2^7&amp;quot;,254.754,4.27406e+09],[&amp;quot;-2^6&amp;quot;,126.876,2.12862e+09],[&amp;quot;-2^5&amp;quot;,62.9354,1.05588e+09],[&amp;quot;-2^4&amp;quot;,30.9629,5.19471e+08],[&amp;quot;-2^3&amp;quot;,14.9718,2.51185e+08],[&amp;quot;-2^2&amp;quot;,6.96672,1.16882e+08],[&amp;quot;-2^1&amp;quot;,2.94633,4.94312e+07],[&amp;quot;-2^0&amp;quot;,0.940715,1.57826e+07],[&amp;quot;-2^-1&amp;quot;,0.127635,2.14135e+06],[&amp;quot;-2^-2&amp;quot;,0.00318735,106950],[&amp;quot;-2^-3&amp;quot;,3.11475e-05,2090.28],[&amp;quot;-2^-4&amp;quot;,2.57737e-07,34.5929],[&amp;quot;-2^-5&amp;quot;,3.89213e-09,1.04478],[&amp;quot;-2^-6&amp;quot;,9.51472e-10,0.510818],[&amp;quot;-2^-7&amp;quot;,4.66402e-10,0.500795],[&amp;quot;-2^-8&amp;quot;,2.32917e-10,0.500185],[&amp;quot;-2^-9&amp;quot;,1.16423e-10,0.500033],[&amp;quot;-2^-10&amp;quot;,5.82082e-11,0.500004],[&amp;quot;-2^-11&amp;quot;,2.91037e-11,0.499998],[&amp;quot;-2^-12&amp;quot;,1.45519e-11,0.5],[&amp;quot;-2^-13&amp;quot;,4.85062e-12,0.333332],[&amp;quot;-2^-14&amp;quot;,6.06328e-13,0.0833331],[&amp;quot;-2^-15&amp;quot;,7.5791e-14,0.0208333],[&amp;quot;-2^-16&amp;quot;,9.47387e-15,0.00520832],[&amp;quot;-2^-17&amp;quot;,1.18424e-15,0.00130208],[&amp;quot;-2^-18&amp;quot;,1.48029e-16,0.00032552],[&amp;quot;-2^-19&amp;quot;,1.85039e-17,8.13808e-05],[&amp;quot;-2^-20&amp;quot;,2.31304e-18,2.03457e-05],[&amp;quot;-2^-21&amp;quot;,2.89156e-19,5.08688e-06],[&amp;quot;-2^-22&amp;quot;,3.61577e-20,1.27219e-06],[&amp;quot;-2^-23&amp;quot;,4.52633e-21,3.18512e-07],[&amp;quot;-2^-24&amp;quot;,5.691e-22,8.00937e-08],[&amp;quot;-2^-25&amp;quot;,7.27919e-23,2.04891e-08],[&amp;quot;-2^-26&amp;quot;,9.92617e-24,5.58794e-09],[&amp;quot;-2^-27&amp;quot;,1.65436e-24,1.86265e-09],[&amp;quot;-2^-28&amp;quot;,8.27181e-25,1.86265e-09],[&amp;quot;-2^-29&amp;quot;,4.1359e-25,1.86265e-09],[&amp;quot;-2^-30&amp;quot;,2.06795e-25,1.86265e-09],[&amp;quot;-2^-31&amp;quot;,1.03398e-25,1.86265e-09],[&amp;quot;-2^-32&amp;quot;,5.16988e-26,1.86265e-09],[&amp;quot;-2^-33&amp;quot;,2.58494e-26,1.86265e-09],[&amp;quot;-2^-34&amp;quot;,1.29247e-26,1.86265e-09],[&amp;quot;-2^-35&amp;quot;,6.46235e-27,1.86265e-09],[&amp;quot;-2^-36&amp;quot;,3.23117e-27,1.86265e-09],[&amp;quot;-2^-37&amp;quot;,1.61559e-27,1.86265e-09],[&amp;quot;-2^-38&amp;quot;,8.07794e-28,1.86265e-09],[&amp;quot;-2^-39&amp;quot;,4.03897e-28,1.86265e-09],[&amp;quot;-2^-40&amp;quot;,2.01948e-28,1.86265e-09],[&amp;quot;-2^-41&amp;quot;,1.00974e-28,1.86265e-09],[&amp;quot;-2^-42&amp;quot;,5.04871e-29,1.86265e-09],[&amp;quot;-2^-43&amp;quot;,2.52435e-29,1.86265e-09],[&amp;quot;-2^-44&amp;quot;,1.26218e-29,1.86265e-09],[&amp;quot;-2^-45&amp;quot;,6.31089e-30,1.86265e-09],[&amp;quot;-2^-46&amp;quot;,3.15544e-30,1.86265e-09],[&amp;quot;-2^-47&amp;quot;,1.57772e-30,1.86265e-09],[&amp;quot;-2^-48&amp;quot;,7.88861e-31,1.86265e-09],[&amp;quot;-2^-49&amp;quot;,3.9443e-31,1.86265e-09],[&amp;quot;-2^-50&amp;quot;,1.97215e-31,1.86265e-09],[&amp;quot;-2^-51&amp;quot;,9.86076e-32,1.86265e-09],[&amp;quot;-2^-52&amp;quot;,4.93038e-32,1.86265e-09],[&amp;quot;-2^-53&amp;quot;,2.46519e-32,1.86265e-09],[&amp;quot;-2^-54&amp;quot;,1.2326e-32,1.86265e-09],[&amp;quot;-2^-55&amp;quot;,0,0],[&amp;quot;-2^-56&amp;quot;,0,0],[&amp;quot;-2^-57&amp;quot;,0,0],[&amp;quot;-2^-58&amp;quot;,0,0],[&amp;quot;-2^-59&amp;quot;,0,0],[&amp;quot;-2^-60&amp;quot;,0,0],[&amp;quot;-2^-61&amp;quot;,0,0],[&amp;quot;-2^-62&amp;quot;,0,0],[&amp;quot;-2^-63&amp;quot;,0,0],[&amp;quot;-2^-64&amp;quot;,0,0],[&amp;quot;-2^-65&amp;quot;,0,0],[&amp;quot;-2^-66&amp;quot;,0,0],[&amp;quot;-2^-67&amp;quot;,0,0],[&amp;quot;-2^-68&amp;quot;,0,0],[&amp;quot;-2^-69&amp;quot;,0,0],[&amp;quot;-2^-70&amp;quot;,0,0],[&amp;quot;-2^-71&amp;quot;,0,0],[&amp;quot;-2^-72&amp;quot;,0,0],[&amp;quot;-2^-73&amp;quot;,0,0],[&amp;quot;-2^-74&amp;quot;,0,0],[&amp;quot;-2^-75&amp;quot;,0,0],[&amp;quot;-2^-76&amp;quot;,0,0],[&amp;quot;-2^-77&amp;quot;,0,0],[&amp;quot;-2^-78&amp;quot;,0,0],[&amp;quot;-2^-79&amp;quot;,0,0],[&amp;quot;-2^-80&amp;quot;,0,0],[&amp;quot;-2^-81&amp;quot;,0,0],[&amp;quot;-2^-82&amp;quot;,0,0],[&amp;quot;-2^-83&amp;quot;,0,0],[&amp;quot;-2^-84&amp;quot;,0,0],[&amp;quot;-2^-85&amp;quot;,0,0],[&amp;quot;-2^-86&amp;quot;,0,0],[&amp;quot;-2^-87&amp;quot;,0,0],[&amp;quot;-2^-88&amp;quot;,0,0],[&amp;quot;-2^-89&amp;quot;,0,0],[&amp;quot;-2^-90&amp;quot;,0,0],[&amp;quot;-2^-91&amp;quot;,0,0],[&amp;quot;-2^-92&amp;quot;,0,0],[&amp;quot;-2^-93&amp;quot;,0,0],[&amp;quot;-2^-94&amp;quot;,0,0],[&amp;quot;-2^-95&amp;quot;,0,0],[&amp;quot;-2^-96&amp;quot;,0,0],[&amp;quot;-2^-97&amp;quot;,0,0],[&amp;quot;-2^-98&amp;quot;,0,0],[&amp;quot;-2^-99&amp;quot;,0,0],[&amp;quot;-2^-100&amp;quot;,0,0],[&amp;quot;-2^-101&amp;quot;,0,0],[&amp;quot;-2^-102&amp;quot;,0,0],[&amp;quot;-2^-103&amp;quot;,0,0],[&amp;quot;-2^-104&amp;quot;,0,0],[&amp;quot;-2^-105&amp;quot;,0,0],[&amp;quot;-2^-106&amp;quot;,0,0],[&amp;quot;-2^-107&amp;quot;,0,0],[&amp;quot;-2^-108&amp;quot;,0,0],[&amp;quot;-2^-109&amp;quot;,0,0],[&amp;quot;-2^-110&amp;quot;,0,0],[&amp;quot;-2^-111&amp;quot;,0,0],[&amp;quot;-2^-112&amp;quot;,0,0],[&amp;quot;-2^-113&amp;quot;,0,0],[&amp;quot;-2^-114&amp;quot;,0,0],[&amp;quot;-2^-115&amp;quot;,0,0],[&amp;quot;-2^-116&amp;quot;,0,0],[&amp;quot;-2^-117&amp;quot;,0,0],[&amp;quot;-2^-118&amp;quot;,0,0],[&amp;quot;-2^-119&amp;quot;,0,0],[&amp;quot;-2^-120&amp;quot;,0,0],[&amp;quot;-2^-121&amp;quot;,0,0],[&amp;quot;-2^-122&amp;quot;,0,0],[&amp;quot;-2^-123&amp;quot;,0,0],[&amp;quot;-2^-124&amp;quot;,0,0],[&amp;quot;-2^-125&amp;quot;,0,0],[&amp;quot;-2^-126&amp;quot;,0,0],[&amp;quot;-SN&amp;quot;,0,0],[&amp;quot;0&amp;quot;,0,0],[&amp;quot;+SN&amp;quot;,0,0],[&amp;quot;+2^-126&amp;quot;,0,0],[&amp;quot;+2^-125&amp;quot;,0,0],[&amp;quot;+2^-124&amp;quot;,0,0],[&amp;quot;+2^-123&amp;quot;,0,0],[&amp;quot;+2^-122&amp;quot;,0,0],[&amp;quot;+2^-121&amp;quot;,0,0],[&amp;quot;+2^-120&amp;quot;,0,0],[&amp;quot;+2^-119&amp;quot;,0,0],[&amp;quot;+2^-118&amp;quot;,0,0],[&amp;quot;+2^-117&amp;quot;,0,0],[&amp;quot;+2^-116&amp;quot;,0,0],[&amp;quot;+2^-115&amp;quot;,0,0],[&amp;quot;+2^-114&amp;quot;,0,0],[&amp;quot;+2^-113&amp;quot;,0,0],[&amp;quot;+2^-112&amp;quot;,0,0],[&amp;quot;+2^-111&amp;quot;,0,0],[&amp;quot;+2^-110&amp;quot;,0,0],[&amp;quot;+2^-109&amp;quot;,0,0],[&amp;quot;+2^-108&amp;quot;,0,0],[&amp;quot;+2^-107&amp;quot;,0,0],[&amp;quot;+2^-106&amp;quot;,0,0],[&amp;quot;+2^-105&amp;quot;,0,0],[&amp;quot;+2^-104&amp;quot;,0,0],[&amp;quot;+2^-103&amp;quot;,0,0],[&amp;quot;+2^-102&amp;quot;,0,0],[&amp;quot;+2^-101&amp;quot;,0,0],[&amp;quot;+2^-100&amp;quot;,0,0],[&amp;quot;+2^-99&amp;quot;,0,0],[&amp;quot;+2^-98&amp;quot;,0,0],[&amp;quot;+2^-97&amp;quot;,0,0],[&amp;quot;+2^-96&amp;quot;,0,0],[&amp;quot;+2^-95&amp;quot;,0,0],[&amp;quot;+2^-94&amp;quot;,0,0],[&amp;quot;+2^-93&amp;quot;,0,0],[&amp;quot;+2^-92&amp;quot;,0,0],[&amp;quot;+2^-91&amp;quot;,0,0],[&amp;quot;+2^-90&amp;quot;,0,0],[&amp;quot;+2^-89&amp;quot;,0,0],[&amp;quot;+2^-88&amp;quot;,0,0],[&amp;quot;+2^-87&amp;quot;,0,0],[&amp;quot;+2^-86&amp;quot;,0,0],[&amp;quot;+2^-85&amp;quot;,0,0],[&amp;quot;+2^-84&amp;quot;,0,0],[&amp;quot;+2^-83&amp;quot;,0,0],[&amp;quot;+2^-82&amp;quot;,0,0],[&amp;quot;+2^-81&amp;quot;,0,0],[&amp;quot;+2^-80&amp;quot;,0,0],[&amp;quot;+2^-79&amp;quot;,0,0],[&amp;quot;+2^-78&amp;quot;,0,0],[&amp;quot;+2^-77&amp;quot;,0,0],[&amp;quot;+2^-76&amp;quot;,0,0],[&amp;quot;+2^-75&amp;quot;,0,0],[&amp;quot;+2^-74&amp;quot;,0,0],[&amp;quot;+2^-73&amp;quot;,0,0],[&amp;quot;+2^-72&amp;quot;,0,0],[&amp;quot;+2^-71&amp;quot;,0,0],[&amp;quot;+2^-70&amp;quot;,0,0],[&amp;quot;+2^-69&amp;quot;,0,0],[&amp;quot;+2^-68&amp;quot;,0,0],[&amp;quot;+2^-67&amp;quot;,0,0],[&amp;quot;+2^-66&amp;quot;,0,0],[&amp;quot;+2^-65&amp;quot;,0,0],[&amp;quot;+2^-64&amp;quot;,0,0],[&amp;quot;+2^-63&amp;quot;,0,0],[&amp;quot;+2^-62&amp;quot;,0,0],[&amp;quot;+2^-61&amp;quot;,0,0],[&amp;quot;+2^-60&amp;quot;,0,0],[&amp;quot;+2^-59&amp;quot;,0,0],[&amp;quot;+2^-58&amp;quot;,0,0],[&amp;quot;+2^-57&amp;quot;,0,0],[&amp;quot;+2^-56&amp;quot;,0,0],[&amp;quot;+2^-55&amp;quot;,0,0],[&amp;quot;+2^-54&amp;quot;,1.2326e-32,1.86265e-09],[&amp;quot;+2^-53&amp;quot;,2.46519e-32,1.86265e-09],[&amp;quot;+2^-52&amp;quot;,4.93038e-32,1.86265e-09],[&amp;quot;+2^-51&amp;quot;,9.86076e-32,1.86265e-09],[&amp;quot;+2^-50&amp;quot;,1.97215e-31,1.86265e-09],[&amp;quot;+2^-49&amp;quot;,3.9443e-31,1.86265e-09],[&amp;quot;+2^-48&amp;quot;,7.88861e-31,1.86265e-09],[&amp;quot;+2^-47&amp;quot;,1.57772e-30,1.86265e-09],[&amp;quot;+2^-46&amp;quot;,3.15544e-30,1.86265e-09],[&amp;quot;+2^-45&amp;quot;,6.31089e-30,1.86265e-09],[&amp;quot;+2^-44&amp;quot;,1.26218e-29,1.86265e-09],[&amp;quot;+2^-43&amp;quot;,2.52435e-29,1.86265e-09],[&amp;quot;+2^-42&amp;quot;,5.04871e-29,1.86265e-09],[&amp;quot;+2^-41&amp;quot;,1.00974e-28,1.86265e-09],[&amp;quot;+2^-40&amp;quot;,2.01948e-28,1.86265e-09],[&amp;quot;+2^-39&amp;quot;,4.03897e-28,1.86265e-09],[&amp;quot;+2^-38&amp;quot;,8.07794e-28,1.86265e-09],[&amp;quot;+2^-37&amp;quot;,1.61559e-27,1.86265e-09],[&amp;quot;+2^-36&amp;quot;,3.23117e-27,1.86265e-09],[&amp;quot;+2^-35&amp;quot;,6.46235e-27,1.86265e-09],[&amp;quot;+2^-34&amp;quot;,1.29247e-26,1.86265e-09],[&amp;quot;+2^-33&amp;quot;,2.58494e-26,1.86265e-09],[&amp;quot;+2^-32&amp;quot;,5.16988e-26,1.86265e-09],[&amp;quot;+2^-31&amp;quot;,1.03398e-25,1.86265e-09],[&amp;quot;+2^-30&amp;quot;,2.06795e-25,1.86265e-09],[&amp;quot;+2^-29&amp;quot;,4.1359e-25,1.86265e-09],[&amp;quot;+2^-28&amp;quot;,8.27181e-25,1.86265e-09],[&amp;quot;+2^-27&amp;quot;,1.65436e-24,1.86265e-09],[&amp;quot;+2^-26&amp;quot;,9.92617e-24,5.58794e-09],[&amp;quot;+2^-25&amp;quot;,7.27919e-23,2.04891e-08],[&amp;quot;+2^-24&amp;quot;,5.691e-22,8.00937e-08],[&amp;quot;+2^-23&amp;quot;,4.52633e-21,3.18512e-07],[&amp;quot;+2^-22&amp;quot;,3.61577e-20,1.27219e-06],[&amp;quot;+2^-21&amp;quot;,2.89156e-19,5.08688e-06],[&amp;quot;+2^-20&amp;quot;,2.31304e-18,2.03457e-05],[&amp;quot;+2^-19&amp;quot;,1.85039e-17,8.13808e-05],[&amp;quot;+2^-18&amp;quot;,1.48029e-16,0.00032552],[&amp;quot;+2^-17&amp;quot;,1.18424e-15,0.00130208],[&amp;quot;+2^-16&amp;quot;,9.47387e-15,0.00520832],[&amp;quot;+2^-15&amp;quot;,7.5791e-14,0.0208333],[&amp;quot;+2^-14&amp;quot;,6.06328e-13,0.0833331],[&amp;quot;+2^-13&amp;quot;,4.85062e-12,0.333332],[&amp;quot;+2^-12&amp;quot;,1.45519e-11,0.5],[&amp;quot;+2^-11&amp;quot;,2.91037e-11,0.499998],[&amp;quot;+2^-10&amp;quot;,5.82082e-11,0.500004],[&amp;quot;+2^-9&amp;quot;,1.16423e-10,0.500033],[&amp;quot;+2^-8&amp;quot;,2.32917e-10,0.500185],[&amp;quot;+2^-7&amp;quot;,4.66402e-10,0.500795],[&amp;quot;+2^-6&amp;quot;,9.51472e-10,0.510818],[&amp;quot;+2^-5&amp;quot;,3.89213e-09,1.04478],[&amp;quot;+2^-4&amp;quot;,2.57737e-07,34.5929],[&amp;quot;+2^-3&amp;quot;,3.11475e-05,2090.28],[&amp;quot;+2^-2&amp;quot;,0.00318735,106950],[&amp;quot;+2^-1&amp;quot;,0.127635,2.14135e+06],[&amp;quot;+2^0&amp;quot;,0.940715,1.57826e+07],[&amp;quot;+2^1&amp;quot;,2.94633,4.94312e+07],[&amp;quot;+2^2&amp;quot;,6.96672,1.16882e+08],[&amp;quot;+2^3&amp;quot;,14.9718,1.33874e+08],[&amp;quot;+2^4&amp;quot;,30.9629,2.59736e+08],[&amp;quot;+2^5&amp;quot;,62.9354,5.27941e+08],[&amp;quot;+2^6&amp;quot;,126.876,1.06431e+09],[&amp;quot;+2^7&amp;quot;,254.754,2.13703e+09],[&amp;quot;+2^8&amp;quot;,510.509,4.28246e+09],[&amp;quot;+2^9&amp;quot;,1022.02,8.57331e+09],[&amp;quot;+2^10&amp;quot;,2045.04,1.7155e+10],[&amp;quot;+2^11&amp;quot;,4091.07,3.43184e+10],[&amp;quot;+2^12&amp;quot;,8183.15,6.86452e+10],[&amp;quot;+2^13&amp;quot;,16367.3,1.37299e+11],[&amp;quot;+2^14&amp;quot;,32735.6,2.74606e+11],[&amp;quot;+2^15&amp;quot;,65472.2,5.4922e+11],[&amp;quot;+2^16&amp;quot;,130945,1.09845e+12],[&amp;quot;+2^17&amp;quot;,261892,2.19691e+12],[&amp;quot;+2^18&amp;quot;,523784,4.39382e+12],[&amp;quot;+2^19&amp;quot;,1.04757e+06,8.78765e+12],[&amp;quot;+2^20&amp;quot;,2.09514e+06,1.75753e+13],[&amp;quot;+2^21&amp;quot;,4.19028e+06,3.51506e+13],[&amp;quot;+2^22&amp;quot;,8.38056e+06,7.03013e+13],[&amp;quot;+2^23&amp;quot;,1.67611e+07,1.40603e+14],[&amp;quot;+2^24&amp;quot;,3.35223e+07,2.81205e+14],[&amp;quot;+2^25&amp;quot;,6.70445e+07,5.6241e+14],[&amp;quot;+2^26&amp;quot;,1.34089e+08,1.12482e+15],[&amp;quot;+2^27&amp;quot;,2.68178e+08,2.24964e+15],[&amp;quot;+2^28&amp;quot;,5.36356e+08,4.49928e+15],[&amp;quot;+2^29&amp;quot;,1.07271e+09,8.99856e+15],[&amp;quot;+2^30&amp;quot;,2.14542e+09,1.79971e+16],[&amp;quot;+2^31&amp;quot;,4.29085e+09,3.59943e+16],[&amp;quot;+2^32&amp;quot;,8.58993e+09,7.20575e+16],[&amp;quot;+2^33&amp;quot;,1.71799e+10,1.44115e+17],[&amp;quot;+2^34&amp;quot;,3.43597e+10,2.8823e+17],[&amp;quot;+2^35&amp;quot;,6.87194e+10,5.7646e+17],[&amp;quot;+2^36&amp;quot;,1.37439e+11,1.15292e+18],[&amp;quot;+2^37&amp;quot;,2.74878e+11,2.30584e+18],[&amp;quot;+2^38&amp;quot;,5.49755e+11,4.61168e+18],[&amp;quot;+2^39&amp;quot;,1.09951e+12,9.22336e+18],[&amp;quot;+2^40&amp;quot;,2.19902e+12,1.84467e+19],[&amp;quot;+2^41&amp;quot;,4.39804e+12,3.68935e+19],[&amp;quot;+2^42&amp;quot;,8.79608e+12,7.37869e+19],[&amp;quot;+2^43&amp;quot;,1.75922e+13,1.47574e+20],[&amp;quot;+2^44&amp;quot;,3.51843e+13,2.95148e+20],[&amp;quot;+2^45&amp;quot;,7.03687e+13,5.90295e+20],[&amp;quot;+2^46&amp;quot;,1.40737e+14,1.18059e+21],[&amp;quot;+2^47&amp;quot;,2.81475e+14,2.36118e+21],[&amp;quot;+2^48&amp;quot;,5.62949e+14,4.72236e+21],[&amp;quot;+2^49&amp;quot;,1.1259e+15,9.44472e+21],[&amp;quot;+2^50&amp;quot;,2.2518e+15,1.88894e+22],[&amp;quot;+2^51&amp;quot;,4.5036e+15,3.77789e+22],[&amp;quot;+2^52&amp;quot;,9.00719e+15,7.55578e+22],[&amp;quot;+2^53&amp;quot;,1.80144e+16,1.51116e+23],[&amp;quot;+2^54&amp;quot;,3.60288e+16,3.02231e+23],[&amp;quot;+2^55&amp;quot;,7.20575e+16,6.04462e+23],[&amp;quot;+2^56&amp;quot;,1.44115e+17,1.20892e+24],[&amp;quot;+2^57&amp;quot;,2.8823e+17,2.41785e+24],[&amp;quot;+2^58&amp;quot;,5.7646e+17,4.8357e+24],[&amp;quot;+2^59&amp;quot;,1.15292e+18,9.6714e+24],[&amp;quot;+2^60&amp;quot;,2.30584e+18,1.93428e+25],[&amp;quot;+2^61&amp;quot;,4.61168e+18,3.86856e+25],[&amp;quot;+2^62&amp;quot;,9.22336e+18,7.73712e+25],[&amp;quot;+2^63&amp;quot;,1.84467e+19,1.54742e+26],[&amp;quot;+2^64&amp;quot;,0,0],[&amp;quot;+2^65&amp;quot;,0,0],[&amp;quot;+2^66&amp;quot;,0,0],[&amp;quot;+2^67&amp;quot;,0,0],[&amp;quot;+2^68&amp;quot;,0,0],[&amp;quot;+2^69&amp;quot;,0,0],[&amp;quot;+2^70&amp;quot;,0,0],[&amp;quot;+2^71&amp;quot;,0,0],[&amp;quot;+2^72&amp;quot;,0,0],[&amp;quot;+2^73&amp;quot;,0,0],[&amp;quot;+2^74&amp;quot;,0,0],[&amp;quot;+2^75&amp;quot;,0,0],[&amp;quot;+2^76&amp;quot;,0,0],[&amp;quot;+2^77&amp;quot;,0,0],[&amp;quot;+2^78&amp;quot;,0,0],[&amp;quot;+2^79&amp;quot;,0,0],[&amp;quot;+2^80&amp;quot;,0,0],[&amp;quot;+2^81&amp;quot;,0,0],[&amp;quot;+2^82&amp;quot;,0,0],[&amp;quot;+2^83&amp;quot;,0,0],[&amp;quot;+2^84&amp;quot;,0,0],[&amp;quot;+2^85&amp;quot;,0,0],[&amp;quot;+2^86&amp;quot;,0,0],[&amp;quot;+2^87&amp;quot;,0,0],[&amp;quot;+2^88&amp;quot;,0,0],[&amp;quot;+2^89&amp;quot;,0,0],[&amp;quot;+2^90&amp;quot;,0,0],[&amp;quot;+2^91&amp;quot;,0,0],[&amp;quot;+2^92&amp;quot;,0,0],[&amp;quot;+2^93&amp;quot;,0,0],[&amp;quot;+2^94&amp;quot;,0,0],[&amp;quot;+2^95&amp;quot;,0,0],[&amp;quot;+2^96&amp;quot;,0,0],[&amp;quot;+2^97&amp;quot;,0,0],[&amp;quot;+2^98&amp;quot;,0,0],[&amp;quot;+2^99&amp;quot;,0,0],[&amp;quot;+2^100&amp;quot;,0,0],[&amp;quot;+2^101&amp;quot;,0,0],[&amp;quot;+2^102&amp;quot;,0,0],[&amp;quot;+2^103&amp;quot;,0,0],[&amp;quot;+2^104&amp;quot;,0,0],[&amp;quot;+2^105&amp;quot;,0,0],[&amp;quot;+2^106&amp;quot;,0,0],[&amp;quot;+2^107&amp;quot;,0,0],[&amp;quot;+2^108&amp;quot;,0,0],[&amp;quot;+2^109&amp;quot;,0,0],[&amp;quot;+2^110&amp;quot;,0,0],[&amp;quot;+2^111&amp;quot;,0,0],[&amp;quot;+2^112&amp;quot;,0,0],[&amp;quot;+2^113&amp;quot;,0,0],[&amp;quot;+2^114&amp;quot;,0,0],[&amp;quot;+2^115&amp;quot;,0,0],[&amp;quot;+2^116&amp;quot;,0,0],[&amp;quot;+2^117&amp;quot;,0,0],[&amp;quot;+2^118&amp;quot;,0,0],[&amp;quot;+2^119&amp;quot;,0,0],[&amp;quot;+2^120&amp;quot;,0,0],[&amp;quot;+2^121&amp;quot;,0,0],[&amp;quot;+2^122&amp;quot;,0,0],[&amp;quot;+2^123&amp;quot;,0,0],[&amp;quot;+2^124&amp;quot;,0,0],[&amp;quot;+2^125&amp;quot;,0,0],[&amp;quot;+2^126&amp;quot;,0,0],[&amp;quot;+2^127&amp;quot;,0,0] ]&lt;br /&gt;
 }}&lt;br /&gt;
&lt;br /&gt;
== Localized Approximation Functions ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
!Id&lt;br /&gt;
!Formula&lt;br /&gt;
!ULP &amp;lt;= 2.0&lt;br /&gt;
!NAN&lt;br /&gt;
|-&lt;br /&gt;
|signum&lt;br /&gt;
|&amp;lt;math&amp;gt;\sgn{x}&amp;lt;/math&amp;gt;&lt;br /&gt;
| -inf &amp;lt; x &amp;lt; -8.31763&lt;br /&gt;
8.31763 &amp;lt; x &amp;lt; inf&lt;br /&gt;
| -&lt;br /&gt;
|-&lt;br /&gt;
|poly1&lt;br /&gt;
|&amp;lt;math&amp;gt;x&amp;lt;/math&amp;gt;&lt;br /&gt;
| -0.000704229 &amp;lt; x &amp;lt; 0.000704229&lt;br /&gt;
| -&lt;br /&gt;
|-&lt;br /&gt;
|poly3&lt;br /&gt;
|&amp;lt;math&amp;gt;x - \frac{1}{3} x^3&amp;lt;/math&amp;gt;&lt;br /&gt;
| -0.0291781 &amp;lt; x &amp;lt; 0.0291781 &lt;br /&gt;
| -&lt;br /&gt;
|-&lt;br /&gt;
|lampert7&lt;br /&gt;
|&amp;lt;math&amp;gt;\frac{x \cdot (135135 + x^2 \cdot (17325 + x^2 \cdot (378 + x^2)))}{135135 + x^2 \cdot (62370 + x^2 \cdot (3150 + 28 \cdot x^2))}&lt;br /&gt;
&amp;lt;/math&amp;gt;&lt;br /&gt;
| -0.000947416 &amp;lt; x &amp;lt; 0.000947207&lt;br /&gt;
| -inf &amp;lt; x &amp;lt; -1.51629e+06&lt;br /&gt;
1.51629e+06 &amp;lt; x &amp;lt; inf&lt;br /&gt;
|-&lt;br /&gt;
|pade&lt;br /&gt;
|&amp;lt;math&amp;gt;x + x \cdot \frac{x^2 \cdot \left( p_1 \cdot x^2 + p_0 \right)}{\left( x^2 + q_1 \right) \cdot x^2 + q_0} &lt;br /&gt;
&lt;br /&gt;
&amp;lt;/math&amp;gt; &amp;lt;math&amp;gt;\begin{align}&lt;br /&gt;
p_0 &amp;amp;= -0.2059432032 \\&lt;br /&gt;
p_1 &amp;amp;= -0.0009577527 \\&lt;br /&gt;
q_0 &amp;amp;= 0.6178299136 \\ &lt;br /&gt;
q_1 &amp;amp;= 0.25&lt;br /&gt;
\end{align}&lt;br /&gt;
&amp;lt;/math&amp;gt;&lt;br /&gt;
| -0.0016973 &amp;lt; x &amp;lt; 0.0016973 &lt;br /&gt;
| -inf &amp;lt; x &amp;lt; -6.14896e+18&lt;br /&gt;
6.14896e+18 &amp;lt; x &amp;lt; inf&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== External Links ==&lt;br /&gt;
&lt;br /&gt;
* https://yaikhom.com/2020-04-28-localised-approximation-of-hyperbolic-tangents.html#mjx-eqn-eqn7-th%20degree%20lambert%20approximant&lt;br /&gt;
*https://github.com/llvm/llvm-project/blob/main/libc/src/math/generic/tanhf.cpp&lt;br /&gt;
*https://forums.developer.nvidia.com/t/hardware-accelerated-tanh-on-turing/173291&lt;br /&gt;
[[Category:Numerical Precision]]&lt;/div&gt;</summary>
		<author><name>Timo.stripf</name></author>
	</entry>
	<entry>
		<id>https://www.emmtrix.com/w139/index.php?title=tanh_Software_Implementation&amp;diff=2927</id>
		<title>tanh Software Implementation</title>
		<link rel="alternate" type="text/html" href="https://www.emmtrix.com/w139/index.php?title=tanh_Software_Implementation&amp;diff=2927"/>
		<updated>2026-02-04T11:51:20Z</updated>

		<summary type="html">&lt;p&gt;Timo.stripf: /* Problem Overview */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;[[File:Hyperbolic Tangent.svg|thumb]]&lt;br /&gt;
The hyperbolic tangent function, commonly referred to as &amp;lt;code&amp;gt;tanh&amp;lt;/code&amp;gt;, is a mathematical function that maps real numbers to the range &amp;lt;math&amp;gt;(-1, 1)&amp;lt;/math&amp;gt;. It is defined as:&lt;br /&gt;
&lt;br /&gt;
:&amp;lt;math&amp;gt;\tanh(x) = \frac{e^x - e^{-x}}{e^x + e^{-x}}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
From a software implementation perspective, &amp;lt;code&amp;gt;tanh&amp;lt;/code&amp;gt; is a numerically challenging function due to the use of exponential functions, which can easily overflow for large inputs and suffer from catastrophic cancellation for very small values of &amp;lt;math&amp;gt;x&amp;lt;/math&amp;gt;. These issues are particularly relevant in single-precision floating-point (&amp;lt;code&amp;gt;float32&amp;lt;/code&amp;gt;) implementations, where both accuracy and performance are critical.&lt;br /&gt;
&lt;br /&gt;
In machine learning and deep learning, &amp;lt;code&amp;gt;tanh&amp;lt;/code&amp;gt; is widely used as an activation function because it introduces non-linearity and produces outputs centered around zero, which can improve gradient-based optimization. As a result, &amp;lt;code&amp;gt;tanh&amp;lt;/code&amp;gt; is often evaluated billions of times during training and inference, making efficient and numerically stable implementations essential.&lt;br /&gt;
&lt;br /&gt;
This article focuses on practical software implementations of &amp;lt;code&amp;gt;tanh&amp;lt;/code&amp;gt;, analyzing different computational formulations with respect to floating-point accuracy, ULP error, overflow behavior, and NaN generation. The goal is to provide a foundation for designing robust &amp;lt;code&amp;gt;tanh&amp;lt;/code&amp;gt; implementations suitable for performance-critical environments such as numerical libraries, embedded systems, and AI workloads.&lt;br /&gt;
&lt;br /&gt;
== Problem Overview ==&lt;br /&gt;
The hyperbolic tangent function poses several challenges when implemented in finite-precision floating-point arithmetic. While the mathematical definition is simple, direct translations into software often suffer from numerical instability and performance issues.&lt;br /&gt;
&lt;br /&gt;
A primary challenge is the use of exponential functions. For large input values, &amp;lt;code&amp;gt;exp(x)&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;exp(-x)&amp;lt;/code&amp;gt; can overflow in single-precision floating-point arithmetic, leading to infinities and, in some formulations, NaN results. Avoiding overflow therefore requires careful reformulation or explicit range handling.&lt;br /&gt;
&lt;br /&gt;
For very small input values, catastrophic cancellation becomes the dominant issue. Expressions such as &amp;lt;math&amp;gt;e^x - e^{-x}&amp;lt;/math&amp;gt; involve the subtraction of nearly equal numbers, causing a severe loss of significant bits and large ULP errors. In these regions, naive implementations may return zero or highly inaccurate results even though &amp;lt;math&amp;gt;\tanh(x) \approx x&amp;lt;/math&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
In addition to accuracy concerns, performance is a critical factor. In many applications, especially machine learning and numerical simulations, &amp;lt;code&amp;gt;tanh&amp;lt;/code&amp;gt; is evaluated extremely frequently. This makes it necessary to balance numerical robustness with the cost of transcendental function calls, branch complexity, and instruction-level efficiency.&lt;br /&gt;
==Direct Exponential Formulations==&lt;br /&gt;
==Numerical Issues and Error Metrics==&lt;br /&gt;
== Simple Approximation with Exponential Math Function ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+&lt;br /&gt;
!Id&lt;br /&gt;
!Formula&lt;br /&gt;
!Highest ULP Error&lt;br /&gt;
!ULP &amp;lt;= 2.0&lt;br /&gt;
!NaN&lt;br /&gt;
|-&lt;br /&gt;
|exp_v1&lt;br /&gt;
|&amp;lt;math&amp;gt;\frac{e^x - e^{-x}}{e^x + e^{-x}}&amp;lt;/math&amp;gt;&lt;br /&gt;
|x &amp;lt; 0: 8.55638e+08 at -2.98023e-08 with 0x0p+0 instead of -0x1p-25&lt;br /&gt;
&lt;br /&gt;
x &amp;gt; 0: 8.55638e+08 at 2.98023e-08 with 0x0p+0 instead of 0x1p-25&lt;br /&gt;
| -88.7246 &amp;lt; x &amp;lt; -7.14197&lt;br /&gt;
7.14197 &amp;lt; x &amp;lt; 88.7246&lt;br /&gt;
| -inf &amp;lt; x &amp;lt; -88.7246&lt;br /&gt;
88.7246 &amp;lt; x &amp;lt; inf&lt;br /&gt;
|-&lt;br /&gt;
|exp_v2&lt;br /&gt;
|&amp;lt;math&amp;gt;\frac{e^{2x} - 1}{e^{2x} + 1}&amp;lt;/math&amp;gt;&lt;br /&gt;
|x &amp;lt; 0: 8.47249e+08 at -1.49012e-08 with 0x0p+0 instead of -0x1p-26&lt;br /&gt;
x &amp;gt; 0: 8.55638e+08 at 2.98023e-08 with 0x0p+0 instead of 0x1.fffffep-26&lt;br /&gt;
| -inf &amp;lt; x &amp;lt; -0.547821&lt;br /&gt;
0.254822 &amp;lt; x &amp;lt; 8.31787&lt;br /&gt;
&lt;br /&gt;
8.38379 &amp;lt; x &amp;lt; 44.3623&lt;br /&gt;
|44.3623 &amp;lt; x &amp;lt; inf&lt;br /&gt;
|-&lt;br /&gt;
|exp_v3&lt;br /&gt;
|&amp;lt;math&amp;gt;1 - \frac{2}{e^{2x} + 1}&amp;lt;/math&amp;gt;&lt;br /&gt;
|x &amp;lt; 0: 8.59832e+08 at -4.47035e-08 with 0x0p+0 instead of -0x1.8p-25&lt;br /&gt;
x &amp;gt; 0: 8.68221e+08 at 8.9407e-08 with 0x0p+0 instead of 0x1.7ffffcp-24&lt;br /&gt;
| -inf &amp;lt; x &amp;lt; -6.26233&lt;br /&gt;
0.346382 &amp;lt; x &amp;lt; inf&lt;br /&gt;
| -&lt;br /&gt;
|-&lt;br /&gt;
|expm1&lt;br /&gt;
|&amp;lt;math&amp;gt;\frac{expm1(2x)}{expm1(2x) + 2}&amp;lt;/math&amp;gt; &lt;br /&gt;
|x &amp;lt; 0: 2.49585 at -3.95627 with -0x1.ffa00ap-1 instead of -0x1.ffa00ep-1&lt;br /&gt;
x &amp;gt; 0: 2.41657 at 0.0155837 with 0x1.fe9b66p-7 instead of 0x1.fe9b62p-7&lt;br /&gt;
| -inf &amp;lt; x &amp;lt; -5.57227&lt;br /&gt;
-0.42981 &amp;lt; x &amp;lt; 0.00777948&lt;br /&gt;
&lt;br /&gt;
0.125351 &amp;lt; x &amp;lt; 44.3623&lt;br /&gt;
|44.3623 &amp;lt; x &amp;lt; inf&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
The first equation is the definition of the hyperbolic tangent function. This equation requires two exponentials with different values. Since exponentials are expensive to compute, we can use the second equation to reduce the number of exponentials to one.&lt;br /&gt;
&lt;br /&gt;
The second equation has the drawback that both the numerator and the denominator can become infinity for large values of x which results in NaN. That happens for &amp;lt;math&amp;gt;|x| &amp;gt; \log(FLT\_MAX)/2 \approx 44&amp;lt;/math&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
The third equation solves this problem but has a slightly higher error for &amp;lt;math&amp;gt;x \approx 2^{-25}&amp;lt;/math&amp;gt;. In the following graph you can see the error chart for equation 3. The error charts for the other two equations are very similar. All three equations have an error of up to 16 mio ULPs for &amp;lt;math&amp;gt;x &amp;lt; 2^{-5}&amp;lt;/math&amp;gt; which means that up to 24 bits could be wrong.&lt;br /&gt;
&lt;br /&gt;
=== exp_v2 ===&lt;br /&gt;
&lt;br /&gt;
{{#widget:FloatErrorGraph|id=tanhf_exp_v2&lt;br /&gt;
  |data=[ [&amp;quot;-2^127&amp;quot;,0,1.17549e-38],[&amp;quot;-2^126&amp;quot;,0,1.17549e-38],[&amp;quot;-2^125&amp;quot;,0,1.17549e-38],[&amp;quot;-2^124&amp;quot;,0,1.17549e-38],[&amp;quot;-2^123&amp;quot;,0,1.17549e-38],[&amp;quot;-2^122&amp;quot;,0,1.17549e-38],[&amp;quot;-2^121&amp;quot;,0,1.17549e-38],[&amp;quot;-2^120&amp;quot;,0,1.17549e-38],[&amp;quot;-2^119&amp;quot;,0,1.17549e-38],[&amp;quot;-2^118&amp;quot;,0,1.17549e-38],[&amp;quot;-2^117&amp;quot;,0,1.17549e-38],[&amp;quot;-2^116&amp;quot;,0,1.17549e-38],[&amp;quot;-2^115&amp;quot;,0,1.17549e-38],[&amp;quot;-2^114&amp;quot;,0,1.17549e-38],[&amp;quot;-2^113&amp;quot;,0,1.17549e-38],[&amp;quot;-2^112&amp;quot;,0,1.17549e-38],[&amp;quot;-2^111&amp;quot;,0,1.17549e-38],[&amp;quot;-2^110&amp;quot;,0,1.17549e-38],[&amp;quot;-2^109&amp;quot;,0,1.17549e-38],[&amp;quot;-2^108&amp;quot;,0,1.17549e-38],[&amp;quot;-2^107&amp;quot;,0,1.17549e-38],[&amp;quot;-2^106&amp;quot;,0,1.17549e-38],[&amp;quot;-2^105&amp;quot;,0,1.17549e-38],[&amp;quot;-2^104&amp;quot;,0,1.17549e-38],[&amp;quot;-2^103&amp;quot;,0,1.17549e-38],[&amp;quot;-2^102&amp;quot;,0,1.17549e-38],[&amp;quot;-2^101&amp;quot;,0,1.17549e-38],[&amp;quot;-2^100&amp;quot;,0,1.17549e-38],[&amp;quot;-2^99&amp;quot;,0,1.17549e-38],[&amp;quot;-2^98&amp;quot;,0,1.17549e-38],[&amp;quot;-2^97&amp;quot;,0,1.17549e-38],[&amp;quot;-2^96&amp;quot;,0,1.17549e-38],[&amp;quot;-2^95&amp;quot;,0,1.17549e-38],[&amp;quot;-2^94&amp;quot;,0,1.17549e-38],[&amp;quot;-2^93&amp;quot;,0,1.17549e-38],[&amp;quot;-2^92&amp;quot;,0,1.17549e-38],[&amp;quot;-2^91&amp;quot;,0,1.17549e-38],[&amp;quot;-2^90&amp;quot;,0,1.17549e-38],[&amp;quot;-2^89&amp;quot;,0,1.17549e-38],[&amp;quot;-2^88&amp;quot;,0,1.17549e-38],[&amp;quot;-2^87&amp;quot;,0,1.17549e-38],[&amp;quot;-2^86&amp;quot;,0,1.17549e-38],[&amp;quot;-2^85&amp;quot;,0,1.17549e-38],[&amp;quot;-2^84&amp;quot;,0,1.17549e-38],[&amp;quot;-2^83&amp;quot;,0,1.17549e-38],[&amp;quot;-2^82&amp;quot;,0,1.17549e-38],[&amp;quot;-2^81&amp;quot;,0,1.17549e-38],[&amp;quot;-2^80&amp;quot;,0,1.17549e-38],[&amp;quot;-2^79&amp;quot;,0,1.17549e-38],[&amp;quot;-2^78&amp;quot;,0,1.17549e-38],[&amp;quot;-2^77&amp;quot;,0,1.17549e-38],[&amp;quot;-2^76&amp;quot;,0,1.17549e-38],[&amp;quot;-2^75&amp;quot;,0,1.17549e-38],[&amp;quot;-2^74&amp;quot;,0,1.17549e-38],[&amp;quot;-2^73&amp;quot;,0,1.17549e-38],[&amp;quot;-2^72&amp;quot;,0,1.17549e-38],[&amp;quot;-2^71&amp;quot;,0,1.17549e-38],[&amp;quot;-2^70&amp;quot;,0,1.17549e-38],[&amp;quot;-2^69&amp;quot;,0,1.17549e-38],[&amp;quot;-2^68&amp;quot;,0,1.17549e-38],[&amp;quot;-2^67&amp;quot;,0,1.17549e-38],[&amp;quot;-2^66&amp;quot;,0,1.17549e-38],[&amp;quot;-2^65&amp;quot;,0,1.17549e-38],[&amp;quot;-2^64&amp;quot;,0,1.17549e-38],[&amp;quot;-2^63&amp;quot;,0,1.17549e-38],[&amp;quot;-2^62&amp;quot;,0,1.17549e-38],[&amp;quot;-2^61&amp;quot;,0,1.17549e-38],[&amp;quot;-2^60&amp;quot;,0,1.17549e-38],[&amp;quot;-2^59&amp;quot;,0,1.17549e-38],[&amp;quot;-2^58&amp;quot;,0,1.17549e-38],[&amp;quot;-2^57&amp;quot;,0,1.17549e-38],[&amp;quot;-2^56&amp;quot;,0,1.17549e-38],[&amp;quot;-2^55&amp;quot;,0,1.17549e-38],[&amp;quot;-2^54&amp;quot;,0,1.17549e-38],[&amp;quot;-2^53&amp;quot;,0,1.17549e-38],[&amp;quot;-2^52&amp;quot;,0,1.17549e-38],[&amp;quot;-2^51&amp;quot;,0,1.17549e-38],[&amp;quot;-2^50&amp;quot;,0,1.17549e-38],[&amp;quot;-2^49&amp;quot;,0,1.17549e-38],[&amp;quot;-2^48&amp;quot;,0,1.17549e-38],[&amp;quot;-2^47&amp;quot;,0,1.17549e-38],[&amp;quot;-2^46&amp;quot;,0,1.17549e-38],[&amp;quot;-2^45&amp;quot;,0,1.17549e-38],[&amp;quot;-2^44&amp;quot;,0,1.17549e-38],[&amp;quot;-2^43&amp;quot;,0,1.17549e-38],[&amp;quot;-2^42&amp;quot;,0,1.17549e-38],[&amp;quot;-2^41&amp;quot;,0,1.17549e-38],[&amp;quot;-2^40&amp;quot;,0,1.17549e-38],[&amp;quot;-2^39&amp;quot;,0,1.17549e-38],[&amp;quot;-2^38&amp;quot;,0,1.17549e-38],[&amp;quot;-2^37&amp;quot;,0,1.17549e-38],[&amp;quot;-2^36&amp;quot;,0,1.17549e-38],[&amp;quot;-2^35&amp;quot;,0,1.17549e-38],[&amp;quot;-2^34&amp;quot;,0,1.17549e-38],[&amp;quot;-2^33&amp;quot;,0,1.17549e-38],[&amp;quot;-2^32&amp;quot;,0,1.17549e-38],[&amp;quot;-2^31&amp;quot;,0,1.17549e-38],[&amp;quot;-2^30&amp;quot;,0,1.17549e-38],[&amp;quot;-2^29&amp;quot;,0,1.17549e-38],[&amp;quot;-2^28&amp;quot;,0,1.17549e-38],[&amp;quot;-2^27&amp;quot;,0,1.17549e-38],[&amp;quot;-2^26&amp;quot;,0,1.17549e-38],[&amp;quot;-2^25&amp;quot;,0,1.17549e-38],[&amp;quot;-2^24&amp;quot;,0,1.17549e-38],[&amp;quot;-2^23&amp;quot;,0,1.17549e-38],[&amp;quot;-2^22&amp;quot;,0,1.17549e-38],[&amp;quot;-2^21&amp;quot;,0,1.17549e-38],[&amp;quot;-2^20&amp;quot;,0,1.17549e-38],[&amp;quot;-2^19&amp;quot;,0,1.17549e-38],[&amp;quot;-2^18&amp;quot;,0,1.17549e-38],[&amp;quot;-2^17&amp;quot;,0,1.17549e-38],[&amp;quot;-2^16&amp;quot;,0,1.17549e-38],[&amp;quot;-2^15&amp;quot;,0,1.17549e-38],[&amp;quot;-2^14&amp;quot;,0,1.17549e-38],[&amp;quot;-2^13&amp;quot;,0,1.17549e-38],[&amp;quot;-2^12&amp;quot;,0,1.17549e-38],[&amp;quot;-2^11&amp;quot;,0,1.17549e-38],[&amp;quot;-2^10&amp;quot;,0,1.17549e-38],[&amp;quot;-2^9&amp;quot;,0,1.17549e-38],[&amp;quot;-2^8&amp;quot;,0,1.17549e-38],[&amp;quot;-2^7&amp;quot;,0,1.17549e-38],[&amp;quot;-2^6&amp;quot;,0,1.17549e-38],[&amp;quot;-2^5&amp;quot;,0,1.17549e-38],[&amp;quot;-2^4&amp;quot;,2.53131e-14,4.24683e-07],[&amp;quot;-2^3&amp;quot;,5.95888e-08,0.999734],[&amp;quot;-2^2&amp;quot;,8.86696e-08,1.48763],[&amp;quot;-2^1&amp;quot;,8.91609e-08,1.49587],[&amp;quot;-2^0&amp;quot;,8.73743e-08,1.4659],[&amp;quot;-2^-1&amp;quot;,8.65382e-08,2.13222],[&amp;quot;-2^-2&amp;quot;,6.18481e-08,2.61428],[&amp;quot;-2^-3&amp;quot;,3.88071e-08,3.08594],[&amp;quot;-2^-4&amp;quot;,2.6234e-08,4.23594],[&amp;quot;-2^-5&amp;quot;,2.02799e-08,8.19726],[&amp;quot;-2^-6&amp;quot;,1.75183e-08,12.4495],[&amp;quot;-2^-7&amp;quot;,1.61937e-08,17.3878],[&amp;quot;-2^-8&amp;quot;,1.5566e-08,33.4278],[&amp;quot;-2^-9&amp;quot;,1.51688e-08,65.1495],[&amp;quot;-2^-10&amp;quot;,1.50281e-08,129.09],[&amp;quot;-2^-11&amp;quot;,1.49613e-08,257.033],[&amp;quot;-2^-12&amp;quot;,1.49296e-08,512.977],[&amp;quot;-2^-13&amp;quot;,1.49174e-08,1025.12],[&amp;quot;-2^-14&amp;quot;,1.4909e-08,2049.08],[&amp;quot;-2^-15&amp;quot;,1.49049e-08,4097.02],[&amp;quot;-2^-16&amp;quot;,1.4903e-08,8193],[&amp;quot;-2^-17&amp;quot;,1.49021e-08,16385],[&amp;quot;-2^-18&amp;quot;,1.48862e-08,32735],[&amp;quot;-2^-19&amp;quot;,1.48973e-08,65519],[&amp;quot;-2^-20&amp;quot;,1.49001e-08,131063],[&amp;quot;-2^-21&amp;quot;,1.49009e-08,262139],[&amp;quot;-2^-22&amp;quot;,1.49011e-08,524285],[&amp;quot;-2^-23&amp;quot;,1.49011e-08,1.04857e+06],[&amp;quot;-2^-24&amp;quot;,1.49011e-08,2.09715e+06],[&amp;quot;-2^-25&amp;quot;,1.49012e-08,4.1943e+06],[&amp;quot;-2^-26&amp;quot;,1.49007e-08,8.38835e+06],[&amp;quot;-2^-27&amp;quot;,1.49012e-08,8.47249e+08],[&amp;quot;-2^-28&amp;quot;,7.45058e-09,8.38861e+08],[&amp;quot;-2^-29&amp;quot;,3.72529e-09,8.30472e+08],[&amp;quot;-2^-30&amp;quot;,1.86265e-09,8.22084e+08],[&amp;quot;-2^-31&amp;quot;,9.31323e-10,8.13695e+08],[&amp;quot;-2^-32&amp;quot;,4.65661e-10,8.05306e+08],[&amp;quot;-2^-33&amp;quot;,2.32831e-10,7.96918e+08],[&amp;quot;-2^-34&amp;quot;,1.16415e-10,7.88529e+08],[&amp;quot;-2^-35&amp;quot;,5.82077e-11,7.80141e+08],[&amp;quot;-2^-36&amp;quot;,2.91038e-11,7.71752e+08],[&amp;quot;-2^-37&amp;quot;,1.45519e-11,7.63363e+08],[&amp;quot;-2^-38&amp;quot;,7.27596e-12,7.54975e+08],[&amp;quot;-2^-39&amp;quot;,3.63798e-12,7.46586e+08],[&amp;quot;-2^-40&amp;quot;,1.81899e-12,7.38198e+08],[&amp;quot;-2^-41&amp;quot;,9.09495e-13,7.29809e+08],[&amp;quot;-2^-42&amp;quot;,4.54747e-13,7.2142e+08],[&amp;quot;-2^-43&amp;quot;,2.27374e-13,7.13032e+08],[&amp;quot;-2^-44&amp;quot;,1.13687e-13,7.04643e+08],[&amp;quot;-2^-45&amp;quot;,5.68434e-14,6.96254e+08],[&amp;quot;-2^-46&amp;quot;,2.84217e-14,6.87866e+08],[&amp;quot;-2^-47&amp;quot;,1.42109e-14,6.79477e+08],[&amp;quot;-2^-48&amp;quot;,7.10543e-15,6.71089e+08],[&amp;quot;-2^-49&amp;quot;,3.55271e-15,6.627e+08],[&amp;quot;-2^-50&amp;quot;,1.77636e-15,6.54311e+08],[&amp;quot;-2^-51&amp;quot;,8.88178e-16,6.45923e+08],[&amp;quot;-2^-52&amp;quot;,4.44089e-16,6.37534e+08],[&amp;quot;-2^-53&amp;quot;,2.22045e-16,6.29146e+08],[&amp;quot;-2^-54&amp;quot;,1.11022e-16,6.20757e+08],[&amp;quot;-2^-55&amp;quot;,5.55111e-17,6.12368e+08],[&amp;quot;-2^-56&amp;quot;,2.77556e-17,6.0398e+08],[&amp;quot;-2^-57&amp;quot;,1.38778e-17,5.95591e+08],[&amp;quot;-2^-58&amp;quot;,6.93889e-18,5.87203e+08],[&amp;quot;-2^-59&amp;quot;,3.46945e-18,5.78814e+08],[&amp;quot;-2^-60&amp;quot;,1.73472e-18,5.70425e+08],[&amp;quot;-2^-61&amp;quot;,8.67362e-19,5.62037e+08],[&amp;quot;-2^-62&amp;quot;,4.33681e-19,5.53648e+08],[&amp;quot;-2^-63&amp;quot;,2.1684e-19,5.4526e+08],[&amp;quot;-2^-64&amp;quot;,1.0842e-19,5.36871e+08],[&amp;quot;-2^-65&amp;quot;,5.42101e-20,5.28482e+08],[&amp;quot;-2^-66&amp;quot;,2.71051e-20,5.20094e+08],[&amp;quot;-2^-67&amp;quot;,1.35525e-20,5.11705e+08],[&amp;quot;-2^-68&amp;quot;,6.77626e-21,5.03316e+08],[&amp;quot;-2^-69&amp;quot;,3.38813e-21,4.94928e+08],[&amp;quot;-2^-70&amp;quot;,1.69407e-21,4.86539e+08],[&amp;quot;-2^-71&amp;quot;,8.47033e-22,4.78151e+08],[&amp;quot;-2^-72&amp;quot;,4.23516e-22,4.69762e+08],[&amp;quot;-2^-73&amp;quot;,2.11758e-22,4.61373e+08],[&amp;quot;-2^-74&amp;quot;,1.05879e-22,4.52985e+08],[&amp;quot;-2^-75&amp;quot;,5.29396e-23,4.44596e+08],[&amp;quot;-2^-76&amp;quot;,2.64698e-23,4.36208e+08],[&amp;quot;-2^-77&amp;quot;,1.32349e-23,4.27819e+08],[&amp;quot;-2^-78&amp;quot;,6.61744e-24,4.1943e+08],[&amp;quot;-2^-79&amp;quot;,3.30872e-24,4.11042e+08],[&amp;quot;-2^-80&amp;quot;,1.65436e-24,4.02653e+08],[&amp;quot;-2^-81&amp;quot;,8.27181e-25,3.94265e+08],[&amp;quot;-2^-82&amp;quot;,4.1359e-25,3.85876e+08],[&amp;quot;-2^-83&amp;quot;,2.06795e-25,3.77487e+08],[&amp;quot;-2^-84&amp;quot;,1.03398e-25,3.69099e+08],[&amp;quot;-2^-85&amp;quot;,5.16988e-26,3.6071e+08],[&amp;quot;-2^-86&amp;quot;,2.58494e-26,3.52322e+08],[&amp;quot;-2^-87&amp;quot;,1.29247e-26,3.43933e+08],[&amp;quot;-2^-88&amp;quot;,6.46235e-27,3.35544e+08],[&amp;quot;-2^-89&amp;quot;,3.23117e-27,3.27156e+08],[&amp;quot;-2^-90&amp;quot;,1.61559e-27,3.18767e+08],[&amp;quot;-2^-91&amp;quot;,8.07794e-28,3.10378e+08],[&amp;quot;-2^-92&amp;quot;,4.03897e-28,3.0199e+08],[&amp;quot;-2^-93&amp;quot;,2.01948e-28,2.93601e+08],[&amp;quot;-2^-94&amp;quot;,1.00974e-28,2.85213e+08],[&amp;quot;-2^-95&amp;quot;,5.04871e-29,2.76824e+08],[&amp;quot;-2^-96&amp;quot;,2.52435e-29,2.68435e+08],[&amp;quot;-2^-97&amp;quot;,1.26218e-29,2.60047e+08],[&amp;quot;-2^-98&amp;quot;,6.31089e-30,2.51658e+08],[&amp;quot;-2^-99&amp;quot;,3.15544e-30,2.4327e+08],[&amp;quot;-2^-100&amp;quot;,1.57772e-30,2.34881e+08],[&amp;quot;-2^-101&amp;quot;,7.88861e-31,2.26492e+08],[&amp;quot;-2^-102&amp;quot;,3.9443e-31,2.18104e+08],[&amp;quot;-2^-103&amp;quot;,1.97215e-31,2.09715e+08],[&amp;quot;-2^-104&amp;quot;,9.86076e-32,2.01327e+08],[&amp;quot;-2^-105&amp;quot;,4.93038e-32,1.92938e+08],[&amp;quot;-2^-106&amp;quot;,2.46519e-32,1.84549e+08],[&amp;quot;-2^-107&amp;quot;,1.2326e-32,1.76161e+08],[&amp;quot;-2^-108&amp;quot;,6.16298e-33,1.67772e+08],[&amp;quot;-2^-109&amp;quot;,3.08149e-33,1.59384e+08],[&amp;quot;-2^-110&amp;quot;,1.54074e-33,1.50995e+08],[&amp;quot;-2^-111&amp;quot;,7.70372e-34,1.42606e+08],[&amp;quot;-2^-112&amp;quot;,3.85186e-34,1.34218e+08],[&amp;quot;-2^-113&amp;quot;,1.92593e-34,1.25829e+08],[&amp;quot;-2^-114&amp;quot;,9.62965e-35,1.17441e+08],[&amp;quot;-2^-115&amp;quot;,4.81482e-35,1.09052e+08],[&amp;quot;-2^-116&amp;quot;,2.40741e-35,1.00663e+08],[&amp;quot;-2^-117&amp;quot;,1.20371e-35,9.22747e+07],[&amp;quot;-2^-118&amp;quot;,6.01853e-36,8.38861e+07],[&amp;quot;-2^-119&amp;quot;,3.00927e-36,7.54975e+07],[&amp;quot;-2^-120&amp;quot;,1.50463e-36,6.71089e+07],[&amp;quot;-2^-121&amp;quot;,7.52316e-37,5.87203e+07],[&amp;quot;-2^-122&amp;quot;,3.76158e-37,5.03316e+07],[&amp;quot;-2^-123&amp;quot;,1.88079e-37,4.1943e+07],[&amp;quot;-2^-124&amp;quot;,9.40395e-38,3.35544e+07],[&amp;quot;-2^-125&amp;quot;,4.70198e-38,2.51658e+07],[&amp;quot;-2^-126&amp;quot;,2.35099e-38,1.67772e+07],[&amp;quot;-SN&amp;quot;,1.17546e-38,8.38835e+06],[&amp;quot;0&amp;quot;,0,1.17549e-38],[&amp;quot;+SN&amp;quot;,1.17546e-38,8.38835e+06],[&amp;quot;+2^-126&amp;quot;,2.35095e-38,1.6777e+07],[&amp;quot;+2^-125&amp;quot;,4.70191e-38,2.51656e+07],[&amp;quot;+2^-124&amp;quot;,9.40381e-38,3.35542e+07],[&amp;quot;+2^-123&amp;quot;,1.88076e-37,4.19428e+07],[&amp;quot;+2^-122&amp;quot;,3.76152e-37,5.03314e+07],[&amp;quot;+2^-121&amp;quot;,7.52305e-37,5.872e+07],[&amp;quot;+2^-120&amp;quot;,1.50461e-36,6.71086e+07],[&amp;quot;+2^-119&amp;quot;,3.00922e-36,7.54972e+07],[&amp;quot;+2^-118&amp;quot;,6.01844e-36,8.38858e+07],[&amp;quot;+2^-117&amp;quot;,1.20369e-35,9.22744e+07],[&amp;quot;+2^-116&amp;quot;,2.40738e-35,1.00663e+08],[&amp;quot;+2^-115&amp;quot;,4.81475e-35,1.09052e+08],[&amp;quot;+2^-114&amp;quot;,9.6295e-35,1.1744e+08],[&amp;quot;+2^-113&amp;quot;,1.9259e-34,1.25829e+08],[&amp;quot;+2^-112&amp;quot;,3.8518e-34,1.34217e+08],[&amp;quot;+2^-111&amp;quot;,7.7036e-34,1.42606e+08],[&amp;quot;+2^-110&amp;quot;,1.54072e-33,1.50995e+08],[&amp;quot;+2^-109&amp;quot;,3.08144e-33,1.59383e+08],[&amp;quot;+2^-108&amp;quot;,6.16288e-33,1.67772e+08],[&amp;quot;+2^-107&amp;quot;,1.23258e-32,1.76161e+08],[&amp;quot;+2^-106&amp;quot;,2.46515e-32,1.84549e+08],[&amp;quot;+2^-105&amp;quot;,4.93031e-32,1.92938e+08],[&amp;quot;+2^-104&amp;quot;,9.86061e-32,2.01326e+08],[&amp;quot;+2^-103&amp;quot;,1.97212e-31,2.09715e+08],[&amp;quot;+2^-102&amp;quot;,3.94424e-31,2.18104e+08],[&amp;quot;+2^-101&amp;quot;,7.88849e-31,2.26492e+08],[&amp;quot;+2^-100&amp;quot;,1.5777e-30,2.34881e+08],[&amp;quot;+2^-99&amp;quot;,3.1554e-30,2.43269e+08],[&amp;quot;+2^-98&amp;quot;,6.31079e-30,2.51658e+08],[&amp;quot;+2^-97&amp;quot;,1.26216e-29,2.60047e+08],[&amp;quot;+2^-96&amp;quot;,2.52432e-29,2.68435e+08],[&amp;quot;+2^-95&amp;quot;,5.04863e-29,2.76824e+08],[&amp;quot;+2^-94&amp;quot;,1.00973e-28,2.85212e+08],[&amp;quot;+2^-93&amp;quot;,2.01945e-28,2.93601e+08],[&amp;quot;+2^-92&amp;quot;,4.03891e-28,3.0199e+08],[&amp;quot;+2^-91&amp;quot;,8.07781e-28,3.10378e+08],[&amp;quot;+2^-90&amp;quot;,1.61556e-27,3.18767e+08],[&amp;quot;+2^-89&amp;quot;,3.23112e-27,3.27155e+08],[&amp;quot;+2^-88&amp;quot;,6.46225e-27,3.35544e+08],[&amp;quot;+2^-87&amp;quot;,1.29245e-26,3.43933e+08],[&amp;quot;+2^-86&amp;quot;,2.5849e-26,3.52321e+08],[&amp;quot;+2^-85&amp;quot;,5.1698e-26,3.6071e+08],[&amp;quot;+2^-84&amp;quot;,1.03396e-25,3.69098e+08],[&amp;quot;+2^-83&amp;quot;,2.06792e-25,3.77487e+08],[&amp;quot;+2^-82&amp;quot;,4.13584e-25,3.85876e+08],[&amp;quot;+2^-81&amp;quot;,8.27168e-25,3.94264e+08],[&amp;quot;+2^-80&amp;quot;,1.65434e-24,4.02653e+08],[&amp;quot;+2^-79&amp;quot;,3.30867e-24,4.11042e+08],[&amp;quot;+2^-78&amp;quot;,6.61734e-24,4.1943e+08],[&amp;quot;+2^-77&amp;quot;,1.32347e-23,4.27819e+08],[&amp;quot;+2^-76&amp;quot;,2.64694e-23,4.36207e+08],[&amp;quot;+2^-75&amp;quot;,5.29388e-23,4.44596e+08],[&amp;quot;+2^-74&amp;quot;,1.05878e-22,4.52985e+08],[&amp;quot;+2^-73&amp;quot;,2.11755e-22,4.61373e+08],[&amp;quot;+2^-72&amp;quot;,4.2351e-22,4.69762e+08],[&amp;quot;+2^-71&amp;quot;,8.4702e-22,4.7815e+08],[&amp;quot;+2^-70&amp;quot;,1.69404e-21,4.86539e+08],[&amp;quot;+2^-69&amp;quot;,3.38808e-21,4.94928e+08],[&amp;quot;+2^-68&amp;quot;,6.77616e-21,5.03316e+08],[&amp;quot;+2^-67&amp;quot;,1.35523e-20,5.11705e+08],[&amp;quot;+2^-66&amp;quot;,2.71046e-20,5.20093e+08],[&amp;quot;+2^-65&amp;quot;,5.42093e-20,5.28482e+08],[&amp;quot;+2^-64&amp;quot;,1.08419e-19,5.36871e+08],[&amp;quot;+2^-63&amp;quot;,2.16837e-19,5.45259e+08],[&amp;quot;+2^-62&amp;quot;,4.33674e-19,5.53648e+08],[&amp;quot;+2^-61&amp;quot;,8.67349e-19,5.62036e+08],[&amp;quot;+2^-60&amp;quot;,1.7347e-18,5.70425e+08],[&amp;quot;+2^-59&amp;quot;,3.46939e-18,5.78814e+08],[&amp;quot;+2^-58&amp;quot;,6.93879e-18,5.87202e+08],[&amp;quot;+2^-57&amp;quot;,1.38776e-17,5.95591e+08],[&amp;quot;+2^-56&amp;quot;,2.77552e-17,6.0398e+08],[&amp;quot;+2^-55&amp;quot;,5.55103e-17,6.12368e+08],[&amp;quot;+2^-54&amp;quot;,1.11021e-16,6.20757e+08],[&amp;quot;+2^-53&amp;quot;,2.22041e-16,6.29145e+08],[&amp;quot;+2^-52&amp;quot;,4.44082e-16,6.37534e+08],[&amp;quot;+2^-51&amp;quot;,8.88165e-16,6.45923e+08],[&amp;quot;+2^-50&amp;quot;,1.77633e-15,6.54311e+08],[&amp;quot;+2^-49&amp;quot;,3.55266e-15,6.627e+08],[&amp;quot;+2^-48&amp;quot;,7.10532e-15,6.71088e+08],[&amp;quot;+2^-47&amp;quot;,1.42106e-14,6.79477e+08],[&amp;quot;+2^-46&amp;quot;,2.84213e-14,6.87866e+08],[&amp;quot;+2^-45&amp;quot;,5.68426e-14,6.96254e+08],[&amp;quot;+2^-44&amp;quot;,1.13685e-13,7.04643e+08],[&amp;quot;+2^-43&amp;quot;,2.2737e-13,7.13031e+08],[&amp;quot;+2^-42&amp;quot;,4.5474e-13,7.2142e+08],[&amp;quot;+2^-41&amp;quot;,9.09481e-13,7.29809e+08],[&amp;quot;+2^-40&amp;quot;,1.81896e-12,7.38197e+08],[&amp;quot;+2^-39&amp;quot;,3.63792e-12,7.46586e+08],[&amp;quot;+2^-38&amp;quot;,7.27585e-12,7.54974e+08],[&amp;quot;+2^-37&amp;quot;,1.45517e-11,7.63363e+08],[&amp;quot;+2^-36&amp;quot;,2.91034e-11,7.71752e+08],[&amp;quot;+2^-35&amp;quot;,5.82068e-11,7.8014e+08],[&amp;quot;+2^-34&amp;quot;,1.16414e-10,7.88529e+08],[&amp;quot;+2^-33&amp;quot;,2.32827e-10,7.96918e+08],[&amp;quot;+2^-32&amp;quot;,4.65654e-10,8.05306e+08],[&amp;quot;+2^-31&amp;quot;,9.31308e-10,8.13695e+08],[&amp;quot;+2^-30&amp;quot;,1.86262e-09,8.22083e+08],[&amp;quot;+2^-29&amp;quot;,3.72523e-09,8.30472e+08],[&amp;quot;+2^-28&amp;quot;,7.45047e-09,8.38861e+08],[&amp;quot;+2^-27&amp;quot;,1.49009e-08,8.47249e+08],[&amp;quot;+2^-26&amp;quot;,2.98019e-08,8.55638e+08],[&amp;quot;+2^-25&amp;quot;,2.98023e-08,8.38861e+06],[&amp;quot;+2^-24&amp;quot;,2.98023e-08,4.1943e+06],[&amp;quot;+2^-23&amp;quot;,2.98023e-08,2.09715e+06],[&amp;quot;+2^-22&amp;quot;,2.98023e-08,1.04857e+06],[&amp;quot;+2^-21&amp;quot;,2.98021e-08,524284],[&amp;quot;+2^-20&amp;quot;,2.98014e-08,262136],[&amp;quot;+2^-19&amp;quot;,2.97987e-08,131056],[&amp;quot;+2^-18&amp;quot;,2.97878e-08,65504],[&amp;quot;+2^-17&amp;quot;,2.98014e-08,32767],[&amp;quot;+2^-16&amp;quot;,2.98005e-08,16384],[&amp;quot;+2^-15&amp;quot;,2.98023e-08,8192],[&amp;quot;+2^-14&amp;quot;,2.98025e-08,4351.99],[&amp;quot;+2^-13&amp;quot;,2.98044e-08,2815.96],[&amp;quot;+2^-12&amp;quot;,2.98091e-08,1024.23],[&amp;quot;+2^-11&amp;quot;,2.9812e-08,512.167],[&amp;quot;+2^-10&amp;quot;,2.98154e-08,256.113],[&amp;quot;+2^-9&amp;quot;,2.98591e-08,128.244],[&amp;quot;+2^-8&amp;quot;,3.00204e-08,85.3328],[&amp;quot;+2^-7&amp;quot;,3.01947e-08,43.675],[&amp;quot;+2^-6&amp;quot;,3.05713e-08,30.45],[&amp;quot;+2^-5&amp;quot;,3.14319e-08,8.43743],[&amp;quot;+2^-4&amp;quot;,3.31291e-08,7.77058],[&amp;quot;+2^-3&amp;quot;,3.58597e-08,3.99659],[&amp;quot;+2^-2&amp;quot;,4.14639e-08,2.29246],[&amp;quot;+2^-1&amp;quot;,8.02409e-08,1.34622],[&amp;quot;+2^0&amp;quot;,8.66201e-08,1.45324],[&amp;quot;+2^1&amp;quot;,8.8654e-08,1.48737],[&amp;quot;+2^2&amp;quot;,6.7976e-08,1.14045],[&amp;quot;+2^3&amp;quot;,1.33955e-07,2.2474],[&amp;quot;+2^4&amp;quot;,2.53131e-14,4.24683e-07],[&amp;quot;+2^5&amp;quot;,0,null],[&amp;quot;+2^6&amp;quot;,0,null],[&amp;quot;+2^7&amp;quot;,0,null],[&amp;quot;+2^8&amp;quot;,0,null],[&amp;quot;+2^9&amp;quot;,0,null],[&amp;quot;+2^10&amp;quot;,0,null],[&amp;quot;+2^11&amp;quot;,0,null],[&amp;quot;+2^12&amp;quot;,0,null],[&amp;quot;+2^13&amp;quot;,0,null],[&amp;quot;+2^14&amp;quot;,0,null],[&amp;quot;+2^15&amp;quot;,0,null],[&amp;quot;+2^16&amp;quot;,0,null],[&amp;quot;+2^17&amp;quot;,0,null],[&amp;quot;+2^18&amp;quot;,0,null],[&amp;quot;+2^19&amp;quot;,0,null],[&amp;quot;+2^20&amp;quot;,0,null],[&amp;quot;+2^21&amp;quot;,0,null],[&amp;quot;+2^22&amp;quot;,0,null],[&amp;quot;+2^23&amp;quot;,0,null],[&amp;quot;+2^24&amp;quot;,0,null],[&amp;quot;+2^25&amp;quot;,0,null],[&amp;quot;+2^26&amp;quot;,0,null],[&amp;quot;+2^27&amp;quot;,0,null],[&amp;quot;+2^28&amp;quot;,0,null],[&amp;quot;+2^29&amp;quot;,0,null],[&amp;quot;+2^30&amp;quot;,0,null],[&amp;quot;+2^31&amp;quot;,0,null],[&amp;quot;+2^32&amp;quot;,0,null],[&amp;quot;+2^33&amp;quot;,0,null],[&amp;quot;+2^34&amp;quot;,0,null],[&amp;quot;+2^35&amp;quot;,0,null],[&amp;quot;+2^36&amp;quot;,0,null],[&amp;quot;+2^37&amp;quot;,0,null],[&amp;quot;+2^38&amp;quot;,0,null],[&amp;quot;+2^39&amp;quot;,0,null],[&amp;quot;+2^40&amp;quot;,0,null],[&amp;quot;+2^41&amp;quot;,0,null],[&amp;quot;+2^42&amp;quot;,0,null],[&amp;quot;+2^43&amp;quot;,0,null],[&amp;quot;+2^44&amp;quot;,0,null],[&amp;quot;+2^45&amp;quot;,0,null],[&amp;quot;+2^46&amp;quot;,0,null],[&amp;quot;+2^47&amp;quot;,0,null],[&amp;quot;+2^48&amp;quot;,0,null],[&amp;quot;+2^49&amp;quot;,0,null],[&amp;quot;+2^50&amp;quot;,0,null],[&amp;quot;+2^51&amp;quot;,0,null],[&amp;quot;+2^52&amp;quot;,0,null],[&amp;quot;+2^53&amp;quot;,0,null],[&amp;quot;+2^54&amp;quot;,0,null],[&amp;quot;+2^55&amp;quot;,0,null],[&amp;quot;+2^56&amp;quot;,0,null],[&amp;quot;+2^57&amp;quot;,0,null],[&amp;quot;+2^58&amp;quot;,0,null],[&amp;quot;+2^59&amp;quot;,0,null],[&amp;quot;+2^60&amp;quot;,0,null],[&amp;quot;+2^61&amp;quot;,0,null],[&amp;quot;+2^62&amp;quot;,0,null],[&amp;quot;+2^63&amp;quot;,0,null],[&amp;quot;+2^64&amp;quot;,0,null],[&amp;quot;+2^65&amp;quot;,0,null],[&amp;quot;+2^66&amp;quot;,0,null],[&amp;quot;+2^67&amp;quot;,0,null],[&amp;quot;+2^68&amp;quot;,0,null],[&amp;quot;+2^69&amp;quot;,0,null],[&amp;quot;+2^70&amp;quot;,0,null],[&amp;quot;+2^71&amp;quot;,0,null],[&amp;quot;+2^72&amp;quot;,0,null],[&amp;quot;+2^73&amp;quot;,0,null],[&amp;quot;+2^74&amp;quot;,0,null],[&amp;quot;+2^75&amp;quot;,0,null],[&amp;quot;+2^76&amp;quot;,0,null],[&amp;quot;+2^77&amp;quot;,0,null],[&amp;quot;+2^78&amp;quot;,0,null],[&amp;quot;+2^79&amp;quot;,0,null],[&amp;quot;+2^80&amp;quot;,0,null],[&amp;quot;+2^81&amp;quot;,0,null],[&amp;quot;+2^82&amp;quot;,0,null],[&amp;quot;+2^83&amp;quot;,0,null],[&amp;quot;+2^84&amp;quot;,0,null],[&amp;quot;+2^85&amp;quot;,0,null],[&amp;quot;+2^86&amp;quot;,0,null],[&amp;quot;+2^87&amp;quot;,0,null],[&amp;quot;+2^88&amp;quot;,0,null],[&amp;quot;+2^89&amp;quot;,0,null],[&amp;quot;+2^90&amp;quot;,0,null],[&amp;quot;+2^91&amp;quot;,0,null],[&amp;quot;+2^92&amp;quot;,0,null],[&amp;quot;+2^93&amp;quot;,0,null],[&amp;quot;+2^94&amp;quot;,0,null],[&amp;quot;+2^95&amp;quot;,0,null],[&amp;quot;+2^96&amp;quot;,0,null],[&amp;quot;+2^97&amp;quot;,0,null],[&amp;quot;+2^98&amp;quot;,0,null],[&amp;quot;+2^99&amp;quot;,0,null],[&amp;quot;+2^100&amp;quot;,0,null],[&amp;quot;+2^101&amp;quot;,0,null],[&amp;quot;+2^102&amp;quot;,0,null],[&amp;quot;+2^103&amp;quot;,0,null],[&amp;quot;+2^104&amp;quot;,0,null],[&amp;quot;+2^105&amp;quot;,0,null],[&amp;quot;+2^106&amp;quot;,0,null],[&amp;quot;+2^107&amp;quot;,0,null],[&amp;quot;+2^108&amp;quot;,0,null],[&amp;quot;+2^109&amp;quot;,0,null],[&amp;quot;+2^110&amp;quot;,0,null],[&amp;quot;+2^111&amp;quot;,0,null],[&amp;quot;+2^112&amp;quot;,0,null],[&amp;quot;+2^113&amp;quot;,0,null],[&amp;quot;+2^114&amp;quot;,0,null],[&amp;quot;+2^115&amp;quot;,0,null],[&amp;quot;+2^116&amp;quot;,0,null],[&amp;quot;+2^117&amp;quot;,0,null],[&amp;quot;+2^118&amp;quot;,0,null],[&amp;quot;+2^119&amp;quot;,0,null],[&amp;quot;+2^120&amp;quot;,0,null],[&amp;quot;+2^121&amp;quot;,0,null],[&amp;quot;+2^122&amp;quot;,0,null],[&amp;quot;+2^123&amp;quot;,0,null],[&amp;quot;+2^124&amp;quot;,0,null],[&amp;quot;+2^125&amp;quot;,0,null],[&amp;quot;+2^126&amp;quot;,0,null],[&amp;quot;+2^127&amp;quot;,0,null] ]&lt;br /&gt;
 }}&lt;br /&gt;
=== expm1 ===&lt;br /&gt;
&lt;br /&gt;
{{#widget:FloatErrorGraph|id=tanhf_expm1&lt;br /&gt;
  |data=[ [&amp;quot;-2^127&amp;quot;,0,1.17549e-38],[&amp;quot;-2^126&amp;quot;,0,1.17549e-38],[&amp;quot;-2^125&amp;quot;,0,1.17549e-38],[&amp;quot;-2^124&amp;quot;,0,1.17549e-38],[&amp;quot;-2^123&amp;quot;,0,1.17549e-38],[&amp;quot;-2^122&amp;quot;,0,1.17549e-38],[&amp;quot;-2^121&amp;quot;,0,1.17549e-38],[&amp;quot;-2^120&amp;quot;,0,1.17549e-38],[&amp;quot;-2^119&amp;quot;,0,1.17549e-38],[&amp;quot;-2^118&amp;quot;,0,1.17549e-38],[&amp;quot;-2^117&amp;quot;,0,1.17549e-38],[&amp;quot;-2^116&amp;quot;,0,1.17549e-38],[&amp;quot;-2^115&amp;quot;,0,1.17549e-38],[&amp;quot;-2^114&amp;quot;,0,1.17549e-38],[&amp;quot;-2^113&amp;quot;,0,1.17549e-38],[&amp;quot;-2^112&amp;quot;,0,1.17549e-38],[&amp;quot;-2^111&amp;quot;,0,1.17549e-38],[&amp;quot;-2^110&amp;quot;,0,1.17549e-38],[&amp;quot;-2^109&amp;quot;,0,1.17549e-38],[&amp;quot;-2^108&amp;quot;,0,1.17549e-38],[&amp;quot;-2^107&amp;quot;,0,1.17549e-38],[&amp;quot;-2^106&amp;quot;,0,1.17549e-38],[&amp;quot;-2^105&amp;quot;,0,1.17549e-38],[&amp;quot;-2^104&amp;quot;,0,1.17549e-38],[&amp;quot;-2^103&amp;quot;,0,1.17549e-38],[&amp;quot;-2^102&amp;quot;,0,1.17549e-38],[&amp;quot;-2^101&amp;quot;,0,1.17549e-38],[&amp;quot;-2^100&amp;quot;,0,1.17549e-38],[&amp;quot;-2^99&amp;quot;,0,1.17549e-38],[&amp;quot;-2^98&amp;quot;,0,1.17549e-38],[&amp;quot;-2^97&amp;quot;,0,1.17549e-38],[&amp;quot;-2^96&amp;quot;,0,1.17549e-38],[&amp;quot;-2^95&amp;quot;,0,1.17549e-38],[&amp;quot;-2^94&amp;quot;,0,1.17549e-38],[&amp;quot;-2^93&amp;quot;,0,1.17549e-38],[&amp;quot;-2^92&amp;quot;,0,1.17549e-38],[&amp;quot;-2^91&amp;quot;,0,1.17549e-38],[&amp;quot;-2^90&amp;quot;,0,1.17549e-38],[&amp;quot;-2^89&amp;quot;,0,1.17549e-38],[&amp;quot;-2^88&amp;quot;,0,1.17549e-38],[&amp;quot;-2^87&amp;quot;,0,1.17549e-38],[&amp;quot;-2^86&amp;quot;,0,1.17549e-38],[&amp;quot;-2^85&amp;quot;,0,1.17549e-38],[&amp;quot;-2^84&amp;quot;,0,1.17549e-38],[&amp;quot;-2^83&amp;quot;,0,1.17549e-38],[&amp;quot;-2^82&amp;quot;,0,1.17549e-38],[&amp;quot;-2^81&amp;quot;,0,1.17549e-38],[&amp;quot;-2^80&amp;quot;,0,1.17549e-38],[&amp;quot;-2^79&amp;quot;,0,1.17549e-38],[&amp;quot;-2^78&amp;quot;,0,1.17549e-38],[&amp;quot;-2^77&amp;quot;,0,1.17549e-38],[&amp;quot;-2^76&amp;quot;,0,1.17549e-38],[&amp;quot;-2^75&amp;quot;,0,1.17549e-38],[&amp;quot;-2^74&amp;quot;,0,1.17549e-38],[&amp;quot;-2^73&amp;quot;,0,1.17549e-38],[&amp;quot;-2^72&amp;quot;,0,1.17549e-38],[&amp;quot;-2^71&amp;quot;,0,1.17549e-38],[&amp;quot;-2^70&amp;quot;,0,1.17549e-38],[&amp;quot;-2^69&amp;quot;,0,1.17549e-38],[&amp;quot;-2^68&amp;quot;,0,1.17549e-38],[&amp;quot;-2^67&amp;quot;,0,1.17549e-38],[&amp;quot;-2^66&amp;quot;,0,1.17549e-38],[&amp;quot;-2^65&amp;quot;,0,1.17549e-38],[&amp;quot;-2^64&amp;quot;,0,1.17549e-38],[&amp;quot;-2^63&amp;quot;,0,1.17549e-38],[&amp;quot;-2^62&amp;quot;,0,1.17549e-38],[&amp;quot;-2^61&amp;quot;,0,1.17549e-38],[&amp;quot;-2^60&amp;quot;,0,1.17549e-38],[&amp;quot;-2^59&amp;quot;,0,1.17549e-38],[&amp;quot;-2^58&amp;quot;,0,1.17549e-38],[&amp;quot;-2^57&amp;quot;,0,1.17549e-38],[&amp;quot;-2^56&amp;quot;,0,1.17549e-38],[&amp;quot;-2^55&amp;quot;,0,1.17549e-38],[&amp;quot;-2^54&amp;quot;,0,1.17549e-38],[&amp;quot;-2^53&amp;quot;,0,1.17549e-38],[&amp;quot;-2^52&amp;quot;,0,1.17549e-38],[&amp;quot;-2^51&amp;quot;,0,1.17549e-38],[&amp;quot;-2^50&amp;quot;,0,1.17549e-38],[&amp;quot;-2^49&amp;quot;,0,1.17549e-38],[&amp;quot;-2^48&amp;quot;,0,1.17549e-38],[&amp;quot;-2^47&amp;quot;,0,1.17549e-38],[&amp;quot;-2^46&amp;quot;,0,1.17549e-38],[&amp;quot;-2^45&amp;quot;,0,1.17549e-38],[&amp;quot;-2^44&amp;quot;,0,1.17549e-38],[&amp;quot;-2^43&amp;quot;,0,1.17549e-38],[&amp;quot;-2^42&amp;quot;,0,1.17549e-38],[&amp;quot;-2^41&amp;quot;,0,1.17549e-38],[&amp;quot;-2^40&amp;quot;,0,1.17549e-38],[&amp;quot;-2^39&amp;quot;,0,1.17549e-38],[&amp;quot;-2^38&amp;quot;,0,1.17549e-38],[&amp;quot;-2^37&amp;quot;,0,1.17549e-38],[&amp;quot;-2^36&amp;quot;,0,1.17549e-38],[&amp;quot;-2^35&amp;quot;,0,1.17549e-38],[&amp;quot;-2^34&amp;quot;,0,1.17549e-38],[&amp;quot;-2^33&amp;quot;,0,1.17549e-38],[&amp;quot;-2^32&amp;quot;,0,1.17549e-38],[&amp;quot;-2^31&amp;quot;,0,1.17549e-38],[&amp;quot;-2^30&amp;quot;,0,1.17549e-38],[&amp;quot;-2^29&amp;quot;,0,1.17549e-38],[&amp;quot;-2^28&amp;quot;,0,1.17549e-38],[&amp;quot;-2^27&amp;quot;,0,1.17549e-38],[&amp;quot;-2^26&amp;quot;,0,1.17549e-38],[&amp;quot;-2^25&amp;quot;,0,1.17549e-38],[&amp;quot;-2^24&amp;quot;,0,1.17549e-38],[&amp;quot;-2^23&amp;quot;,0,1.17549e-38],[&amp;quot;-2^22&amp;quot;,0,1.17549e-38],[&amp;quot;-2^21&amp;quot;,0,1.17549e-38],[&amp;quot;-2^20&amp;quot;,0,1.17549e-38],[&amp;quot;-2^19&amp;quot;,0,1.17549e-38],[&amp;quot;-2^18&amp;quot;,0,1.17549e-38],[&amp;quot;-2^17&amp;quot;,0,1.17549e-38],[&amp;quot;-2^16&amp;quot;,0,1.17549e-38],[&amp;quot;-2^15&amp;quot;,0,1.17549e-38],[&amp;quot;-2^14&amp;quot;,0,1.17549e-38],[&amp;quot;-2^13&amp;quot;,0,1.17549e-38],[&amp;quot;-2^12&amp;quot;,0,1.17549e-38],[&amp;quot;-2^11&amp;quot;,0,1.17549e-38],[&amp;quot;-2^10&amp;quot;,0,1.17549e-38],[&amp;quot;-2^9&amp;quot;,0,1.17549e-38],[&amp;quot;-2^8&amp;quot;,0,1.17549e-38],[&amp;quot;-2^7&amp;quot;,0,1.17549e-38],[&amp;quot;-2^6&amp;quot;,0,1.17549e-38],[&amp;quot;-2^5&amp;quot;,0,1.17549e-38],[&amp;quot;-2^4&amp;quot;,2.53131e-14,4.24683e-07],[&amp;quot;-2^3&amp;quot;,1.19138e-07,1.9988],[&amp;quot;-2^2&amp;quot;,1.46791e-07,2.46274],[&amp;quot;-2^1&amp;quot;,1.47723e-07,2.47839],[&amp;quot;-2^0&amp;quot;,1.40758e-07,2.36153],[&amp;quot;-2^-1&amp;quot;,1.13422e-07,2.23445],[&amp;quot;-2^-2&amp;quot;,6.38012e-08,2.14081],[&amp;quot;-2^-3&amp;quot;,2.69538e-08,1.80884],[&amp;quot;-2^-4&amp;quot;,1.21308e-08,1.62817],[&amp;quot;-2^-5&amp;quot;,5.59535e-09,1.50199],[&amp;quot;-2^-6&amp;quot;,2.69177e-09,1.44513],[&amp;quot;-2^-7&amp;quot;,1.31289e-09,1.4097],[&amp;quot;-2^-8&amp;quot;,6.49688e-10,1.3952],[&amp;quot;-2^-9&amp;quot;,3.32043e-10,1.42611],[&amp;quot;-2^-10&amp;quot;,1.66275e-10,1.4283],[&amp;quot;-2^-11&amp;quot;,8.08595e-11,1.38916],[&amp;quot;-2^-12&amp;quot;,3.8939e-11,1.33794],[&amp;quot;-2^-13&amp;quot;,1.9401e-11,1.33323],[&amp;quot;-2^-14&amp;quot;,7.88212e-12,1.08331],[&amp;quot;-2^-15&amp;quot;,3.71372e-12,1.02082],[&amp;quot;-2^-16&amp;quot;,1.82845e-12,1.0052],[&amp;quot;-2^-17&amp;quot;,9.10676e-13,1.0013],[&amp;quot;-2^-18&amp;quot;,4.54895e-13,1.00032],[&amp;quot;-2^-19&amp;quot;,2.27392e-13,1.00008],[&amp;quot;-2^-20&amp;quot;,1.13689e-13,1.00002],[&amp;quot;-2^-21&amp;quot;,5.68437e-14,1],[&amp;quot;-2^-22&amp;quot;,2.84217e-14,1],[&amp;quot;-2^-23&amp;quot;,1.42109e-14,1],[&amp;quot;-2^-24&amp;quot;,7.10543e-15,1],[&amp;quot;-2^-25&amp;quot;,3.55271e-15,1],[&amp;quot;-2^-26&amp;quot;,9.92617e-24,5.58794e-09],[&amp;quot;-2^-27&amp;quot;,1.65436e-24,1.86265e-09],[&amp;quot;-2^-28&amp;quot;,8.27181e-25,1.86265e-09],[&amp;quot;-2^-29&amp;quot;,4.1359e-25,1.86265e-09],[&amp;quot;-2^-30&amp;quot;,2.06795e-25,1.86265e-09],[&amp;quot;-2^-31&amp;quot;,1.03398e-25,1.86265e-09],[&amp;quot;-2^-32&amp;quot;,5.16988e-26,1.86265e-09],[&amp;quot;-2^-33&amp;quot;,2.58494e-26,1.86265e-09],[&amp;quot;-2^-34&amp;quot;,1.29247e-26,1.86265e-09],[&amp;quot;-2^-35&amp;quot;,6.46235e-27,1.86265e-09],[&amp;quot;-2^-36&amp;quot;,3.23117e-27,1.86265e-09],[&amp;quot;-2^-37&amp;quot;,1.61559e-27,1.86265e-09],[&amp;quot;-2^-38&amp;quot;,8.07794e-28,1.86265e-09],[&amp;quot;-2^-39&amp;quot;,4.03897e-28,1.86265e-09],[&amp;quot;-2^-40&amp;quot;,2.01948e-28,1.86265e-09],[&amp;quot;-2^-41&amp;quot;,1.00974e-28,1.86265e-09],[&amp;quot;-2^-42&amp;quot;,5.04871e-29,1.86265e-09],[&amp;quot;-2^-43&amp;quot;,2.52435e-29,1.86265e-09],[&amp;quot;-2^-44&amp;quot;,1.26218e-29,1.86265e-09],[&amp;quot;-2^-45&amp;quot;,6.31089e-30,1.86265e-09],[&amp;quot;-2^-46&amp;quot;,3.15544e-30,1.86265e-09],[&amp;quot;-2^-47&amp;quot;,1.57772e-30,1.86265e-09],[&amp;quot;-2^-48&amp;quot;,7.88861e-31,1.86265e-09],[&amp;quot;-2^-49&amp;quot;,3.9443e-31,1.86265e-09],[&amp;quot;-2^-50&amp;quot;,1.97215e-31,1.86265e-09],[&amp;quot;-2^-51&amp;quot;,9.86076e-32,1.86265e-09],[&amp;quot;-2^-52&amp;quot;,4.93038e-32,1.86265e-09],[&amp;quot;-2^-53&amp;quot;,2.46519e-32,1.86265e-09],[&amp;quot;-2^-54&amp;quot;,1.2326e-32,1.86265e-09],[&amp;quot;-2^-55&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-56&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-57&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-58&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-59&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-60&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-61&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-62&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-63&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-64&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-65&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-66&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-67&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-68&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-69&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-70&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-71&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-72&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-73&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-74&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-75&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-76&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-77&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-78&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-79&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-80&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-81&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-82&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-83&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-84&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-85&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-86&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-87&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-88&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-89&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-90&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-91&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-92&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-93&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-94&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-95&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-96&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-97&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-98&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-99&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-100&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-101&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-102&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-103&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-104&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-105&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-106&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-107&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-108&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-109&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-110&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-111&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-112&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-113&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-114&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-115&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-116&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-117&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-118&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-119&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-120&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-121&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-122&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-123&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-124&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-125&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-126&amp;quot;,0,1.17549e-38],[&amp;quot;-SN&amp;quot;,0,1.17549e-38],[&amp;quot;0&amp;quot;,0,1.17549e-38],[&amp;quot;+SN&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-126&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-125&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-124&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-123&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-122&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-121&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-120&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-119&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-118&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-117&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-116&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-115&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-114&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-113&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-112&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-111&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-110&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-109&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-108&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-107&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-106&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-105&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-104&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-103&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-102&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-101&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-100&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-99&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-98&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-97&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-96&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-95&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-94&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-93&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-92&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-91&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-90&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-89&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-88&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-87&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-86&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-85&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-84&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-83&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-82&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-81&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-80&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-79&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-78&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-77&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-76&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-75&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-74&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-73&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-72&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-71&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-70&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-69&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-68&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-67&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-66&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-65&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-64&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-63&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-62&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-61&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-60&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-59&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-58&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-57&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-56&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-55&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-54&amp;quot;,1.2326e-32,1.86265e-09],[&amp;quot;+2^-53&amp;quot;,2.46519e-32,1.86265e-09],[&amp;quot;+2^-52&amp;quot;,4.93038e-32,1.86265e-09],[&amp;quot;+2^-51&amp;quot;,9.86076e-32,1.86265e-09],[&amp;quot;+2^-50&amp;quot;,1.97215e-31,1.86265e-09],[&amp;quot;+2^-49&amp;quot;,3.9443e-31,1.86265e-09],[&amp;quot;+2^-48&amp;quot;,7.88861e-31,1.86265e-09],[&amp;quot;+2^-47&amp;quot;,1.57772e-30,1.86265e-09],[&amp;quot;+2^-46&amp;quot;,3.15544e-30,1.86265e-09],[&amp;quot;+2^-45&amp;quot;,6.31089e-30,1.86265e-09],[&amp;quot;+2^-44&amp;quot;,1.26218e-29,1.86265e-09],[&amp;quot;+2^-43&amp;quot;,2.52435e-29,1.86265e-09],[&amp;quot;+2^-42&amp;quot;,5.04871e-29,1.86265e-09],[&amp;quot;+2^-41&amp;quot;,1.00974e-28,1.86265e-09],[&amp;quot;+2^-40&amp;quot;,2.01948e-28,1.86265e-09],[&amp;quot;+2^-39&amp;quot;,4.03897e-28,1.86265e-09],[&amp;quot;+2^-38&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-37&amp;quot;,1.61559e-27,1.86265e-09],[&amp;quot;+2^-36&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-35&amp;quot;,6.46235e-27,1.86265e-09],[&amp;quot;+2^-34&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-33&amp;quot;,2.58494e-26,1.86265e-09],[&amp;quot;+2^-32&amp;quot;,5.16988e-26,1.86265e-09],[&amp;quot;+2^-31&amp;quot;,1.03398e-25,1.86265e-09],[&amp;quot;+2^-30&amp;quot;,2.06795e-25,1.86265e-09],[&amp;quot;+2^-29&amp;quot;,4.1359e-25,1.86265e-09],[&amp;quot;+2^-28&amp;quot;,8.27181e-25,1.86265e-09],[&amp;quot;+2^-27&amp;quot;,1.65436e-24,1.86265e-09],[&amp;quot;+2^-26&amp;quot;,9.92617e-24,5.58794e-09],[&amp;quot;+2^-25&amp;quot;,3.55271e-15,1],[&amp;quot;+2^-24&amp;quot;,7.10543e-15,1],[&amp;quot;+2^-23&amp;quot;,1.42109e-14,1],[&amp;quot;+2^-22&amp;quot;,2.84217e-14,1],[&amp;quot;+2^-21&amp;quot;,5.68437e-14,1],[&amp;quot;+2^-20&amp;quot;,1.13689e-13,1.00002],[&amp;quot;+2^-19&amp;quot;,2.27391e-13,1.00008],[&amp;quot;+2^-18&amp;quot;,4.54892e-13,1.00032],[&amp;quot;+2^-17&amp;quot;,9.10665e-13,1.00129],[&amp;quot;+2^-16&amp;quot;,1.82841e-12,1.00518],[&amp;quot;+2^-15&amp;quot;,3.71353e-12,1.02077],[&amp;quot;+2^-14&amp;quot;,1.39798e-11,1.92136],[&amp;quot;+2^-13&amp;quot;,2.63239e-11,1.80896],[&amp;quot;+2^-12&amp;quot;,5.37712e-11,1.84757],[&amp;quot;+2^-11&amp;quot;,1.09932e-10,1.88861],[&amp;quot;+2^-10&amp;quot;,2.24787e-10,1.93091],[&amp;quot;+2^-9&amp;quot;,4.3452e-10,1.86625],[&amp;quot;+2^-8&amp;quot;,1.01783e-09,2.18576],[&amp;quot;+2^-7&amp;quot;,2.10562e-09,2.26089],[&amp;quot;+2^-6&amp;quot;,4.03954e-09,2.16871],[&amp;quot;+2^-5&amp;quot;,8.15746e-09,2.18975],[&amp;quot;+2^-4&amp;quot;,1.5545e-08,2.08641],[&amp;quot;+2^-3&amp;quot;,2.62153e-08,2.05864],[&amp;quot;+2^-2&amp;quot;,4.09295e-08,1.73804],[&amp;quot;+2^-1&amp;quot;,8.02409e-08,1.34622],[&amp;quot;+2^0&amp;quot;,8.66201e-08,1.45324],[&amp;quot;+2^1&amp;quot;,8.8654e-08,1.48737],[&amp;quot;+2^2&amp;quot;,6.7976e-08,1.14045],[&amp;quot;+2^3&amp;quot;,7.44117e-08,1.24842],[&amp;quot;+2^4&amp;quot;,2.53131e-14,4.24683e-07],[&amp;quot;+2^5&amp;quot;,0,null],[&amp;quot;+2^6&amp;quot;,0,null],[&amp;quot;+2^7&amp;quot;,0,null],[&amp;quot;+2^8&amp;quot;,0,null],[&amp;quot;+2^9&amp;quot;,0,null],[&amp;quot;+2^10&amp;quot;,0,null],[&amp;quot;+2^11&amp;quot;,0,null],[&amp;quot;+2^12&amp;quot;,0,null],[&amp;quot;+2^13&amp;quot;,0,null],[&amp;quot;+2^14&amp;quot;,0,null],[&amp;quot;+2^15&amp;quot;,0,null],[&amp;quot;+2^16&amp;quot;,0,null],[&amp;quot;+2^17&amp;quot;,0,null],[&amp;quot;+2^18&amp;quot;,0,null],[&amp;quot;+2^19&amp;quot;,0,null],[&amp;quot;+2^20&amp;quot;,0,null],[&amp;quot;+2^21&amp;quot;,0,null],[&amp;quot;+2^22&amp;quot;,0,null],[&amp;quot;+2^23&amp;quot;,0,null],[&amp;quot;+2^24&amp;quot;,0,null],[&amp;quot;+2^25&amp;quot;,0,null],[&amp;quot;+2^26&amp;quot;,0,null],[&amp;quot;+2^27&amp;quot;,0,null],[&amp;quot;+2^28&amp;quot;,0,null],[&amp;quot;+2^29&amp;quot;,0,null],[&amp;quot;+2^30&amp;quot;,0,null],[&amp;quot;+2^31&amp;quot;,0,null],[&amp;quot;+2^32&amp;quot;,0,null],[&amp;quot;+2^33&amp;quot;,0,null],[&amp;quot;+2^34&amp;quot;,0,null],[&amp;quot;+2^35&amp;quot;,0,null],[&amp;quot;+2^36&amp;quot;,0,null],[&amp;quot;+2^37&amp;quot;,0,null],[&amp;quot;+2^38&amp;quot;,0,null],[&amp;quot;+2^39&amp;quot;,0,null],[&amp;quot;+2^40&amp;quot;,0,null],[&amp;quot;+2^41&amp;quot;,0,null],[&amp;quot;+2^42&amp;quot;,0,null],[&amp;quot;+2^43&amp;quot;,0,null],[&amp;quot;+2^44&amp;quot;,0,null],[&amp;quot;+2^45&amp;quot;,0,null],[&amp;quot;+2^46&amp;quot;,0,null],[&amp;quot;+2^47&amp;quot;,0,null],[&amp;quot;+2^48&amp;quot;,0,null],[&amp;quot;+2^49&amp;quot;,0,null],[&amp;quot;+2^50&amp;quot;,0,null],[&amp;quot;+2^51&amp;quot;,0,null],[&amp;quot;+2^52&amp;quot;,0,null],[&amp;quot;+2^53&amp;quot;,0,null],[&amp;quot;+2^54&amp;quot;,0,null],[&amp;quot;+2^55&amp;quot;,0,null],[&amp;quot;+2^56&amp;quot;,0,null],[&amp;quot;+2^57&amp;quot;,0,null],[&amp;quot;+2^58&amp;quot;,0,null],[&amp;quot;+2^59&amp;quot;,0,null],[&amp;quot;+2^60&amp;quot;,0,null],[&amp;quot;+2^61&amp;quot;,0,null],[&amp;quot;+2^62&amp;quot;,0,null],[&amp;quot;+2^63&amp;quot;,0,null],[&amp;quot;+2^64&amp;quot;,0,null],[&amp;quot;+2^65&amp;quot;,0,null],[&amp;quot;+2^66&amp;quot;,0,null],[&amp;quot;+2^67&amp;quot;,0,null],[&amp;quot;+2^68&amp;quot;,0,null],[&amp;quot;+2^69&amp;quot;,0,null],[&amp;quot;+2^70&amp;quot;,0,null],[&amp;quot;+2^71&amp;quot;,0,null],[&amp;quot;+2^72&amp;quot;,0,null],[&amp;quot;+2^73&amp;quot;,0,null],[&amp;quot;+2^74&amp;quot;,0,null],[&amp;quot;+2^75&amp;quot;,0,null],[&amp;quot;+2^76&amp;quot;,0,null],[&amp;quot;+2^77&amp;quot;,0,null],[&amp;quot;+2^78&amp;quot;,0,null],[&amp;quot;+2^79&amp;quot;,0,null],[&amp;quot;+2^80&amp;quot;,0,null],[&amp;quot;+2^81&amp;quot;,0,null],[&amp;quot;+2^82&amp;quot;,0,null],[&amp;quot;+2^83&amp;quot;,0,null],[&amp;quot;+2^84&amp;quot;,0,null],[&amp;quot;+2^85&amp;quot;,0,null],[&amp;quot;+2^86&amp;quot;,0,null],[&amp;quot;+2^87&amp;quot;,0,null],[&amp;quot;+2^88&amp;quot;,0,null],[&amp;quot;+2^89&amp;quot;,0,null],[&amp;quot;+2^90&amp;quot;,0,null],[&amp;quot;+2^91&amp;quot;,0,null],[&amp;quot;+2^92&amp;quot;,0,null],[&amp;quot;+2^93&amp;quot;,0,null],[&amp;quot;+2^94&amp;quot;,0,null],[&amp;quot;+2^95&amp;quot;,0,null],[&amp;quot;+2^96&amp;quot;,0,null],[&amp;quot;+2^97&amp;quot;,0,null],[&amp;quot;+2^98&amp;quot;,0,null],[&amp;quot;+2^99&amp;quot;,0,null],[&amp;quot;+2^100&amp;quot;,0,null],[&amp;quot;+2^101&amp;quot;,0,null],[&amp;quot;+2^102&amp;quot;,0,null],[&amp;quot;+2^103&amp;quot;,0,null],[&amp;quot;+2^104&amp;quot;,0,null],[&amp;quot;+2^105&amp;quot;,0,null],[&amp;quot;+2^106&amp;quot;,0,null],[&amp;quot;+2^107&amp;quot;,0,null],[&amp;quot;+2^108&amp;quot;,0,null],[&amp;quot;+2^109&amp;quot;,0,null],[&amp;quot;+2^110&amp;quot;,0,null],[&amp;quot;+2^111&amp;quot;,0,null],[&amp;quot;+2^112&amp;quot;,0,null],[&amp;quot;+2^113&amp;quot;,0,null],[&amp;quot;+2^114&amp;quot;,0,null],[&amp;quot;+2^115&amp;quot;,0,null],[&amp;quot;+2^116&amp;quot;,0,null],[&amp;quot;+2^117&amp;quot;,0,null],[&amp;quot;+2^118&amp;quot;,0,null],[&amp;quot;+2^119&amp;quot;,0,null],[&amp;quot;+2^120&amp;quot;,0,null],[&amp;quot;+2^121&amp;quot;,0,null],[&amp;quot;+2^122&amp;quot;,0,null],[&amp;quot;+2^123&amp;quot;,0,null],[&amp;quot;+2^124&amp;quot;,0,null],[&amp;quot;+2^125&amp;quot;,0,null],[&amp;quot;+2^126&amp;quot;,0,null],[&amp;quot;+2^127&amp;quot;,0,null] ]&lt;br /&gt;
 }}&lt;br /&gt;
&lt;br /&gt;
==Taylor==&lt;br /&gt;
&lt;br /&gt;
{{#widget:FloatErrorGraph|id=taylor&lt;br /&gt;
  |data=[ [&amp;quot;-2^127&amp;quot;,0,0],[&amp;quot;-2^126&amp;quot;,0,0],[&amp;quot;-2^125&amp;quot;,0,0],[&amp;quot;-2^124&amp;quot;,0,0],[&amp;quot;-2^123&amp;quot;,0,0],[&amp;quot;-2^122&amp;quot;,0,0],[&amp;quot;-2^121&amp;quot;,0,0],[&amp;quot;-2^120&amp;quot;,0,0],[&amp;quot;-2^119&amp;quot;,0,0],[&amp;quot;-2^118&amp;quot;,0,0],[&amp;quot;-2^117&amp;quot;,0,0],[&amp;quot;-2^116&amp;quot;,0,0],[&amp;quot;-2^115&amp;quot;,0,0],[&amp;quot;-2^114&amp;quot;,0,0],[&amp;quot;-2^113&amp;quot;,0,0],[&amp;quot;-2^112&amp;quot;,0,0],[&amp;quot;-2^111&amp;quot;,0,0],[&amp;quot;-2^110&amp;quot;,0,0],[&amp;quot;-2^109&amp;quot;,0,0],[&amp;quot;-2^108&amp;quot;,0,0],[&amp;quot;-2^107&amp;quot;,0,0],[&amp;quot;-2^106&amp;quot;,0,0],[&amp;quot;-2^105&amp;quot;,0,0],[&amp;quot;-2^104&amp;quot;,0,0],[&amp;quot;-2^103&amp;quot;,0,0],[&amp;quot;-2^102&amp;quot;,0,0],[&amp;quot;-2^101&amp;quot;,0,0],[&amp;quot;-2^100&amp;quot;,0,0],[&amp;quot;-2^99&amp;quot;,0,0],[&amp;quot;-2^98&amp;quot;,0,0],[&amp;quot;-2^97&amp;quot;,0,0],[&amp;quot;-2^96&amp;quot;,0,0],[&amp;quot;-2^95&amp;quot;,0,0],[&amp;quot;-2^94&amp;quot;,0,0],[&amp;quot;-2^93&amp;quot;,0,0],[&amp;quot;-2^92&amp;quot;,0,0],[&amp;quot;-2^91&amp;quot;,0,0],[&amp;quot;-2^90&amp;quot;,0,0],[&amp;quot;-2^89&amp;quot;,0,0],[&amp;quot;-2^88&amp;quot;,0,0],[&amp;quot;-2^87&amp;quot;,0,0],[&amp;quot;-2^86&amp;quot;,0,0],[&amp;quot;-2^85&amp;quot;,0,0],[&amp;quot;-2^84&amp;quot;,0,0],[&amp;quot;-2^83&amp;quot;,0,0],[&amp;quot;-2^82&amp;quot;,0,0],[&amp;quot;-2^81&amp;quot;,0,0],[&amp;quot;-2^80&amp;quot;,0,0],[&amp;quot;-2^79&amp;quot;,0,0],[&amp;quot;-2^78&amp;quot;,0,0],[&amp;quot;-2^77&amp;quot;,0,0],[&amp;quot;-2^76&amp;quot;,0,0],[&amp;quot;-2^75&amp;quot;,0,0],[&amp;quot;-2^74&amp;quot;,0,0],[&amp;quot;-2^73&amp;quot;,0,0],[&amp;quot;-2^72&amp;quot;,0,0],[&amp;quot;-2^71&amp;quot;,0,0],[&amp;quot;-2^70&amp;quot;,0,0],[&amp;quot;-2^69&amp;quot;,0,0],[&amp;quot;-2^68&amp;quot;,0,0],[&amp;quot;-2^67&amp;quot;,0,0],[&amp;quot;-2^66&amp;quot;,0,0],[&amp;quot;-2^65&amp;quot;,0,0],[&amp;quot;-2^64&amp;quot;,0,0],[&amp;quot;-2^63&amp;quot;,1.84467e+19,3.09485e+26],[&amp;quot;-2^62&amp;quot;,9.22336e+18,1.54742e+26],[&amp;quot;-2^61&amp;quot;,4.61168e+18,7.73712e+25],[&amp;quot;-2^60&amp;quot;,2.30584e+18,3.86856e+25],[&amp;quot;-2^59&amp;quot;,1.15292e+18,1.93428e+25],[&amp;quot;-2^58&amp;quot;,5.7646e+17,9.6714e+24],[&amp;quot;-2^57&amp;quot;,2.8823e+17,4.8357e+24],[&amp;quot;-2^56&amp;quot;,1.44115e+17,2.41785e+24],[&amp;quot;-2^55&amp;quot;,7.20575e+16,1.20892e+24],[&amp;quot;-2^54&amp;quot;,3.60288e+16,6.04462e+23],[&amp;quot;-2^53&amp;quot;,1.80144e+16,3.02231e+23],[&amp;quot;-2^52&amp;quot;,9.00719e+15,1.51116e+23],[&amp;quot;-2^51&amp;quot;,4.5036e+15,7.55578e+22],[&amp;quot;-2^50&amp;quot;,2.2518e+15,3.77789e+22],[&amp;quot;-2^49&amp;quot;,1.1259e+15,1.88894e+22],[&amp;quot;-2^48&amp;quot;,5.62949e+14,9.44472e+21],[&amp;quot;-2^47&amp;quot;,2.81475e+14,4.72236e+21],[&amp;quot;-2^46&amp;quot;,1.40737e+14,2.36118e+21],[&amp;quot;-2^45&amp;quot;,7.03687e+13,1.18059e+21],[&amp;quot;-2^44&amp;quot;,3.51843e+13,5.90295e+20],[&amp;quot;-2^43&amp;quot;,1.75922e+13,2.95148e+20],[&amp;quot;-2^42&amp;quot;,8.79608e+12,1.47574e+20],[&amp;quot;-2^41&amp;quot;,4.39804e+12,7.37869e+19],[&amp;quot;-2^40&amp;quot;,2.19902e+12,3.68935e+19],[&amp;quot;-2^39&amp;quot;,1.09951e+12,1.84467e+19],[&amp;quot;-2^38&amp;quot;,5.49755e+11,9.22336e+18],[&amp;quot;-2^37&amp;quot;,2.74878e+11,4.61168e+18],[&amp;quot;-2^36&amp;quot;,1.37439e+11,2.30584e+18],[&amp;quot;-2^35&amp;quot;,6.87194e+10,1.15292e+18],[&amp;quot;-2^34&amp;quot;,3.43597e+10,5.7646e+17],[&amp;quot;-2^33&amp;quot;,1.71799e+10,2.8823e+17],[&amp;quot;-2^32&amp;quot;,8.58993e+09,1.44115e+17],[&amp;quot;-2^31&amp;quot;,4.29085e+09,7.19885e+16],[&amp;quot;-2^30&amp;quot;,2.14542e+09,3.59943e+16],[&amp;quot;-2^29&amp;quot;,1.07271e+09,1.79971e+16],[&amp;quot;-2^28&amp;quot;,5.36356e+08,8.99856e+15],[&amp;quot;-2^27&amp;quot;,2.68178e+08,4.49928e+15],[&amp;quot;-2^26&amp;quot;,1.34089e+08,2.24964e+15],[&amp;quot;-2^25&amp;quot;,6.70445e+07,1.12482e+15],[&amp;quot;-2^24&amp;quot;,3.35223e+07,5.6241e+14],[&amp;quot;-2^23&amp;quot;,1.67611e+07,2.81205e+14],[&amp;quot;-2^22&amp;quot;,8.38056e+06,1.40603e+14],[&amp;quot;-2^21&amp;quot;,4.19028e+06,7.03013e+13],[&amp;quot;-2^20&amp;quot;,2.09514e+06,3.51506e+13],[&amp;quot;-2^19&amp;quot;,1.04757e+06,1.75753e+13],[&amp;quot;-2^18&amp;quot;,523784,8.78764e+12],[&amp;quot;-2^17&amp;quot;,261892,4.39381e+12],[&amp;quot;-2^16&amp;quot;,130945,2.1969e+12],[&amp;quot;-2^15&amp;quot;,65472.2,1.09844e+12],[&amp;quot;-2^14&amp;quot;,32735.6,5.49212e+11],[&amp;quot;-2^13&amp;quot;,16367.3,2.74598e+11],[&amp;quot;-2^12&amp;quot;,8183.15,1.3729e+11],[&amp;quot;-2^11&amp;quot;,4091.07,6.86368e+10],[&amp;quot;-2^10&amp;quot;,2045.04,3.431e+10],[&amp;quot;-2^9&amp;quot;,1022.02,1.71466e+10],[&amp;quot;-2^8&amp;quot;,510.509,8.56492e+09],[&amp;quot;-2^7&amp;quot;,254.754,4.27406e+09],[&amp;quot;-2^6&amp;quot;,126.876,2.12862e+09],[&amp;quot;-2^5&amp;quot;,62.9354,1.05588e+09],[&amp;quot;-2^4&amp;quot;,30.9629,5.19471e+08],[&amp;quot;-2^3&amp;quot;,14.9718,2.51185e+08],[&amp;quot;-2^2&amp;quot;,6.96672,1.16882e+08],[&amp;quot;-2^1&amp;quot;,2.94633,4.94312e+07],[&amp;quot;-2^0&amp;quot;,0.940715,1.57826e+07],[&amp;quot;-2^-1&amp;quot;,0.127635,2.14135e+06],[&amp;quot;-2^-2&amp;quot;,0.00318735,106950],[&amp;quot;-2^-3&amp;quot;,3.11475e-05,2090.28],[&amp;quot;-2^-4&amp;quot;,2.57737e-07,34.5929],[&amp;quot;-2^-5&amp;quot;,3.89213e-09,1.04478],[&amp;quot;-2^-6&amp;quot;,9.51472e-10,0.510818],[&amp;quot;-2^-7&amp;quot;,4.66402e-10,0.500795],[&amp;quot;-2^-8&amp;quot;,2.32917e-10,0.500185],[&amp;quot;-2^-9&amp;quot;,1.16423e-10,0.500033],[&amp;quot;-2^-10&amp;quot;,5.82082e-11,0.500004],[&amp;quot;-2^-11&amp;quot;,2.91037e-11,0.499998],[&amp;quot;-2^-12&amp;quot;,1.45519e-11,0.5],[&amp;quot;-2^-13&amp;quot;,4.85062e-12,0.333332],[&amp;quot;-2^-14&amp;quot;,6.06328e-13,0.0833331],[&amp;quot;-2^-15&amp;quot;,7.5791e-14,0.0208333],[&amp;quot;-2^-16&amp;quot;,9.47387e-15,0.00520832],[&amp;quot;-2^-17&amp;quot;,1.18424e-15,0.00130208],[&amp;quot;-2^-18&amp;quot;,1.48029e-16,0.00032552],[&amp;quot;-2^-19&amp;quot;,1.85039e-17,8.13808e-05],[&amp;quot;-2^-20&amp;quot;,2.31304e-18,2.03457e-05],[&amp;quot;-2^-21&amp;quot;,2.89156e-19,5.08688e-06],[&amp;quot;-2^-22&amp;quot;,3.61577e-20,1.27219e-06],[&amp;quot;-2^-23&amp;quot;,4.52633e-21,3.18512e-07],[&amp;quot;-2^-24&amp;quot;,5.691e-22,8.00937e-08],[&amp;quot;-2^-25&amp;quot;,7.27919e-23,2.04891e-08],[&amp;quot;-2^-26&amp;quot;,9.92617e-24,5.58794e-09],[&amp;quot;-2^-27&amp;quot;,1.65436e-24,1.86265e-09],[&amp;quot;-2^-28&amp;quot;,8.27181e-25,1.86265e-09],[&amp;quot;-2^-29&amp;quot;,4.1359e-25,1.86265e-09],[&amp;quot;-2^-30&amp;quot;,2.06795e-25,1.86265e-09],[&amp;quot;-2^-31&amp;quot;,1.03398e-25,1.86265e-09],[&amp;quot;-2^-32&amp;quot;,5.16988e-26,1.86265e-09],[&amp;quot;-2^-33&amp;quot;,2.58494e-26,1.86265e-09],[&amp;quot;-2^-34&amp;quot;,1.29247e-26,1.86265e-09],[&amp;quot;-2^-35&amp;quot;,6.46235e-27,1.86265e-09],[&amp;quot;-2^-36&amp;quot;,3.23117e-27,1.86265e-09],[&amp;quot;-2^-37&amp;quot;,1.61559e-27,1.86265e-09],[&amp;quot;-2^-38&amp;quot;,8.07794e-28,1.86265e-09],[&amp;quot;-2^-39&amp;quot;,4.03897e-28,1.86265e-09],[&amp;quot;-2^-40&amp;quot;,2.01948e-28,1.86265e-09],[&amp;quot;-2^-41&amp;quot;,1.00974e-28,1.86265e-09],[&amp;quot;-2^-42&amp;quot;,5.04871e-29,1.86265e-09],[&amp;quot;-2^-43&amp;quot;,2.52435e-29,1.86265e-09],[&amp;quot;-2^-44&amp;quot;,1.26218e-29,1.86265e-09],[&amp;quot;-2^-45&amp;quot;,6.31089e-30,1.86265e-09],[&amp;quot;-2^-46&amp;quot;,3.15544e-30,1.86265e-09],[&amp;quot;-2^-47&amp;quot;,1.57772e-30,1.86265e-09],[&amp;quot;-2^-48&amp;quot;,7.88861e-31,1.86265e-09],[&amp;quot;-2^-49&amp;quot;,3.9443e-31,1.86265e-09],[&amp;quot;-2^-50&amp;quot;,1.97215e-31,1.86265e-09],[&amp;quot;-2^-51&amp;quot;,9.86076e-32,1.86265e-09],[&amp;quot;-2^-52&amp;quot;,4.93038e-32,1.86265e-09],[&amp;quot;-2^-53&amp;quot;,2.46519e-32,1.86265e-09],[&amp;quot;-2^-54&amp;quot;,1.2326e-32,1.86265e-09],[&amp;quot;-2^-55&amp;quot;,0,0],[&amp;quot;-2^-56&amp;quot;,0,0],[&amp;quot;-2^-57&amp;quot;,0,0],[&amp;quot;-2^-58&amp;quot;,0,0],[&amp;quot;-2^-59&amp;quot;,0,0],[&amp;quot;-2^-60&amp;quot;,0,0],[&amp;quot;-2^-61&amp;quot;,0,0],[&amp;quot;-2^-62&amp;quot;,0,0],[&amp;quot;-2^-63&amp;quot;,0,0],[&amp;quot;-2^-64&amp;quot;,0,0],[&amp;quot;-2^-65&amp;quot;,0,0],[&amp;quot;-2^-66&amp;quot;,0,0],[&amp;quot;-2^-67&amp;quot;,0,0],[&amp;quot;-2^-68&amp;quot;,0,0],[&amp;quot;-2^-69&amp;quot;,0,0],[&amp;quot;-2^-70&amp;quot;,0,0],[&amp;quot;-2^-71&amp;quot;,0,0],[&amp;quot;-2^-72&amp;quot;,0,0],[&amp;quot;-2^-73&amp;quot;,0,0],[&amp;quot;-2^-74&amp;quot;,0,0],[&amp;quot;-2^-75&amp;quot;,0,0],[&amp;quot;-2^-76&amp;quot;,0,0],[&amp;quot;-2^-77&amp;quot;,0,0],[&amp;quot;-2^-78&amp;quot;,0,0],[&amp;quot;-2^-79&amp;quot;,0,0],[&amp;quot;-2^-80&amp;quot;,0,0],[&amp;quot;-2^-81&amp;quot;,0,0],[&amp;quot;-2^-82&amp;quot;,0,0],[&amp;quot;-2^-83&amp;quot;,0,0],[&amp;quot;-2^-84&amp;quot;,0,0],[&amp;quot;-2^-85&amp;quot;,0,0],[&amp;quot;-2^-86&amp;quot;,0,0],[&amp;quot;-2^-87&amp;quot;,0,0],[&amp;quot;-2^-88&amp;quot;,0,0],[&amp;quot;-2^-89&amp;quot;,0,0],[&amp;quot;-2^-90&amp;quot;,0,0],[&amp;quot;-2^-91&amp;quot;,0,0],[&amp;quot;-2^-92&amp;quot;,0,0],[&amp;quot;-2^-93&amp;quot;,0,0],[&amp;quot;-2^-94&amp;quot;,0,0],[&amp;quot;-2^-95&amp;quot;,0,0],[&amp;quot;-2^-96&amp;quot;,0,0],[&amp;quot;-2^-97&amp;quot;,0,0],[&amp;quot;-2^-98&amp;quot;,0,0],[&amp;quot;-2^-99&amp;quot;,0,0],[&amp;quot;-2^-100&amp;quot;,0,0],[&amp;quot;-2^-101&amp;quot;,0,0],[&amp;quot;-2^-102&amp;quot;,0,0],[&amp;quot;-2^-103&amp;quot;,0,0],[&amp;quot;-2^-104&amp;quot;,0,0],[&amp;quot;-2^-105&amp;quot;,0,0],[&amp;quot;-2^-106&amp;quot;,0,0],[&amp;quot;-2^-107&amp;quot;,0,0],[&amp;quot;-2^-108&amp;quot;,0,0],[&amp;quot;-2^-109&amp;quot;,0,0],[&amp;quot;-2^-110&amp;quot;,0,0],[&amp;quot;-2^-111&amp;quot;,0,0],[&amp;quot;-2^-112&amp;quot;,0,0],[&amp;quot;-2^-113&amp;quot;,0,0],[&amp;quot;-2^-114&amp;quot;,0,0],[&amp;quot;-2^-115&amp;quot;,0,0],[&amp;quot;-2^-116&amp;quot;,0,0],[&amp;quot;-2^-117&amp;quot;,0,0],[&amp;quot;-2^-118&amp;quot;,0,0],[&amp;quot;-2^-119&amp;quot;,0,0],[&amp;quot;-2^-120&amp;quot;,0,0],[&amp;quot;-2^-121&amp;quot;,0,0],[&amp;quot;-2^-122&amp;quot;,0,0],[&amp;quot;-2^-123&amp;quot;,0,0],[&amp;quot;-2^-124&amp;quot;,0,0],[&amp;quot;-2^-125&amp;quot;,0,0],[&amp;quot;-2^-126&amp;quot;,0,0],[&amp;quot;-SN&amp;quot;,0,0],[&amp;quot;0&amp;quot;,0,0],[&amp;quot;+SN&amp;quot;,0,0],[&amp;quot;+2^-126&amp;quot;,0,0],[&amp;quot;+2^-125&amp;quot;,0,0],[&amp;quot;+2^-124&amp;quot;,0,0],[&amp;quot;+2^-123&amp;quot;,0,0],[&amp;quot;+2^-122&amp;quot;,0,0],[&amp;quot;+2^-121&amp;quot;,0,0],[&amp;quot;+2^-120&amp;quot;,0,0],[&amp;quot;+2^-119&amp;quot;,0,0],[&amp;quot;+2^-118&amp;quot;,0,0],[&amp;quot;+2^-117&amp;quot;,0,0],[&amp;quot;+2^-116&amp;quot;,0,0],[&amp;quot;+2^-115&amp;quot;,0,0],[&amp;quot;+2^-114&amp;quot;,0,0],[&amp;quot;+2^-113&amp;quot;,0,0],[&amp;quot;+2^-112&amp;quot;,0,0],[&amp;quot;+2^-111&amp;quot;,0,0],[&amp;quot;+2^-110&amp;quot;,0,0],[&amp;quot;+2^-109&amp;quot;,0,0],[&amp;quot;+2^-108&amp;quot;,0,0],[&amp;quot;+2^-107&amp;quot;,0,0],[&amp;quot;+2^-106&amp;quot;,0,0],[&amp;quot;+2^-105&amp;quot;,0,0],[&amp;quot;+2^-104&amp;quot;,0,0],[&amp;quot;+2^-103&amp;quot;,0,0],[&amp;quot;+2^-102&amp;quot;,0,0],[&amp;quot;+2^-101&amp;quot;,0,0],[&amp;quot;+2^-100&amp;quot;,0,0],[&amp;quot;+2^-99&amp;quot;,0,0],[&amp;quot;+2^-98&amp;quot;,0,0],[&amp;quot;+2^-97&amp;quot;,0,0],[&amp;quot;+2^-96&amp;quot;,0,0],[&amp;quot;+2^-95&amp;quot;,0,0],[&amp;quot;+2^-94&amp;quot;,0,0],[&amp;quot;+2^-93&amp;quot;,0,0],[&amp;quot;+2^-92&amp;quot;,0,0],[&amp;quot;+2^-91&amp;quot;,0,0],[&amp;quot;+2^-90&amp;quot;,0,0],[&amp;quot;+2^-89&amp;quot;,0,0],[&amp;quot;+2^-88&amp;quot;,0,0],[&amp;quot;+2^-87&amp;quot;,0,0],[&amp;quot;+2^-86&amp;quot;,0,0],[&amp;quot;+2^-85&amp;quot;,0,0],[&amp;quot;+2^-84&amp;quot;,0,0],[&amp;quot;+2^-83&amp;quot;,0,0],[&amp;quot;+2^-82&amp;quot;,0,0],[&amp;quot;+2^-81&amp;quot;,0,0],[&amp;quot;+2^-80&amp;quot;,0,0],[&amp;quot;+2^-79&amp;quot;,0,0],[&amp;quot;+2^-78&amp;quot;,0,0],[&amp;quot;+2^-77&amp;quot;,0,0],[&amp;quot;+2^-76&amp;quot;,0,0],[&amp;quot;+2^-75&amp;quot;,0,0],[&amp;quot;+2^-74&amp;quot;,0,0],[&amp;quot;+2^-73&amp;quot;,0,0],[&amp;quot;+2^-72&amp;quot;,0,0],[&amp;quot;+2^-71&amp;quot;,0,0],[&amp;quot;+2^-70&amp;quot;,0,0],[&amp;quot;+2^-69&amp;quot;,0,0],[&amp;quot;+2^-68&amp;quot;,0,0],[&amp;quot;+2^-67&amp;quot;,0,0],[&amp;quot;+2^-66&amp;quot;,0,0],[&amp;quot;+2^-65&amp;quot;,0,0],[&amp;quot;+2^-64&amp;quot;,0,0],[&amp;quot;+2^-63&amp;quot;,0,0],[&amp;quot;+2^-62&amp;quot;,0,0],[&amp;quot;+2^-61&amp;quot;,0,0],[&amp;quot;+2^-60&amp;quot;,0,0],[&amp;quot;+2^-59&amp;quot;,0,0],[&amp;quot;+2^-58&amp;quot;,0,0],[&amp;quot;+2^-57&amp;quot;,0,0],[&amp;quot;+2^-56&amp;quot;,0,0],[&amp;quot;+2^-55&amp;quot;,0,0],[&amp;quot;+2^-54&amp;quot;,1.2326e-32,1.86265e-09],[&amp;quot;+2^-53&amp;quot;,2.46519e-32,1.86265e-09],[&amp;quot;+2^-52&amp;quot;,4.93038e-32,1.86265e-09],[&amp;quot;+2^-51&amp;quot;,9.86076e-32,1.86265e-09],[&amp;quot;+2^-50&amp;quot;,1.97215e-31,1.86265e-09],[&amp;quot;+2^-49&amp;quot;,3.9443e-31,1.86265e-09],[&amp;quot;+2^-48&amp;quot;,7.88861e-31,1.86265e-09],[&amp;quot;+2^-47&amp;quot;,1.57772e-30,1.86265e-09],[&amp;quot;+2^-46&amp;quot;,3.15544e-30,1.86265e-09],[&amp;quot;+2^-45&amp;quot;,6.31089e-30,1.86265e-09],[&amp;quot;+2^-44&amp;quot;,1.26218e-29,1.86265e-09],[&amp;quot;+2^-43&amp;quot;,2.52435e-29,1.86265e-09],[&amp;quot;+2^-42&amp;quot;,5.04871e-29,1.86265e-09],[&amp;quot;+2^-41&amp;quot;,1.00974e-28,1.86265e-09],[&amp;quot;+2^-40&amp;quot;,2.01948e-28,1.86265e-09],[&amp;quot;+2^-39&amp;quot;,4.03897e-28,1.86265e-09],[&amp;quot;+2^-38&amp;quot;,8.07794e-28,1.86265e-09],[&amp;quot;+2^-37&amp;quot;,1.61559e-27,1.86265e-09],[&amp;quot;+2^-36&amp;quot;,3.23117e-27,1.86265e-09],[&amp;quot;+2^-35&amp;quot;,6.46235e-27,1.86265e-09],[&amp;quot;+2^-34&amp;quot;,1.29247e-26,1.86265e-09],[&amp;quot;+2^-33&amp;quot;,2.58494e-26,1.86265e-09],[&amp;quot;+2^-32&amp;quot;,5.16988e-26,1.86265e-09],[&amp;quot;+2^-31&amp;quot;,1.03398e-25,1.86265e-09],[&amp;quot;+2^-30&amp;quot;,2.06795e-25,1.86265e-09],[&amp;quot;+2^-29&amp;quot;,4.1359e-25,1.86265e-09],[&amp;quot;+2^-28&amp;quot;,8.27181e-25,1.86265e-09],[&amp;quot;+2^-27&amp;quot;,1.65436e-24,1.86265e-09],[&amp;quot;+2^-26&amp;quot;,9.92617e-24,5.58794e-09],[&amp;quot;+2^-25&amp;quot;,7.27919e-23,2.04891e-08],[&amp;quot;+2^-24&amp;quot;,5.691e-22,8.00937e-08],[&amp;quot;+2^-23&amp;quot;,4.52633e-21,3.18512e-07],[&amp;quot;+2^-22&amp;quot;,3.61577e-20,1.27219e-06],[&amp;quot;+2^-21&amp;quot;,2.89156e-19,5.08688e-06],[&amp;quot;+2^-20&amp;quot;,2.31304e-18,2.03457e-05],[&amp;quot;+2^-19&amp;quot;,1.85039e-17,8.13808e-05],[&amp;quot;+2^-18&amp;quot;,1.48029e-16,0.00032552],[&amp;quot;+2^-17&amp;quot;,1.18424e-15,0.00130208],[&amp;quot;+2^-16&amp;quot;,9.47387e-15,0.00520832],[&amp;quot;+2^-15&amp;quot;,7.5791e-14,0.0208333],[&amp;quot;+2^-14&amp;quot;,6.06328e-13,0.0833331],[&amp;quot;+2^-13&amp;quot;,4.85062e-12,0.333332],[&amp;quot;+2^-12&amp;quot;,1.45519e-11,0.5],[&amp;quot;+2^-11&amp;quot;,2.91037e-11,0.499998],[&amp;quot;+2^-10&amp;quot;,5.82082e-11,0.500004],[&amp;quot;+2^-9&amp;quot;,1.16423e-10,0.500033],[&amp;quot;+2^-8&amp;quot;,2.32917e-10,0.500185],[&amp;quot;+2^-7&amp;quot;,4.66402e-10,0.500795],[&amp;quot;+2^-6&amp;quot;,9.51472e-10,0.510818],[&amp;quot;+2^-5&amp;quot;,3.89213e-09,1.04478],[&amp;quot;+2^-4&amp;quot;,2.57737e-07,34.5929],[&amp;quot;+2^-3&amp;quot;,3.11475e-05,2090.28],[&amp;quot;+2^-2&amp;quot;,0.00318735,106950],[&amp;quot;+2^-1&amp;quot;,0.127635,2.14135e+06],[&amp;quot;+2^0&amp;quot;,0.940715,1.57826e+07],[&amp;quot;+2^1&amp;quot;,2.94633,4.94312e+07],[&amp;quot;+2^2&amp;quot;,6.96672,1.16882e+08],[&amp;quot;+2^3&amp;quot;,14.9718,1.33874e+08],[&amp;quot;+2^4&amp;quot;,30.9629,2.59736e+08],[&amp;quot;+2^5&amp;quot;,62.9354,5.27941e+08],[&amp;quot;+2^6&amp;quot;,126.876,1.06431e+09],[&amp;quot;+2^7&amp;quot;,254.754,2.13703e+09],[&amp;quot;+2^8&amp;quot;,510.509,4.28246e+09],[&amp;quot;+2^9&amp;quot;,1022.02,8.57331e+09],[&amp;quot;+2^10&amp;quot;,2045.04,1.7155e+10],[&amp;quot;+2^11&amp;quot;,4091.07,3.43184e+10],[&amp;quot;+2^12&amp;quot;,8183.15,6.86452e+10],[&amp;quot;+2^13&amp;quot;,16367.3,1.37299e+11],[&amp;quot;+2^14&amp;quot;,32735.6,2.74606e+11],[&amp;quot;+2^15&amp;quot;,65472.2,5.4922e+11],[&amp;quot;+2^16&amp;quot;,130945,1.09845e+12],[&amp;quot;+2^17&amp;quot;,261892,2.19691e+12],[&amp;quot;+2^18&amp;quot;,523784,4.39382e+12],[&amp;quot;+2^19&amp;quot;,1.04757e+06,8.78765e+12],[&amp;quot;+2^20&amp;quot;,2.09514e+06,1.75753e+13],[&amp;quot;+2^21&amp;quot;,4.19028e+06,3.51506e+13],[&amp;quot;+2^22&amp;quot;,8.38056e+06,7.03013e+13],[&amp;quot;+2^23&amp;quot;,1.67611e+07,1.40603e+14],[&amp;quot;+2^24&amp;quot;,3.35223e+07,2.81205e+14],[&amp;quot;+2^25&amp;quot;,6.70445e+07,5.6241e+14],[&amp;quot;+2^26&amp;quot;,1.34089e+08,1.12482e+15],[&amp;quot;+2^27&amp;quot;,2.68178e+08,2.24964e+15],[&amp;quot;+2^28&amp;quot;,5.36356e+08,4.49928e+15],[&amp;quot;+2^29&amp;quot;,1.07271e+09,8.99856e+15],[&amp;quot;+2^30&amp;quot;,2.14542e+09,1.79971e+16],[&amp;quot;+2^31&amp;quot;,4.29085e+09,3.59943e+16],[&amp;quot;+2^32&amp;quot;,8.58993e+09,7.20575e+16],[&amp;quot;+2^33&amp;quot;,1.71799e+10,1.44115e+17],[&amp;quot;+2^34&amp;quot;,3.43597e+10,2.8823e+17],[&amp;quot;+2^35&amp;quot;,6.87194e+10,5.7646e+17],[&amp;quot;+2^36&amp;quot;,1.37439e+11,1.15292e+18],[&amp;quot;+2^37&amp;quot;,2.74878e+11,2.30584e+18],[&amp;quot;+2^38&amp;quot;,5.49755e+11,4.61168e+18],[&amp;quot;+2^39&amp;quot;,1.09951e+12,9.22336e+18],[&amp;quot;+2^40&amp;quot;,2.19902e+12,1.84467e+19],[&amp;quot;+2^41&amp;quot;,4.39804e+12,3.68935e+19],[&amp;quot;+2^42&amp;quot;,8.79608e+12,7.37869e+19],[&amp;quot;+2^43&amp;quot;,1.75922e+13,1.47574e+20],[&amp;quot;+2^44&amp;quot;,3.51843e+13,2.95148e+20],[&amp;quot;+2^45&amp;quot;,7.03687e+13,5.90295e+20],[&amp;quot;+2^46&amp;quot;,1.40737e+14,1.18059e+21],[&amp;quot;+2^47&amp;quot;,2.81475e+14,2.36118e+21],[&amp;quot;+2^48&amp;quot;,5.62949e+14,4.72236e+21],[&amp;quot;+2^49&amp;quot;,1.1259e+15,9.44472e+21],[&amp;quot;+2^50&amp;quot;,2.2518e+15,1.88894e+22],[&amp;quot;+2^51&amp;quot;,4.5036e+15,3.77789e+22],[&amp;quot;+2^52&amp;quot;,9.00719e+15,7.55578e+22],[&amp;quot;+2^53&amp;quot;,1.80144e+16,1.51116e+23],[&amp;quot;+2^54&amp;quot;,3.60288e+16,3.02231e+23],[&amp;quot;+2^55&amp;quot;,7.20575e+16,6.04462e+23],[&amp;quot;+2^56&amp;quot;,1.44115e+17,1.20892e+24],[&amp;quot;+2^57&amp;quot;,2.8823e+17,2.41785e+24],[&amp;quot;+2^58&amp;quot;,5.7646e+17,4.8357e+24],[&amp;quot;+2^59&amp;quot;,1.15292e+18,9.6714e+24],[&amp;quot;+2^60&amp;quot;,2.30584e+18,1.93428e+25],[&amp;quot;+2^61&amp;quot;,4.61168e+18,3.86856e+25],[&amp;quot;+2^62&amp;quot;,9.22336e+18,7.73712e+25],[&amp;quot;+2^63&amp;quot;,1.84467e+19,1.54742e+26],[&amp;quot;+2^64&amp;quot;,0,0],[&amp;quot;+2^65&amp;quot;,0,0],[&amp;quot;+2^66&amp;quot;,0,0],[&amp;quot;+2^67&amp;quot;,0,0],[&amp;quot;+2^68&amp;quot;,0,0],[&amp;quot;+2^69&amp;quot;,0,0],[&amp;quot;+2^70&amp;quot;,0,0],[&amp;quot;+2^71&amp;quot;,0,0],[&amp;quot;+2^72&amp;quot;,0,0],[&amp;quot;+2^73&amp;quot;,0,0],[&amp;quot;+2^74&amp;quot;,0,0],[&amp;quot;+2^75&amp;quot;,0,0],[&amp;quot;+2^76&amp;quot;,0,0],[&amp;quot;+2^77&amp;quot;,0,0],[&amp;quot;+2^78&amp;quot;,0,0],[&amp;quot;+2^79&amp;quot;,0,0],[&amp;quot;+2^80&amp;quot;,0,0],[&amp;quot;+2^81&amp;quot;,0,0],[&amp;quot;+2^82&amp;quot;,0,0],[&amp;quot;+2^83&amp;quot;,0,0],[&amp;quot;+2^84&amp;quot;,0,0],[&amp;quot;+2^85&amp;quot;,0,0],[&amp;quot;+2^86&amp;quot;,0,0],[&amp;quot;+2^87&amp;quot;,0,0],[&amp;quot;+2^88&amp;quot;,0,0],[&amp;quot;+2^89&amp;quot;,0,0],[&amp;quot;+2^90&amp;quot;,0,0],[&amp;quot;+2^91&amp;quot;,0,0],[&amp;quot;+2^92&amp;quot;,0,0],[&amp;quot;+2^93&amp;quot;,0,0],[&amp;quot;+2^94&amp;quot;,0,0],[&amp;quot;+2^95&amp;quot;,0,0],[&amp;quot;+2^96&amp;quot;,0,0],[&amp;quot;+2^97&amp;quot;,0,0],[&amp;quot;+2^98&amp;quot;,0,0],[&amp;quot;+2^99&amp;quot;,0,0],[&amp;quot;+2^100&amp;quot;,0,0],[&amp;quot;+2^101&amp;quot;,0,0],[&amp;quot;+2^102&amp;quot;,0,0],[&amp;quot;+2^103&amp;quot;,0,0],[&amp;quot;+2^104&amp;quot;,0,0],[&amp;quot;+2^105&amp;quot;,0,0],[&amp;quot;+2^106&amp;quot;,0,0],[&amp;quot;+2^107&amp;quot;,0,0],[&amp;quot;+2^108&amp;quot;,0,0],[&amp;quot;+2^109&amp;quot;,0,0],[&amp;quot;+2^110&amp;quot;,0,0],[&amp;quot;+2^111&amp;quot;,0,0],[&amp;quot;+2^112&amp;quot;,0,0],[&amp;quot;+2^113&amp;quot;,0,0],[&amp;quot;+2^114&amp;quot;,0,0],[&amp;quot;+2^115&amp;quot;,0,0],[&amp;quot;+2^116&amp;quot;,0,0],[&amp;quot;+2^117&amp;quot;,0,0],[&amp;quot;+2^118&amp;quot;,0,0],[&amp;quot;+2^119&amp;quot;,0,0],[&amp;quot;+2^120&amp;quot;,0,0],[&amp;quot;+2^121&amp;quot;,0,0],[&amp;quot;+2^122&amp;quot;,0,0],[&amp;quot;+2^123&amp;quot;,0,0],[&amp;quot;+2^124&amp;quot;,0,0],[&amp;quot;+2^125&amp;quot;,0,0],[&amp;quot;+2^126&amp;quot;,0,0],[&amp;quot;+2^127&amp;quot;,0,0] ]&lt;br /&gt;
 }}&lt;br /&gt;
&lt;br /&gt;
== Localized Approximation Functions ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
!Id&lt;br /&gt;
!Formula&lt;br /&gt;
!ULP &amp;lt;= 2.0&lt;br /&gt;
!NAN&lt;br /&gt;
|-&lt;br /&gt;
|signum&lt;br /&gt;
|&amp;lt;math&amp;gt;\sgn{x}&amp;lt;/math&amp;gt;&lt;br /&gt;
| -inf &amp;lt; x &amp;lt; -8.31763&lt;br /&gt;
8.31763 &amp;lt; x &amp;lt; inf&lt;br /&gt;
| -&lt;br /&gt;
|-&lt;br /&gt;
|poly1&lt;br /&gt;
|&amp;lt;math&amp;gt;x&amp;lt;/math&amp;gt;&lt;br /&gt;
| -0.000704229 &amp;lt; x &amp;lt; 0.000704229&lt;br /&gt;
| -&lt;br /&gt;
|-&lt;br /&gt;
|poly3&lt;br /&gt;
|&amp;lt;math&amp;gt;x - \frac{1}{3} x^3&amp;lt;/math&amp;gt;&lt;br /&gt;
| -0.0291781 &amp;lt; x &amp;lt; 0.0291781 &lt;br /&gt;
| -&lt;br /&gt;
|-&lt;br /&gt;
|lampert7&lt;br /&gt;
|&amp;lt;math&amp;gt;\frac{x \cdot (135135 + x^2 \cdot (17325 + x^2 \cdot (378 + x^2)))}{135135 + x^2 \cdot (62370 + x^2 \cdot (3150 + 28 \cdot x^2))}&lt;br /&gt;
&amp;lt;/math&amp;gt;&lt;br /&gt;
| -0.000947416 &amp;lt; x &amp;lt; 0.000947207&lt;br /&gt;
| -inf &amp;lt; x &amp;lt; -1.51629e+06&lt;br /&gt;
1.51629e+06 &amp;lt; x &amp;lt; inf&lt;br /&gt;
|-&lt;br /&gt;
|pade&lt;br /&gt;
|&amp;lt;math&amp;gt;x + x \cdot \frac{x^2 \cdot \left( p_1 \cdot x^2 + p_0 \right)}{\left( x^2 + q_1 \right) \cdot x^2 + q_0} &lt;br /&gt;
&lt;br /&gt;
&amp;lt;/math&amp;gt; &amp;lt;math&amp;gt;\begin{align}&lt;br /&gt;
p_0 &amp;amp;= -0.2059432032 \\&lt;br /&gt;
p_1 &amp;amp;= -0.0009577527 \\&lt;br /&gt;
q_0 &amp;amp;= 0.6178299136 \\ &lt;br /&gt;
q_1 &amp;amp;= 0.25&lt;br /&gt;
\end{align}&lt;br /&gt;
&amp;lt;/math&amp;gt;&lt;br /&gt;
| -0.0016973 &amp;lt; x &amp;lt; 0.0016973 &lt;br /&gt;
| -inf &amp;lt; x &amp;lt; -6.14896e+18&lt;br /&gt;
6.14896e+18 &amp;lt; x &amp;lt; inf&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== External Links ==&lt;br /&gt;
&lt;br /&gt;
* https://yaikhom.com/2020-04-28-localised-approximation-of-hyperbolic-tangents.html#mjx-eqn-eqn7-th%20degree%20lambert%20approximant&lt;br /&gt;
*https://github.com/llvm/llvm-project/blob/main/libc/src/math/generic/tanhf.cpp&lt;br /&gt;
*https://forums.developer.nvidia.com/t/hardware-accelerated-tanh-on-turing/173291&lt;br /&gt;
[[Category:Numerical Precision]]&lt;/div&gt;</summary>
		<author><name>Timo.stripf</name></author>
	</entry>
	<entry>
		<id>https://www.emmtrix.com/w139/index.php?title=tanh_Software_Implementation&amp;diff=2926</id>
		<title>tanh Software Implementation</title>
		<link rel="alternate" type="text/html" href="https://www.emmtrix.com/w139/index.php?title=tanh_Software_Implementation&amp;diff=2926"/>
		<updated>2026-02-04T11:49:38Z</updated>

		<summary type="html">&lt;p&gt;Timo.stripf: /* Simple Approximation with Exponential Math Function */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;[[File:Hyperbolic Tangent.svg|thumb]]&lt;br /&gt;
The hyperbolic tangent function, commonly referred to as &amp;lt;code&amp;gt;tanh&amp;lt;/code&amp;gt;, is a mathematical function that maps real numbers to the range &amp;lt;math&amp;gt;(-1, 1)&amp;lt;/math&amp;gt;. It is defined as:&lt;br /&gt;
&lt;br /&gt;
:&amp;lt;math&amp;gt;\tanh(x) = \frac{e^x - e^{-x}}{e^x + e^{-x}}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
From a software implementation perspective, &amp;lt;code&amp;gt;tanh&amp;lt;/code&amp;gt; is a numerically challenging function due to the use of exponential functions, which can easily overflow for large inputs and suffer from catastrophic cancellation for very small values of &amp;lt;math&amp;gt;x&amp;lt;/math&amp;gt;. These issues are particularly relevant in single-precision floating-point (&amp;lt;code&amp;gt;float32&amp;lt;/code&amp;gt;) implementations, where both accuracy and performance are critical.&lt;br /&gt;
&lt;br /&gt;
In machine learning and deep learning, &amp;lt;code&amp;gt;tanh&amp;lt;/code&amp;gt; is widely used as an activation function because it introduces non-linearity and produces outputs centered around zero, which can improve gradient-based optimization. As a result, &amp;lt;code&amp;gt;tanh&amp;lt;/code&amp;gt; is often evaluated billions of times during training and inference, making efficient and numerically stable implementations essential.&lt;br /&gt;
&lt;br /&gt;
This article focuses on practical software implementations of &amp;lt;code&amp;gt;tanh&amp;lt;/code&amp;gt;, analyzing different computational formulations with respect to floating-point accuracy, ULP error, overflow behavior, and NaN generation. The goal is to provide a foundation for designing robust &amp;lt;code&amp;gt;tanh&amp;lt;/code&amp;gt; implementations suitable for performance-critical environments such as numerical libraries, embedded systems, and AI workloads.&lt;br /&gt;
&lt;br /&gt;
== Problem Overview ==&lt;br /&gt;
==Direct Exponential Formulations==&lt;br /&gt;
==Numerical Issues and Error Metrics==&lt;br /&gt;
== Simple Approximation with Exponential Math Function ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+&lt;br /&gt;
!Id&lt;br /&gt;
!Formula&lt;br /&gt;
!Highest ULP Error&lt;br /&gt;
!ULP &amp;lt;= 2.0&lt;br /&gt;
!NaN&lt;br /&gt;
|-&lt;br /&gt;
|exp_v1&lt;br /&gt;
|&amp;lt;math&amp;gt;\frac{e^x - e^{-x}}{e^x + e^{-x}}&amp;lt;/math&amp;gt;&lt;br /&gt;
|x &amp;lt; 0: 8.55638e+08 at -2.98023e-08 with 0x0p+0 instead of -0x1p-25&lt;br /&gt;
&lt;br /&gt;
x &amp;gt; 0: 8.55638e+08 at 2.98023e-08 with 0x0p+0 instead of 0x1p-25&lt;br /&gt;
| -88.7246 &amp;lt; x &amp;lt; -7.14197&lt;br /&gt;
7.14197 &amp;lt; x &amp;lt; 88.7246&lt;br /&gt;
| -inf &amp;lt; x &amp;lt; -88.7246&lt;br /&gt;
88.7246 &amp;lt; x &amp;lt; inf&lt;br /&gt;
|-&lt;br /&gt;
|exp_v2&lt;br /&gt;
|&amp;lt;math&amp;gt;\frac{e^{2x} - 1}{e^{2x} + 1}&amp;lt;/math&amp;gt;&lt;br /&gt;
|x &amp;lt; 0: 8.47249e+08 at -1.49012e-08 with 0x0p+0 instead of -0x1p-26&lt;br /&gt;
x &amp;gt; 0: 8.55638e+08 at 2.98023e-08 with 0x0p+0 instead of 0x1.fffffep-26&lt;br /&gt;
| -inf &amp;lt; x &amp;lt; -0.547821&lt;br /&gt;
0.254822 &amp;lt; x &amp;lt; 8.31787&lt;br /&gt;
&lt;br /&gt;
8.38379 &amp;lt; x &amp;lt; 44.3623&lt;br /&gt;
|44.3623 &amp;lt; x &amp;lt; inf&lt;br /&gt;
|-&lt;br /&gt;
|exp_v3&lt;br /&gt;
|&amp;lt;math&amp;gt;1 - \frac{2}{e^{2x} + 1}&amp;lt;/math&amp;gt;&lt;br /&gt;
|x &amp;lt; 0: 8.59832e+08 at -4.47035e-08 with 0x0p+0 instead of -0x1.8p-25&lt;br /&gt;
x &amp;gt; 0: 8.68221e+08 at 8.9407e-08 with 0x0p+0 instead of 0x1.7ffffcp-24&lt;br /&gt;
| -inf &amp;lt; x &amp;lt; -6.26233&lt;br /&gt;
0.346382 &amp;lt; x &amp;lt; inf&lt;br /&gt;
| -&lt;br /&gt;
|-&lt;br /&gt;
|expm1&lt;br /&gt;
|&amp;lt;math&amp;gt;\frac{expm1(2x)}{expm1(2x) + 2}&amp;lt;/math&amp;gt; &lt;br /&gt;
|x &amp;lt; 0: 2.49585 at -3.95627 with -0x1.ffa00ap-1 instead of -0x1.ffa00ep-1&lt;br /&gt;
x &amp;gt; 0: 2.41657 at 0.0155837 with 0x1.fe9b66p-7 instead of 0x1.fe9b62p-7&lt;br /&gt;
| -inf &amp;lt; x &amp;lt; -5.57227&lt;br /&gt;
-0.42981 &amp;lt; x &amp;lt; 0.00777948&lt;br /&gt;
&lt;br /&gt;
0.125351 &amp;lt; x &amp;lt; 44.3623&lt;br /&gt;
|44.3623 &amp;lt; x &amp;lt; inf&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
The first equation is the definition of the hyperbolic tangent function. This equation requires two exponentials with different values. Since exponentials are expensive to compute, we can use the second equation to reduce the number of exponentials to one.&lt;br /&gt;
&lt;br /&gt;
The second equation has the drawback that both the numerator and the denominator can become infinity for large values of x which results in NaN. That happens for &amp;lt;math&amp;gt;|x| &amp;gt; \log(FLT\_MAX)/2 \approx 44&amp;lt;/math&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
The third equation solves this problem but has a slightly higher error for &amp;lt;math&amp;gt;x \approx 2^{-25}&amp;lt;/math&amp;gt;. In the following graph you can see the error chart for equation 3. The error charts for the other two equations are very similar. All three equations have an error of up to 16 mio ULPs for &amp;lt;math&amp;gt;x &amp;lt; 2^{-5}&amp;lt;/math&amp;gt; which means that up to 24 bits could be wrong.&lt;br /&gt;
&lt;br /&gt;
=== exp_v2 ===&lt;br /&gt;
&lt;br /&gt;
{{#widget:FloatErrorGraph|id=tanhf_exp_v2&lt;br /&gt;
  |data=[ [&amp;quot;-2^127&amp;quot;,0,1.17549e-38],[&amp;quot;-2^126&amp;quot;,0,1.17549e-38],[&amp;quot;-2^125&amp;quot;,0,1.17549e-38],[&amp;quot;-2^124&amp;quot;,0,1.17549e-38],[&amp;quot;-2^123&amp;quot;,0,1.17549e-38],[&amp;quot;-2^122&amp;quot;,0,1.17549e-38],[&amp;quot;-2^121&amp;quot;,0,1.17549e-38],[&amp;quot;-2^120&amp;quot;,0,1.17549e-38],[&amp;quot;-2^119&amp;quot;,0,1.17549e-38],[&amp;quot;-2^118&amp;quot;,0,1.17549e-38],[&amp;quot;-2^117&amp;quot;,0,1.17549e-38],[&amp;quot;-2^116&amp;quot;,0,1.17549e-38],[&amp;quot;-2^115&amp;quot;,0,1.17549e-38],[&amp;quot;-2^114&amp;quot;,0,1.17549e-38],[&amp;quot;-2^113&amp;quot;,0,1.17549e-38],[&amp;quot;-2^112&amp;quot;,0,1.17549e-38],[&amp;quot;-2^111&amp;quot;,0,1.17549e-38],[&amp;quot;-2^110&amp;quot;,0,1.17549e-38],[&amp;quot;-2^109&amp;quot;,0,1.17549e-38],[&amp;quot;-2^108&amp;quot;,0,1.17549e-38],[&amp;quot;-2^107&amp;quot;,0,1.17549e-38],[&amp;quot;-2^106&amp;quot;,0,1.17549e-38],[&amp;quot;-2^105&amp;quot;,0,1.17549e-38],[&amp;quot;-2^104&amp;quot;,0,1.17549e-38],[&amp;quot;-2^103&amp;quot;,0,1.17549e-38],[&amp;quot;-2^102&amp;quot;,0,1.17549e-38],[&amp;quot;-2^101&amp;quot;,0,1.17549e-38],[&amp;quot;-2^100&amp;quot;,0,1.17549e-38],[&amp;quot;-2^99&amp;quot;,0,1.17549e-38],[&amp;quot;-2^98&amp;quot;,0,1.17549e-38],[&amp;quot;-2^97&amp;quot;,0,1.17549e-38],[&amp;quot;-2^96&amp;quot;,0,1.17549e-38],[&amp;quot;-2^95&amp;quot;,0,1.17549e-38],[&amp;quot;-2^94&amp;quot;,0,1.17549e-38],[&amp;quot;-2^93&amp;quot;,0,1.17549e-38],[&amp;quot;-2^92&amp;quot;,0,1.17549e-38],[&amp;quot;-2^91&amp;quot;,0,1.17549e-38],[&amp;quot;-2^90&amp;quot;,0,1.17549e-38],[&amp;quot;-2^89&amp;quot;,0,1.17549e-38],[&amp;quot;-2^88&amp;quot;,0,1.17549e-38],[&amp;quot;-2^87&amp;quot;,0,1.17549e-38],[&amp;quot;-2^86&amp;quot;,0,1.17549e-38],[&amp;quot;-2^85&amp;quot;,0,1.17549e-38],[&amp;quot;-2^84&amp;quot;,0,1.17549e-38],[&amp;quot;-2^83&amp;quot;,0,1.17549e-38],[&amp;quot;-2^82&amp;quot;,0,1.17549e-38],[&amp;quot;-2^81&amp;quot;,0,1.17549e-38],[&amp;quot;-2^80&amp;quot;,0,1.17549e-38],[&amp;quot;-2^79&amp;quot;,0,1.17549e-38],[&amp;quot;-2^78&amp;quot;,0,1.17549e-38],[&amp;quot;-2^77&amp;quot;,0,1.17549e-38],[&amp;quot;-2^76&amp;quot;,0,1.17549e-38],[&amp;quot;-2^75&amp;quot;,0,1.17549e-38],[&amp;quot;-2^74&amp;quot;,0,1.17549e-38],[&amp;quot;-2^73&amp;quot;,0,1.17549e-38],[&amp;quot;-2^72&amp;quot;,0,1.17549e-38],[&amp;quot;-2^71&amp;quot;,0,1.17549e-38],[&amp;quot;-2^70&amp;quot;,0,1.17549e-38],[&amp;quot;-2^69&amp;quot;,0,1.17549e-38],[&amp;quot;-2^68&amp;quot;,0,1.17549e-38],[&amp;quot;-2^67&amp;quot;,0,1.17549e-38],[&amp;quot;-2^66&amp;quot;,0,1.17549e-38],[&amp;quot;-2^65&amp;quot;,0,1.17549e-38],[&amp;quot;-2^64&amp;quot;,0,1.17549e-38],[&amp;quot;-2^63&amp;quot;,0,1.17549e-38],[&amp;quot;-2^62&amp;quot;,0,1.17549e-38],[&amp;quot;-2^61&amp;quot;,0,1.17549e-38],[&amp;quot;-2^60&amp;quot;,0,1.17549e-38],[&amp;quot;-2^59&amp;quot;,0,1.17549e-38],[&amp;quot;-2^58&amp;quot;,0,1.17549e-38],[&amp;quot;-2^57&amp;quot;,0,1.17549e-38],[&amp;quot;-2^56&amp;quot;,0,1.17549e-38],[&amp;quot;-2^55&amp;quot;,0,1.17549e-38],[&amp;quot;-2^54&amp;quot;,0,1.17549e-38],[&amp;quot;-2^53&amp;quot;,0,1.17549e-38],[&amp;quot;-2^52&amp;quot;,0,1.17549e-38],[&amp;quot;-2^51&amp;quot;,0,1.17549e-38],[&amp;quot;-2^50&amp;quot;,0,1.17549e-38],[&amp;quot;-2^49&amp;quot;,0,1.17549e-38],[&amp;quot;-2^48&amp;quot;,0,1.17549e-38],[&amp;quot;-2^47&amp;quot;,0,1.17549e-38],[&amp;quot;-2^46&amp;quot;,0,1.17549e-38],[&amp;quot;-2^45&amp;quot;,0,1.17549e-38],[&amp;quot;-2^44&amp;quot;,0,1.17549e-38],[&amp;quot;-2^43&amp;quot;,0,1.17549e-38],[&amp;quot;-2^42&amp;quot;,0,1.17549e-38],[&amp;quot;-2^41&amp;quot;,0,1.17549e-38],[&amp;quot;-2^40&amp;quot;,0,1.17549e-38],[&amp;quot;-2^39&amp;quot;,0,1.17549e-38],[&amp;quot;-2^38&amp;quot;,0,1.17549e-38],[&amp;quot;-2^37&amp;quot;,0,1.17549e-38],[&amp;quot;-2^36&amp;quot;,0,1.17549e-38],[&amp;quot;-2^35&amp;quot;,0,1.17549e-38],[&amp;quot;-2^34&amp;quot;,0,1.17549e-38],[&amp;quot;-2^33&amp;quot;,0,1.17549e-38],[&amp;quot;-2^32&amp;quot;,0,1.17549e-38],[&amp;quot;-2^31&amp;quot;,0,1.17549e-38],[&amp;quot;-2^30&amp;quot;,0,1.17549e-38],[&amp;quot;-2^29&amp;quot;,0,1.17549e-38],[&amp;quot;-2^28&amp;quot;,0,1.17549e-38],[&amp;quot;-2^27&amp;quot;,0,1.17549e-38],[&amp;quot;-2^26&amp;quot;,0,1.17549e-38],[&amp;quot;-2^25&amp;quot;,0,1.17549e-38],[&amp;quot;-2^24&amp;quot;,0,1.17549e-38],[&amp;quot;-2^23&amp;quot;,0,1.17549e-38],[&amp;quot;-2^22&amp;quot;,0,1.17549e-38],[&amp;quot;-2^21&amp;quot;,0,1.17549e-38],[&amp;quot;-2^20&amp;quot;,0,1.17549e-38],[&amp;quot;-2^19&amp;quot;,0,1.17549e-38],[&amp;quot;-2^18&amp;quot;,0,1.17549e-38],[&amp;quot;-2^17&amp;quot;,0,1.17549e-38],[&amp;quot;-2^16&amp;quot;,0,1.17549e-38],[&amp;quot;-2^15&amp;quot;,0,1.17549e-38],[&amp;quot;-2^14&amp;quot;,0,1.17549e-38],[&amp;quot;-2^13&amp;quot;,0,1.17549e-38],[&amp;quot;-2^12&amp;quot;,0,1.17549e-38],[&amp;quot;-2^11&amp;quot;,0,1.17549e-38],[&amp;quot;-2^10&amp;quot;,0,1.17549e-38],[&amp;quot;-2^9&amp;quot;,0,1.17549e-38],[&amp;quot;-2^8&amp;quot;,0,1.17549e-38],[&amp;quot;-2^7&amp;quot;,0,1.17549e-38],[&amp;quot;-2^6&amp;quot;,0,1.17549e-38],[&amp;quot;-2^5&amp;quot;,0,1.17549e-38],[&amp;quot;-2^4&amp;quot;,2.53131e-14,4.24683e-07],[&amp;quot;-2^3&amp;quot;,5.95888e-08,0.999734],[&amp;quot;-2^2&amp;quot;,8.86696e-08,1.48763],[&amp;quot;-2^1&amp;quot;,8.91609e-08,1.49587],[&amp;quot;-2^0&amp;quot;,8.73743e-08,1.4659],[&amp;quot;-2^-1&amp;quot;,8.65382e-08,2.13222],[&amp;quot;-2^-2&amp;quot;,6.18481e-08,2.61428],[&amp;quot;-2^-3&amp;quot;,3.88071e-08,3.08594],[&amp;quot;-2^-4&amp;quot;,2.6234e-08,4.23594],[&amp;quot;-2^-5&amp;quot;,2.02799e-08,8.19726],[&amp;quot;-2^-6&amp;quot;,1.75183e-08,12.4495],[&amp;quot;-2^-7&amp;quot;,1.61937e-08,17.3878],[&amp;quot;-2^-8&amp;quot;,1.5566e-08,33.4278],[&amp;quot;-2^-9&amp;quot;,1.51688e-08,65.1495],[&amp;quot;-2^-10&amp;quot;,1.50281e-08,129.09],[&amp;quot;-2^-11&amp;quot;,1.49613e-08,257.033],[&amp;quot;-2^-12&amp;quot;,1.49296e-08,512.977],[&amp;quot;-2^-13&amp;quot;,1.49174e-08,1025.12],[&amp;quot;-2^-14&amp;quot;,1.4909e-08,2049.08],[&amp;quot;-2^-15&amp;quot;,1.49049e-08,4097.02],[&amp;quot;-2^-16&amp;quot;,1.4903e-08,8193],[&amp;quot;-2^-17&amp;quot;,1.49021e-08,16385],[&amp;quot;-2^-18&amp;quot;,1.48862e-08,32735],[&amp;quot;-2^-19&amp;quot;,1.48973e-08,65519],[&amp;quot;-2^-20&amp;quot;,1.49001e-08,131063],[&amp;quot;-2^-21&amp;quot;,1.49009e-08,262139],[&amp;quot;-2^-22&amp;quot;,1.49011e-08,524285],[&amp;quot;-2^-23&amp;quot;,1.49011e-08,1.04857e+06],[&amp;quot;-2^-24&amp;quot;,1.49011e-08,2.09715e+06],[&amp;quot;-2^-25&amp;quot;,1.49012e-08,4.1943e+06],[&amp;quot;-2^-26&amp;quot;,1.49007e-08,8.38835e+06],[&amp;quot;-2^-27&amp;quot;,1.49012e-08,8.47249e+08],[&amp;quot;-2^-28&amp;quot;,7.45058e-09,8.38861e+08],[&amp;quot;-2^-29&amp;quot;,3.72529e-09,8.30472e+08],[&amp;quot;-2^-30&amp;quot;,1.86265e-09,8.22084e+08],[&amp;quot;-2^-31&amp;quot;,9.31323e-10,8.13695e+08],[&amp;quot;-2^-32&amp;quot;,4.65661e-10,8.05306e+08],[&amp;quot;-2^-33&amp;quot;,2.32831e-10,7.96918e+08],[&amp;quot;-2^-34&amp;quot;,1.16415e-10,7.88529e+08],[&amp;quot;-2^-35&amp;quot;,5.82077e-11,7.80141e+08],[&amp;quot;-2^-36&amp;quot;,2.91038e-11,7.71752e+08],[&amp;quot;-2^-37&amp;quot;,1.45519e-11,7.63363e+08],[&amp;quot;-2^-38&amp;quot;,7.27596e-12,7.54975e+08],[&amp;quot;-2^-39&amp;quot;,3.63798e-12,7.46586e+08],[&amp;quot;-2^-40&amp;quot;,1.81899e-12,7.38198e+08],[&amp;quot;-2^-41&amp;quot;,9.09495e-13,7.29809e+08],[&amp;quot;-2^-42&amp;quot;,4.54747e-13,7.2142e+08],[&amp;quot;-2^-43&amp;quot;,2.27374e-13,7.13032e+08],[&amp;quot;-2^-44&amp;quot;,1.13687e-13,7.04643e+08],[&amp;quot;-2^-45&amp;quot;,5.68434e-14,6.96254e+08],[&amp;quot;-2^-46&amp;quot;,2.84217e-14,6.87866e+08],[&amp;quot;-2^-47&amp;quot;,1.42109e-14,6.79477e+08],[&amp;quot;-2^-48&amp;quot;,7.10543e-15,6.71089e+08],[&amp;quot;-2^-49&amp;quot;,3.55271e-15,6.627e+08],[&amp;quot;-2^-50&amp;quot;,1.77636e-15,6.54311e+08],[&amp;quot;-2^-51&amp;quot;,8.88178e-16,6.45923e+08],[&amp;quot;-2^-52&amp;quot;,4.44089e-16,6.37534e+08],[&amp;quot;-2^-53&amp;quot;,2.22045e-16,6.29146e+08],[&amp;quot;-2^-54&amp;quot;,1.11022e-16,6.20757e+08],[&amp;quot;-2^-55&amp;quot;,5.55111e-17,6.12368e+08],[&amp;quot;-2^-56&amp;quot;,2.77556e-17,6.0398e+08],[&amp;quot;-2^-57&amp;quot;,1.38778e-17,5.95591e+08],[&amp;quot;-2^-58&amp;quot;,6.93889e-18,5.87203e+08],[&amp;quot;-2^-59&amp;quot;,3.46945e-18,5.78814e+08],[&amp;quot;-2^-60&amp;quot;,1.73472e-18,5.70425e+08],[&amp;quot;-2^-61&amp;quot;,8.67362e-19,5.62037e+08],[&amp;quot;-2^-62&amp;quot;,4.33681e-19,5.53648e+08],[&amp;quot;-2^-63&amp;quot;,2.1684e-19,5.4526e+08],[&amp;quot;-2^-64&amp;quot;,1.0842e-19,5.36871e+08],[&amp;quot;-2^-65&amp;quot;,5.42101e-20,5.28482e+08],[&amp;quot;-2^-66&amp;quot;,2.71051e-20,5.20094e+08],[&amp;quot;-2^-67&amp;quot;,1.35525e-20,5.11705e+08],[&amp;quot;-2^-68&amp;quot;,6.77626e-21,5.03316e+08],[&amp;quot;-2^-69&amp;quot;,3.38813e-21,4.94928e+08],[&amp;quot;-2^-70&amp;quot;,1.69407e-21,4.86539e+08],[&amp;quot;-2^-71&amp;quot;,8.47033e-22,4.78151e+08],[&amp;quot;-2^-72&amp;quot;,4.23516e-22,4.69762e+08],[&amp;quot;-2^-73&amp;quot;,2.11758e-22,4.61373e+08],[&amp;quot;-2^-74&amp;quot;,1.05879e-22,4.52985e+08],[&amp;quot;-2^-75&amp;quot;,5.29396e-23,4.44596e+08],[&amp;quot;-2^-76&amp;quot;,2.64698e-23,4.36208e+08],[&amp;quot;-2^-77&amp;quot;,1.32349e-23,4.27819e+08],[&amp;quot;-2^-78&amp;quot;,6.61744e-24,4.1943e+08],[&amp;quot;-2^-79&amp;quot;,3.30872e-24,4.11042e+08],[&amp;quot;-2^-80&amp;quot;,1.65436e-24,4.02653e+08],[&amp;quot;-2^-81&amp;quot;,8.27181e-25,3.94265e+08],[&amp;quot;-2^-82&amp;quot;,4.1359e-25,3.85876e+08],[&amp;quot;-2^-83&amp;quot;,2.06795e-25,3.77487e+08],[&amp;quot;-2^-84&amp;quot;,1.03398e-25,3.69099e+08],[&amp;quot;-2^-85&amp;quot;,5.16988e-26,3.6071e+08],[&amp;quot;-2^-86&amp;quot;,2.58494e-26,3.52322e+08],[&amp;quot;-2^-87&amp;quot;,1.29247e-26,3.43933e+08],[&amp;quot;-2^-88&amp;quot;,6.46235e-27,3.35544e+08],[&amp;quot;-2^-89&amp;quot;,3.23117e-27,3.27156e+08],[&amp;quot;-2^-90&amp;quot;,1.61559e-27,3.18767e+08],[&amp;quot;-2^-91&amp;quot;,8.07794e-28,3.10378e+08],[&amp;quot;-2^-92&amp;quot;,4.03897e-28,3.0199e+08],[&amp;quot;-2^-93&amp;quot;,2.01948e-28,2.93601e+08],[&amp;quot;-2^-94&amp;quot;,1.00974e-28,2.85213e+08],[&amp;quot;-2^-95&amp;quot;,5.04871e-29,2.76824e+08],[&amp;quot;-2^-96&amp;quot;,2.52435e-29,2.68435e+08],[&amp;quot;-2^-97&amp;quot;,1.26218e-29,2.60047e+08],[&amp;quot;-2^-98&amp;quot;,6.31089e-30,2.51658e+08],[&amp;quot;-2^-99&amp;quot;,3.15544e-30,2.4327e+08],[&amp;quot;-2^-100&amp;quot;,1.57772e-30,2.34881e+08],[&amp;quot;-2^-101&amp;quot;,7.88861e-31,2.26492e+08],[&amp;quot;-2^-102&amp;quot;,3.9443e-31,2.18104e+08],[&amp;quot;-2^-103&amp;quot;,1.97215e-31,2.09715e+08],[&amp;quot;-2^-104&amp;quot;,9.86076e-32,2.01327e+08],[&amp;quot;-2^-105&amp;quot;,4.93038e-32,1.92938e+08],[&amp;quot;-2^-106&amp;quot;,2.46519e-32,1.84549e+08],[&amp;quot;-2^-107&amp;quot;,1.2326e-32,1.76161e+08],[&amp;quot;-2^-108&amp;quot;,6.16298e-33,1.67772e+08],[&amp;quot;-2^-109&amp;quot;,3.08149e-33,1.59384e+08],[&amp;quot;-2^-110&amp;quot;,1.54074e-33,1.50995e+08],[&amp;quot;-2^-111&amp;quot;,7.70372e-34,1.42606e+08],[&amp;quot;-2^-112&amp;quot;,3.85186e-34,1.34218e+08],[&amp;quot;-2^-113&amp;quot;,1.92593e-34,1.25829e+08],[&amp;quot;-2^-114&amp;quot;,9.62965e-35,1.17441e+08],[&amp;quot;-2^-115&amp;quot;,4.81482e-35,1.09052e+08],[&amp;quot;-2^-116&amp;quot;,2.40741e-35,1.00663e+08],[&amp;quot;-2^-117&amp;quot;,1.20371e-35,9.22747e+07],[&amp;quot;-2^-118&amp;quot;,6.01853e-36,8.38861e+07],[&amp;quot;-2^-119&amp;quot;,3.00927e-36,7.54975e+07],[&amp;quot;-2^-120&amp;quot;,1.50463e-36,6.71089e+07],[&amp;quot;-2^-121&amp;quot;,7.52316e-37,5.87203e+07],[&amp;quot;-2^-122&amp;quot;,3.76158e-37,5.03316e+07],[&amp;quot;-2^-123&amp;quot;,1.88079e-37,4.1943e+07],[&amp;quot;-2^-124&amp;quot;,9.40395e-38,3.35544e+07],[&amp;quot;-2^-125&amp;quot;,4.70198e-38,2.51658e+07],[&amp;quot;-2^-126&amp;quot;,2.35099e-38,1.67772e+07],[&amp;quot;-SN&amp;quot;,1.17546e-38,8.38835e+06],[&amp;quot;0&amp;quot;,0,1.17549e-38],[&amp;quot;+SN&amp;quot;,1.17546e-38,8.38835e+06],[&amp;quot;+2^-126&amp;quot;,2.35095e-38,1.6777e+07],[&amp;quot;+2^-125&amp;quot;,4.70191e-38,2.51656e+07],[&amp;quot;+2^-124&amp;quot;,9.40381e-38,3.35542e+07],[&amp;quot;+2^-123&amp;quot;,1.88076e-37,4.19428e+07],[&amp;quot;+2^-122&amp;quot;,3.76152e-37,5.03314e+07],[&amp;quot;+2^-121&amp;quot;,7.52305e-37,5.872e+07],[&amp;quot;+2^-120&amp;quot;,1.50461e-36,6.71086e+07],[&amp;quot;+2^-119&amp;quot;,3.00922e-36,7.54972e+07],[&amp;quot;+2^-118&amp;quot;,6.01844e-36,8.38858e+07],[&amp;quot;+2^-117&amp;quot;,1.20369e-35,9.22744e+07],[&amp;quot;+2^-116&amp;quot;,2.40738e-35,1.00663e+08],[&amp;quot;+2^-115&amp;quot;,4.81475e-35,1.09052e+08],[&amp;quot;+2^-114&amp;quot;,9.6295e-35,1.1744e+08],[&amp;quot;+2^-113&amp;quot;,1.9259e-34,1.25829e+08],[&amp;quot;+2^-112&amp;quot;,3.8518e-34,1.34217e+08],[&amp;quot;+2^-111&amp;quot;,7.7036e-34,1.42606e+08],[&amp;quot;+2^-110&amp;quot;,1.54072e-33,1.50995e+08],[&amp;quot;+2^-109&amp;quot;,3.08144e-33,1.59383e+08],[&amp;quot;+2^-108&amp;quot;,6.16288e-33,1.67772e+08],[&amp;quot;+2^-107&amp;quot;,1.23258e-32,1.76161e+08],[&amp;quot;+2^-106&amp;quot;,2.46515e-32,1.84549e+08],[&amp;quot;+2^-105&amp;quot;,4.93031e-32,1.92938e+08],[&amp;quot;+2^-104&amp;quot;,9.86061e-32,2.01326e+08],[&amp;quot;+2^-103&amp;quot;,1.97212e-31,2.09715e+08],[&amp;quot;+2^-102&amp;quot;,3.94424e-31,2.18104e+08],[&amp;quot;+2^-101&amp;quot;,7.88849e-31,2.26492e+08],[&amp;quot;+2^-100&amp;quot;,1.5777e-30,2.34881e+08],[&amp;quot;+2^-99&amp;quot;,3.1554e-30,2.43269e+08],[&amp;quot;+2^-98&amp;quot;,6.31079e-30,2.51658e+08],[&amp;quot;+2^-97&amp;quot;,1.26216e-29,2.60047e+08],[&amp;quot;+2^-96&amp;quot;,2.52432e-29,2.68435e+08],[&amp;quot;+2^-95&amp;quot;,5.04863e-29,2.76824e+08],[&amp;quot;+2^-94&amp;quot;,1.00973e-28,2.85212e+08],[&amp;quot;+2^-93&amp;quot;,2.01945e-28,2.93601e+08],[&amp;quot;+2^-92&amp;quot;,4.03891e-28,3.0199e+08],[&amp;quot;+2^-91&amp;quot;,8.07781e-28,3.10378e+08],[&amp;quot;+2^-90&amp;quot;,1.61556e-27,3.18767e+08],[&amp;quot;+2^-89&amp;quot;,3.23112e-27,3.27155e+08],[&amp;quot;+2^-88&amp;quot;,6.46225e-27,3.35544e+08],[&amp;quot;+2^-87&amp;quot;,1.29245e-26,3.43933e+08],[&amp;quot;+2^-86&amp;quot;,2.5849e-26,3.52321e+08],[&amp;quot;+2^-85&amp;quot;,5.1698e-26,3.6071e+08],[&amp;quot;+2^-84&amp;quot;,1.03396e-25,3.69098e+08],[&amp;quot;+2^-83&amp;quot;,2.06792e-25,3.77487e+08],[&amp;quot;+2^-82&amp;quot;,4.13584e-25,3.85876e+08],[&amp;quot;+2^-81&amp;quot;,8.27168e-25,3.94264e+08],[&amp;quot;+2^-80&amp;quot;,1.65434e-24,4.02653e+08],[&amp;quot;+2^-79&amp;quot;,3.30867e-24,4.11042e+08],[&amp;quot;+2^-78&amp;quot;,6.61734e-24,4.1943e+08],[&amp;quot;+2^-77&amp;quot;,1.32347e-23,4.27819e+08],[&amp;quot;+2^-76&amp;quot;,2.64694e-23,4.36207e+08],[&amp;quot;+2^-75&amp;quot;,5.29388e-23,4.44596e+08],[&amp;quot;+2^-74&amp;quot;,1.05878e-22,4.52985e+08],[&amp;quot;+2^-73&amp;quot;,2.11755e-22,4.61373e+08],[&amp;quot;+2^-72&amp;quot;,4.2351e-22,4.69762e+08],[&amp;quot;+2^-71&amp;quot;,8.4702e-22,4.7815e+08],[&amp;quot;+2^-70&amp;quot;,1.69404e-21,4.86539e+08],[&amp;quot;+2^-69&amp;quot;,3.38808e-21,4.94928e+08],[&amp;quot;+2^-68&amp;quot;,6.77616e-21,5.03316e+08],[&amp;quot;+2^-67&amp;quot;,1.35523e-20,5.11705e+08],[&amp;quot;+2^-66&amp;quot;,2.71046e-20,5.20093e+08],[&amp;quot;+2^-65&amp;quot;,5.42093e-20,5.28482e+08],[&amp;quot;+2^-64&amp;quot;,1.08419e-19,5.36871e+08],[&amp;quot;+2^-63&amp;quot;,2.16837e-19,5.45259e+08],[&amp;quot;+2^-62&amp;quot;,4.33674e-19,5.53648e+08],[&amp;quot;+2^-61&amp;quot;,8.67349e-19,5.62036e+08],[&amp;quot;+2^-60&amp;quot;,1.7347e-18,5.70425e+08],[&amp;quot;+2^-59&amp;quot;,3.46939e-18,5.78814e+08],[&amp;quot;+2^-58&amp;quot;,6.93879e-18,5.87202e+08],[&amp;quot;+2^-57&amp;quot;,1.38776e-17,5.95591e+08],[&amp;quot;+2^-56&amp;quot;,2.77552e-17,6.0398e+08],[&amp;quot;+2^-55&amp;quot;,5.55103e-17,6.12368e+08],[&amp;quot;+2^-54&amp;quot;,1.11021e-16,6.20757e+08],[&amp;quot;+2^-53&amp;quot;,2.22041e-16,6.29145e+08],[&amp;quot;+2^-52&amp;quot;,4.44082e-16,6.37534e+08],[&amp;quot;+2^-51&amp;quot;,8.88165e-16,6.45923e+08],[&amp;quot;+2^-50&amp;quot;,1.77633e-15,6.54311e+08],[&amp;quot;+2^-49&amp;quot;,3.55266e-15,6.627e+08],[&amp;quot;+2^-48&amp;quot;,7.10532e-15,6.71088e+08],[&amp;quot;+2^-47&amp;quot;,1.42106e-14,6.79477e+08],[&amp;quot;+2^-46&amp;quot;,2.84213e-14,6.87866e+08],[&amp;quot;+2^-45&amp;quot;,5.68426e-14,6.96254e+08],[&amp;quot;+2^-44&amp;quot;,1.13685e-13,7.04643e+08],[&amp;quot;+2^-43&amp;quot;,2.2737e-13,7.13031e+08],[&amp;quot;+2^-42&amp;quot;,4.5474e-13,7.2142e+08],[&amp;quot;+2^-41&amp;quot;,9.09481e-13,7.29809e+08],[&amp;quot;+2^-40&amp;quot;,1.81896e-12,7.38197e+08],[&amp;quot;+2^-39&amp;quot;,3.63792e-12,7.46586e+08],[&amp;quot;+2^-38&amp;quot;,7.27585e-12,7.54974e+08],[&amp;quot;+2^-37&amp;quot;,1.45517e-11,7.63363e+08],[&amp;quot;+2^-36&amp;quot;,2.91034e-11,7.71752e+08],[&amp;quot;+2^-35&amp;quot;,5.82068e-11,7.8014e+08],[&amp;quot;+2^-34&amp;quot;,1.16414e-10,7.88529e+08],[&amp;quot;+2^-33&amp;quot;,2.32827e-10,7.96918e+08],[&amp;quot;+2^-32&amp;quot;,4.65654e-10,8.05306e+08],[&amp;quot;+2^-31&amp;quot;,9.31308e-10,8.13695e+08],[&amp;quot;+2^-30&amp;quot;,1.86262e-09,8.22083e+08],[&amp;quot;+2^-29&amp;quot;,3.72523e-09,8.30472e+08],[&amp;quot;+2^-28&amp;quot;,7.45047e-09,8.38861e+08],[&amp;quot;+2^-27&amp;quot;,1.49009e-08,8.47249e+08],[&amp;quot;+2^-26&amp;quot;,2.98019e-08,8.55638e+08],[&amp;quot;+2^-25&amp;quot;,2.98023e-08,8.38861e+06],[&amp;quot;+2^-24&amp;quot;,2.98023e-08,4.1943e+06],[&amp;quot;+2^-23&amp;quot;,2.98023e-08,2.09715e+06],[&amp;quot;+2^-22&amp;quot;,2.98023e-08,1.04857e+06],[&amp;quot;+2^-21&amp;quot;,2.98021e-08,524284],[&amp;quot;+2^-20&amp;quot;,2.98014e-08,262136],[&amp;quot;+2^-19&amp;quot;,2.97987e-08,131056],[&amp;quot;+2^-18&amp;quot;,2.97878e-08,65504],[&amp;quot;+2^-17&amp;quot;,2.98014e-08,32767],[&amp;quot;+2^-16&amp;quot;,2.98005e-08,16384],[&amp;quot;+2^-15&amp;quot;,2.98023e-08,8192],[&amp;quot;+2^-14&amp;quot;,2.98025e-08,4351.99],[&amp;quot;+2^-13&amp;quot;,2.98044e-08,2815.96],[&amp;quot;+2^-12&amp;quot;,2.98091e-08,1024.23],[&amp;quot;+2^-11&amp;quot;,2.9812e-08,512.167],[&amp;quot;+2^-10&amp;quot;,2.98154e-08,256.113],[&amp;quot;+2^-9&amp;quot;,2.98591e-08,128.244],[&amp;quot;+2^-8&amp;quot;,3.00204e-08,85.3328],[&amp;quot;+2^-7&amp;quot;,3.01947e-08,43.675],[&amp;quot;+2^-6&amp;quot;,3.05713e-08,30.45],[&amp;quot;+2^-5&amp;quot;,3.14319e-08,8.43743],[&amp;quot;+2^-4&amp;quot;,3.31291e-08,7.77058],[&amp;quot;+2^-3&amp;quot;,3.58597e-08,3.99659],[&amp;quot;+2^-2&amp;quot;,4.14639e-08,2.29246],[&amp;quot;+2^-1&amp;quot;,8.02409e-08,1.34622],[&amp;quot;+2^0&amp;quot;,8.66201e-08,1.45324],[&amp;quot;+2^1&amp;quot;,8.8654e-08,1.48737],[&amp;quot;+2^2&amp;quot;,6.7976e-08,1.14045],[&amp;quot;+2^3&amp;quot;,1.33955e-07,2.2474],[&amp;quot;+2^4&amp;quot;,2.53131e-14,4.24683e-07],[&amp;quot;+2^5&amp;quot;,0,null],[&amp;quot;+2^6&amp;quot;,0,null],[&amp;quot;+2^7&amp;quot;,0,null],[&amp;quot;+2^8&amp;quot;,0,null],[&amp;quot;+2^9&amp;quot;,0,null],[&amp;quot;+2^10&amp;quot;,0,null],[&amp;quot;+2^11&amp;quot;,0,null],[&amp;quot;+2^12&amp;quot;,0,null],[&amp;quot;+2^13&amp;quot;,0,null],[&amp;quot;+2^14&amp;quot;,0,null],[&amp;quot;+2^15&amp;quot;,0,null],[&amp;quot;+2^16&amp;quot;,0,null],[&amp;quot;+2^17&amp;quot;,0,null],[&amp;quot;+2^18&amp;quot;,0,null],[&amp;quot;+2^19&amp;quot;,0,null],[&amp;quot;+2^20&amp;quot;,0,null],[&amp;quot;+2^21&amp;quot;,0,null],[&amp;quot;+2^22&amp;quot;,0,null],[&amp;quot;+2^23&amp;quot;,0,null],[&amp;quot;+2^24&amp;quot;,0,null],[&amp;quot;+2^25&amp;quot;,0,null],[&amp;quot;+2^26&amp;quot;,0,null],[&amp;quot;+2^27&amp;quot;,0,null],[&amp;quot;+2^28&amp;quot;,0,null],[&amp;quot;+2^29&amp;quot;,0,null],[&amp;quot;+2^30&amp;quot;,0,null],[&amp;quot;+2^31&amp;quot;,0,null],[&amp;quot;+2^32&amp;quot;,0,null],[&amp;quot;+2^33&amp;quot;,0,null],[&amp;quot;+2^34&amp;quot;,0,null],[&amp;quot;+2^35&amp;quot;,0,null],[&amp;quot;+2^36&amp;quot;,0,null],[&amp;quot;+2^37&amp;quot;,0,null],[&amp;quot;+2^38&amp;quot;,0,null],[&amp;quot;+2^39&amp;quot;,0,null],[&amp;quot;+2^40&amp;quot;,0,null],[&amp;quot;+2^41&amp;quot;,0,null],[&amp;quot;+2^42&amp;quot;,0,null],[&amp;quot;+2^43&amp;quot;,0,null],[&amp;quot;+2^44&amp;quot;,0,null],[&amp;quot;+2^45&amp;quot;,0,null],[&amp;quot;+2^46&amp;quot;,0,null],[&amp;quot;+2^47&amp;quot;,0,null],[&amp;quot;+2^48&amp;quot;,0,null],[&amp;quot;+2^49&amp;quot;,0,null],[&amp;quot;+2^50&amp;quot;,0,null],[&amp;quot;+2^51&amp;quot;,0,null],[&amp;quot;+2^52&amp;quot;,0,null],[&amp;quot;+2^53&amp;quot;,0,null],[&amp;quot;+2^54&amp;quot;,0,null],[&amp;quot;+2^55&amp;quot;,0,null],[&amp;quot;+2^56&amp;quot;,0,null],[&amp;quot;+2^57&amp;quot;,0,null],[&amp;quot;+2^58&amp;quot;,0,null],[&amp;quot;+2^59&amp;quot;,0,null],[&amp;quot;+2^60&amp;quot;,0,null],[&amp;quot;+2^61&amp;quot;,0,null],[&amp;quot;+2^62&amp;quot;,0,null],[&amp;quot;+2^63&amp;quot;,0,null],[&amp;quot;+2^64&amp;quot;,0,null],[&amp;quot;+2^65&amp;quot;,0,null],[&amp;quot;+2^66&amp;quot;,0,null],[&amp;quot;+2^67&amp;quot;,0,null],[&amp;quot;+2^68&amp;quot;,0,null],[&amp;quot;+2^69&amp;quot;,0,null],[&amp;quot;+2^70&amp;quot;,0,null],[&amp;quot;+2^71&amp;quot;,0,null],[&amp;quot;+2^72&amp;quot;,0,null],[&amp;quot;+2^73&amp;quot;,0,null],[&amp;quot;+2^74&amp;quot;,0,null],[&amp;quot;+2^75&amp;quot;,0,null],[&amp;quot;+2^76&amp;quot;,0,null],[&amp;quot;+2^77&amp;quot;,0,null],[&amp;quot;+2^78&amp;quot;,0,null],[&amp;quot;+2^79&amp;quot;,0,null],[&amp;quot;+2^80&amp;quot;,0,null],[&amp;quot;+2^81&amp;quot;,0,null],[&amp;quot;+2^82&amp;quot;,0,null],[&amp;quot;+2^83&amp;quot;,0,null],[&amp;quot;+2^84&amp;quot;,0,null],[&amp;quot;+2^85&amp;quot;,0,null],[&amp;quot;+2^86&amp;quot;,0,null],[&amp;quot;+2^87&amp;quot;,0,null],[&amp;quot;+2^88&amp;quot;,0,null],[&amp;quot;+2^89&amp;quot;,0,null],[&amp;quot;+2^90&amp;quot;,0,null],[&amp;quot;+2^91&amp;quot;,0,null],[&amp;quot;+2^92&amp;quot;,0,null],[&amp;quot;+2^93&amp;quot;,0,null],[&amp;quot;+2^94&amp;quot;,0,null],[&amp;quot;+2^95&amp;quot;,0,null],[&amp;quot;+2^96&amp;quot;,0,null],[&amp;quot;+2^97&amp;quot;,0,null],[&amp;quot;+2^98&amp;quot;,0,null],[&amp;quot;+2^99&amp;quot;,0,null],[&amp;quot;+2^100&amp;quot;,0,null],[&amp;quot;+2^101&amp;quot;,0,null],[&amp;quot;+2^102&amp;quot;,0,null],[&amp;quot;+2^103&amp;quot;,0,null],[&amp;quot;+2^104&amp;quot;,0,null],[&amp;quot;+2^105&amp;quot;,0,null],[&amp;quot;+2^106&amp;quot;,0,null],[&amp;quot;+2^107&amp;quot;,0,null],[&amp;quot;+2^108&amp;quot;,0,null],[&amp;quot;+2^109&amp;quot;,0,null],[&amp;quot;+2^110&amp;quot;,0,null],[&amp;quot;+2^111&amp;quot;,0,null],[&amp;quot;+2^112&amp;quot;,0,null],[&amp;quot;+2^113&amp;quot;,0,null],[&amp;quot;+2^114&amp;quot;,0,null],[&amp;quot;+2^115&amp;quot;,0,null],[&amp;quot;+2^116&amp;quot;,0,null],[&amp;quot;+2^117&amp;quot;,0,null],[&amp;quot;+2^118&amp;quot;,0,null],[&amp;quot;+2^119&amp;quot;,0,null],[&amp;quot;+2^120&amp;quot;,0,null],[&amp;quot;+2^121&amp;quot;,0,null],[&amp;quot;+2^122&amp;quot;,0,null],[&amp;quot;+2^123&amp;quot;,0,null],[&amp;quot;+2^124&amp;quot;,0,null],[&amp;quot;+2^125&amp;quot;,0,null],[&amp;quot;+2^126&amp;quot;,0,null],[&amp;quot;+2^127&amp;quot;,0,null] ]&lt;br /&gt;
 }}&lt;br /&gt;
=== expm1 ===&lt;br /&gt;
&lt;br /&gt;
{{#widget:FloatErrorGraph|id=tanhf_expm1&lt;br /&gt;
  |data=[ [&amp;quot;-2^127&amp;quot;,0,1.17549e-38],[&amp;quot;-2^126&amp;quot;,0,1.17549e-38],[&amp;quot;-2^125&amp;quot;,0,1.17549e-38],[&amp;quot;-2^124&amp;quot;,0,1.17549e-38],[&amp;quot;-2^123&amp;quot;,0,1.17549e-38],[&amp;quot;-2^122&amp;quot;,0,1.17549e-38],[&amp;quot;-2^121&amp;quot;,0,1.17549e-38],[&amp;quot;-2^120&amp;quot;,0,1.17549e-38],[&amp;quot;-2^119&amp;quot;,0,1.17549e-38],[&amp;quot;-2^118&amp;quot;,0,1.17549e-38],[&amp;quot;-2^117&amp;quot;,0,1.17549e-38],[&amp;quot;-2^116&amp;quot;,0,1.17549e-38],[&amp;quot;-2^115&amp;quot;,0,1.17549e-38],[&amp;quot;-2^114&amp;quot;,0,1.17549e-38],[&amp;quot;-2^113&amp;quot;,0,1.17549e-38],[&amp;quot;-2^112&amp;quot;,0,1.17549e-38],[&amp;quot;-2^111&amp;quot;,0,1.17549e-38],[&amp;quot;-2^110&amp;quot;,0,1.17549e-38],[&amp;quot;-2^109&amp;quot;,0,1.17549e-38],[&amp;quot;-2^108&amp;quot;,0,1.17549e-38],[&amp;quot;-2^107&amp;quot;,0,1.17549e-38],[&amp;quot;-2^106&amp;quot;,0,1.17549e-38],[&amp;quot;-2^105&amp;quot;,0,1.17549e-38],[&amp;quot;-2^104&amp;quot;,0,1.17549e-38],[&amp;quot;-2^103&amp;quot;,0,1.17549e-38],[&amp;quot;-2^102&amp;quot;,0,1.17549e-38],[&amp;quot;-2^101&amp;quot;,0,1.17549e-38],[&amp;quot;-2^100&amp;quot;,0,1.17549e-38],[&amp;quot;-2^99&amp;quot;,0,1.17549e-38],[&amp;quot;-2^98&amp;quot;,0,1.17549e-38],[&amp;quot;-2^97&amp;quot;,0,1.17549e-38],[&amp;quot;-2^96&amp;quot;,0,1.17549e-38],[&amp;quot;-2^95&amp;quot;,0,1.17549e-38],[&amp;quot;-2^94&amp;quot;,0,1.17549e-38],[&amp;quot;-2^93&amp;quot;,0,1.17549e-38],[&amp;quot;-2^92&amp;quot;,0,1.17549e-38],[&amp;quot;-2^91&amp;quot;,0,1.17549e-38],[&amp;quot;-2^90&amp;quot;,0,1.17549e-38],[&amp;quot;-2^89&amp;quot;,0,1.17549e-38],[&amp;quot;-2^88&amp;quot;,0,1.17549e-38],[&amp;quot;-2^87&amp;quot;,0,1.17549e-38],[&amp;quot;-2^86&amp;quot;,0,1.17549e-38],[&amp;quot;-2^85&amp;quot;,0,1.17549e-38],[&amp;quot;-2^84&amp;quot;,0,1.17549e-38],[&amp;quot;-2^83&amp;quot;,0,1.17549e-38],[&amp;quot;-2^82&amp;quot;,0,1.17549e-38],[&amp;quot;-2^81&amp;quot;,0,1.17549e-38],[&amp;quot;-2^80&amp;quot;,0,1.17549e-38],[&amp;quot;-2^79&amp;quot;,0,1.17549e-38],[&amp;quot;-2^78&amp;quot;,0,1.17549e-38],[&amp;quot;-2^77&amp;quot;,0,1.17549e-38],[&amp;quot;-2^76&amp;quot;,0,1.17549e-38],[&amp;quot;-2^75&amp;quot;,0,1.17549e-38],[&amp;quot;-2^74&amp;quot;,0,1.17549e-38],[&amp;quot;-2^73&amp;quot;,0,1.17549e-38],[&amp;quot;-2^72&amp;quot;,0,1.17549e-38],[&amp;quot;-2^71&amp;quot;,0,1.17549e-38],[&amp;quot;-2^70&amp;quot;,0,1.17549e-38],[&amp;quot;-2^69&amp;quot;,0,1.17549e-38],[&amp;quot;-2^68&amp;quot;,0,1.17549e-38],[&amp;quot;-2^67&amp;quot;,0,1.17549e-38],[&amp;quot;-2^66&amp;quot;,0,1.17549e-38],[&amp;quot;-2^65&amp;quot;,0,1.17549e-38],[&amp;quot;-2^64&amp;quot;,0,1.17549e-38],[&amp;quot;-2^63&amp;quot;,0,1.17549e-38],[&amp;quot;-2^62&amp;quot;,0,1.17549e-38],[&amp;quot;-2^61&amp;quot;,0,1.17549e-38],[&amp;quot;-2^60&amp;quot;,0,1.17549e-38],[&amp;quot;-2^59&amp;quot;,0,1.17549e-38],[&amp;quot;-2^58&amp;quot;,0,1.17549e-38],[&amp;quot;-2^57&amp;quot;,0,1.17549e-38],[&amp;quot;-2^56&amp;quot;,0,1.17549e-38],[&amp;quot;-2^55&amp;quot;,0,1.17549e-38],[&amp;quot;-2^54&amp;quot;,0,1.17549e-38],[&amp;quot;-2^53&amp;quot;,0,1.17549e-38],[&amp;quot;-2^52&amp;quot;,0,1.17549e-38],[&amp;quot;-2^51&amp;quot;,0,1.17549e-38],[&amp;quot;-2^50&amp;quot;,0,1.17549e-38],[&amp;quot;-2^49&amp;quot;,0,1.17549e-38],[&amp;quot;-2^48&amp;quot;,0,1.17549e-38],[&amp;quot;-2^47&amp;quot;,0,1.17549e-38],[&amp;quot;-2^46&amp;quot;,0,1.17549e-38],[&amp;quot;-2^45&amp;quot;,0,1.17549e-38],[&amp;quot;-2^44&amp;quot;,0,1.17549e-38],[&amp;quot;-2^43&amp;quot;,0,1.17549e-38],[&amp;quot;-2^42&amp;quot;,0,1.17549e-38],[&amp;quot;-2^41&amp;quot;,0,1.17549e-38],[&amp;quot;-2^40&amp;quot;,0,1.17549e-38],[&amp;quot;-2^39&amp;quot;,0,1.17549e-38],[&amp;quot;-2^38&amp;quot;,0,1.17549e-38],[&amp;quot;-2^37&amp;quot;,0,1.17549e-38],[&amp;quot;-2^36&amp;quot;,0,1.17549e-38],[&amp;quot;-2^35&amp;quot;,0,1.17549e-38],[&amp;quot;-2^34&amp;quot;,0,1.17549e-38],[&amp;quot;-2^33&amp;quot;,0,1.17549e-38],[&amp;quot;-2^32&amp;quot;,0,1.17549e-38],[&amp;quot;-2^31&amp;quot;,0,1.17549e-38],[&amp;quot;-2^30&amp;quot;,0,1.17549e-38],[&amp;quot;-2^29&amp;quot;,0,1.17549e-38],[&amp;quot;-2^28&amp;quot;,0,1.17549e-38],[&amp;quot;-2^27&amp;quot;,0,1.17549e-38],[&amp;quot;-2^26&amp;quot;,0,1.17549e-38],[&amp;quot;-2^25&amp;quot;,0,1.17549e-38],[&amp;quot;-2^24&amp;quot;,0,1.17549e-38],[&amp;quot;-2^23&amp;quot;,0,1.17549e-38],[&amp;quot;-2^22&amp;quot;,0,1.17549e-38],[&amp;quot;-2^21&amp;quot;,0,1.17549e-38],[&amp;quot;-2^20&amp;quot;,0,1.17549e-38],[&amp;quot;-2^19&amp;quot;,0,1.17549e-38],[&amp;quot;-2^18&amp;quot;,0,1.17549e-38],[&amp;quot;-2^17&amp;quot;,0,1.17549e-38],[&amp;quot;-2^16&amp;quot;,0,1.17549e-38],[&amp;quot;-2^15&amp;quot;,0,1.17549e-38],[&amp;quot;-2^14&amp;quot;,0,1.17549e-38],[&amp;quot;-2^13&amp;quot;,0,1.17549e-38],[&amp;quot;-2^12&amp;quot;,0,1.17549e-38],[&amp;quot;-2^11&amp;quot;,0,1.17549e-38],[&amp;quot;-2^10&amp;quot;,0,1.17549e-38],[&amp;quot;-2^9&amp;quot;,0,1.17549e-38],[&amp;quot;-2^8&amp;quot;,0,1.17549e-38],[&amp;quot;-2^7&amp;quot;,0,1.17549e-38],[&amp;quot;-2^6&amp;quot;,0,1.17549e-38],[&amp;quot;-2^5&amp;quot;,0,1.17549e-38],[&amp;quot;-2^4&amp;quot;,2.53131e-14,4.24683e-07],[&amp;quot;-2^3&amp;quot;,1.19138e-07,1.9988],[&amp;quot;-2^2&amp;quot;,1.46791e-07,2.46274],[&amp;quot;-2^1&amp;quot;,1.47723e-07,2.47839],[&amp;quot;-2^0&amp;quot;,1.40758e-07,2.36153],[&amp;quot;-2^-1&amp;quot;,1.13422e-07,2.23445],[&amp;quot;-2^-2&amp;quot;,6.38012e-08,2.14081],[&amp;quot;-2^-3&amp;quot;,2.69538e-08,1.80884],[&amp;quot;-2^-4&amp;quot;,1.21308e-08,1.62817],[&amp;quot;-2^-5&amp;quot;,5.59535e-09,1.50199],[&amp;quot;-2^-6&amp;quot;,2.69177e-09,1.44513],[&amp;quot;-2^-7&amp;quot;,1.31289e-09,1.4097],[&amp;quot;-2^-8&amp;quot;,6.49688e-10,1.3952],[&amp;quot;-2^-9&amp;quot;,3.32043e-10,1.42611],[&amp;quot;-2^-10&amp;quot;,1.66275e-10,1.4283],[&amp;quot;-2^-11&amp;quot;,8.08595e-11,1.38916],[&amp;quot;-2^-12&amp;quot;,3.8939e-11,1.33794],[&amp;quot;-2^-13&amp;quot;,1.9401e-11,1.33323],[&amp;quot;-2^-14&amp;quot;,7.88212e-12,1.08331],[&amp;quot;-2^-15&amp;quot;,3.71372e-12,1.02082],[&amp;quot;-2^-16&amp;quot;,1.82845e-12,1.0052],[&amp;quot;-2^-17&amp;quot;,9.10676e-13,1.0013],[&amp;quot;-2^-18&amp;quot;,4.54895e-13,1.00032],[&amp;quot;-2^-19&amp;quot;,2.27392e-13,1.00008],[&amp;quot;-2^-20&amp;quot;,1.13689e-13,1.00002],[&amp;quot;-2^-21&amp;quot;,5.68437e-14,1],[&amp;quot;-2^-22&amp;quot;,2.84217e-14,1],[&amp;quot;-2^-23&amp;quot;,1.42109e-14,1],[&amp;quot;-2^-24&amp;quot;,7.10543e-15,1],[&amp;quot;-2^-25&amp;quot;,3.55271e-15,1],[&amp;quot;-2^-26&amp;quot;,9.92617e-24,5.58794e-09],[&amp;quot;-2^-27&amp;quot;,1.65436e-24,1.86265e-09],[&amp;quot;-2^-28&amp;quot;,8.27181e-25,1.86265e-09],[&amp;quot;-2^-29&amp;quot;,4.1359e-25,1.86265e-09],[&amp;quot;-2^-30&amp;quot;,2.06795e-25,1.86265e-09],[&amp;quot;-2^-31&amp;quot;,1.03398e-25,1.86265e-09],[&amp;quot;-2^-32&amp;quot;,5.16988e-26,1.86265e-09],[&amp;quot;-2^-33&amp;quot;,2.58494e-26,1.86265e-09],[&amp;quot;-2^-34&amp;quot;,1.29247e-26,1.86265e-09],[&amp;quot;-2^-35&amp;quot;,6.46235e-27,1.86265e-09],[&amp;quot;-2^-36&amp;quot;,3.23117e-27,1.86265e-09],[&amp;quot;-2^-37&amp;quot;,1.61559e-27,1.86265e-09],[&amp;quot;-2^-38&amp;quot;,8.07794e-28,1.86265e-09],[&amp;quot;-2^-39&amp;quot;,4.03897e-28,1.86265e-09],[&amp;quot;-2^-40&amp;quot;,2.01948e-28,1.86265e-09],[&amp;quot;-2^-41&amp;quot;,1.00974e-28,1.86265e-09],[&amp;quot;-2^-42&amp;quot;,5.04871e-29,1.86265e-09],[&amp;quot;-2^-43&amp;quot;,2.52435e-29,1.86265e-09],[&amp;quot;-2^-44&amp;quot;,1.26218e-29,1.86265e-09],[&amp;quot;-2^-45&amp;quot;,6.31089e-30,1.86265e-09],[&amp;quot;-2^-46&amp;quot;,3.15544e-30,1.86265e-09],[&amp;quot;-2^-47&amp;quot;,1.57772e-30,1.86265e-09],[&amp;quot;-2^-48&amp;quot;,7.88861e-31,1.86265e-09],[&amp;quot;-2^-49&amp;quot;,3.9443e-31,1.86265e-09],[&amp;quot;-2^-50&amp;quot;,1.97215e-31,1.86265e-09],[&amp;quot;-2^-51&amp;quot;,9.86076e-32,1.86265e-09],[&amp;quot;-2^-52&amp;quot;,4.93038e-32,1.86265e-09],[&amp;quot;-2^-53&amp;quot;,2.46519e-32,1.86265e-09],[&amp;quot;-2^-54&amp;quot;,1.2326e-32,1.86265e-09],[&amp;quot;-2^-55&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-56&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-57&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-58&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-59&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-60&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-61&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-62&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-63&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-64&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-65&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-66&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-67&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-68&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-69&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-70&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-71&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-72&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-73&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-74&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-75&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-76&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-77&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-78&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-79&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-80&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-81&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-82&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-83&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-84&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-85&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-86&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-87&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-88&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-89&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-90&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-91&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-92&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-93&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-94&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-95&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-96&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-97&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-98&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-99&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-100&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-101&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-102&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-103&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-104&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-105&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-106&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-107&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-108&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-109&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-110&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-111&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-112&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-113&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-114&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-115&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-116&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-117&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-118&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-119&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-120&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-121&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-122&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-123&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-124&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-125&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-126&amp;quot;,0,1.17549e-38],[&amp;quot;-SN&amp;quot;,0,1.17549e-38],[&amp;quot;0&amp;quot;,0,1.17549e-38],[&amp;quot;+SN&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-126&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-125&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-124&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-123&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-122&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-121&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-120&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-119&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-118&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-117&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-116&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-115&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-114&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-113&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-112&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-111&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-110&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-109&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-108&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-107&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-106&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-105&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-104&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-103&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-102&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-101&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-100&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-99&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-98&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-97&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-96&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-95&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-94&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-93&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-92&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-91&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-90&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-89&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-88&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-87&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-86&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-85&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-84&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-83&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-82&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-81&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-80&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-79&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-78&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-77&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-76&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-75&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-74&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-73&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-72&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-71&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-70&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-69&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-68&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-67&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-66&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-65&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-64&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-63&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-62&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-61&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-60&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-59&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-58&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-57&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-56&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-55&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-54&amp;quot;,1.2326e-32,1.86265e-09],[&amp;quot;+2^-53&amp;quot;,2.46519e-32,1.86265e-09],[&amp;quot;+2^-52&amp;quot;,4.93038e-32,1.86265e-09],[&amp;quot;+2^-51&amp;quot;,9.86076e-32,1.86265e-09],[&amp;quot;+2^-50&amp;quot;,1.97215e-31,1.86265e-09],[&amp;quot;+2^-49&amp;quot;,3.9443e-31,1.86265e-09],[&amp;quot;+2^-48&amp;quot;,7.88861e-31,1.86265e-09],[&amp;quot;+2^-47&amp;quot;,1.57772e-30,1.86265e-09],[&amp;quot;+2^-46&amp;quot;,3.15544e-30,1.86265e-09],[&amp;quot;+2^-45&amp;quot;,6.31089e-30,1.86265e-09],[&amp;quot;+2^-44&amp;quot;,1.26218e-29,1.86265e-09],[&amp;quot;+2^-43&amp;quot;,2.52435e-29,1.86265e-09],[&amp;quot;+2^-42&amp;quot;,5.04871e-29,1.86265e-09],[&amp;quot;+2^-41&amp;quot;,1.00974e-28,1.86265e-09],[&amp;quot;+2^-40&amp;quot;,2.01948e-28,1.86265e-09],[&amp;quot;+2^-39&amp;quot;,4.03897e-28,1.86265e-09],[&amp;quot;+2^-38&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-37&amp;quot;,1.61559e-27,1.86265e-09],[&amp;quot;+2^-36&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-35&amp;quot;,6.46235e-27,1.86265e-09],[&amp;quot;+2^-34&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-33&amp;quot;,2.58494e-26,1.86265e-09],[&amp;quot;+2^-32&amp;quot;,5.16988e-26,1.86265e-09],[&amp;quot;+2^-31&amp;quot;,1.03398e-25,1.86265e-09],[&amp;quot;+2^-30&amp;quot;,2.06795e-25,1.86265e-09],[&amp;quot;+2^-29&amp;quot;,4.1359e-25,1.86265e-09],[&amp;quot;+2^-28&amp;quot;,8.27181e-25,1.86265e-09],[&amp;quot;+2^-27&amp;quot;,1.65436e-24,1.86265e-09],[&amp;quot;+2^-26&amp;quot;,9.92617e-24,5.58794e-09],[&amp;quot;+2^-25&amp;quot;,3.55271e-15,1],[&amp;quot;+2^-24&amp;quot;,7.10543e-15,1],[&amp;quot;+2^-23&amp;quot;,1.42109e-14,1],[&amp;quot;+2^-22&amp;quot;,2.84217e-14,1],[&amp;quot;+2^-21&amp;quot;,5.68437e-14,1],[&amp;quot;+2^-20&amp;quot;,1.13689e-13,1.00002],[&amp;quot;+2^-19&amp;quot;,2.27391e-13,1.00008],[&amp;quot;+2^-18&amp;quot;,4.54892e-13,1.00032],[&amp;quot;+2^-17&amp;quot;,9.10665e-13,1.00129],[&amp;quot;+2^-16&amp;quot;,1.82841e-12,1.00518],[&amp;quot;+2^-15&amp;quot;,3.71353e-12,1.02077],[&amp;quot;+2^-14&amp;quot;,1.39798e-11,1.92136],[&amp;quot;+2^-13&amp;quot;,2.63239e-11,1.80896],[&amp;quot;+2^-12&amp;quot;,5.37712e-11,1.84757],[&amp;quot;+2^-11&amp;quot;,1.09932e-10,1.88861],[&amp;quot;+2^-10&amp;quot;,2.24787e-10,1.93091],[&amp;quot;+2^-9&amp;quot;,4.3452e-10,1.86625],[&amp;quot;+2^-8&amp;quot;,1.01783e-09,2.18576],[&amp;quot;+2^-7&amp;quot;,2.10562e-09,2.26089],[&amp;quot;+2^-6&amp;quot;,4.03954e-09,2.16871],[&amp;quot;+2^-5&amp;quot;,8.15746e-09,2.18975],[&amp;quot;+2^-4&amp;quot;,1.5545e-08,2.08641],[&amp;quot;+2^-3&amp;quot;,2.62153e-08,2.05864],[&amp;quot;+2^-2&amp;quot;,4.09295e-08,1.73804],[&amp;quot;+2^-1&amp;quot;,8.02409e-08,1.34622],[&amp;quot;+2^0&amp;quot;,8.66201e-08,1.45324],[&amp;quot;+2^1&amp;quot;,8.8654e-08,1.48737],[&amp;quot;+2^2&amp;quot;,6.7976e-08,1.14045],[&amp;quot;+2^3&amp;quot;,7.44117e-08,1.24842],[&amp;quot;+2^4&amp;quot;,2.53131e-14,4.24683e-07],[&amp;quot;+2^5&amp;quot;,0,null],[&amp;quot;+2^6&amp;quot;,0,null],[&amp;quot;+2^7&amp;quot;,0,null],[&amp;quot;+2^8&amp;quot;,0,null],[&amp;quot;+2^9&amp;quot;,0,null],[&amp;quot;+2^10&amp;quot;,0,null],[&amp;quot;+2^11&amp;quot;,0,null],[&amp;quot;+2^12&amp;quot;,0,null],[&amp;quot;+2^13&amp;quot;,0,null],[&amp;quot;+2^14&amp;quot;,0,null],[&amp;quot;+2^15&amp;quot;,0,null],[&amp;quot;+2^16&amp;quot;,0,null],[&amp;quot;+2^17&amp;quot;,0,null],[&amp;quot;+2^18&amp;quot;,0,null],[&amp;quot;+2^19&amp;quot;,0,null],[&amp;quot;+2^20&amp;quot;,0,null],[&amp;quot;+2^21&amp;quot;,0,null],[&amp;quot;+2^22&amp;quot;,0,null],[&amp;quot;+2^23&amp;quot;,0,null],[&amp;quot;+2^24&amp;quot;,0,null],[&amp;quot;+2^25&amp;quot;,0,null],[&amp;quot;+2^26&amp;quot;,0,null],[&amp;quot;+2^27&amp;quot;,0,null],[&amp;quot;+2^28&amp;quot;,0,null],[&amp;quot;+2^29&amp;quot;,0,null],[&amp;quot;+2^30&amp;quot;,0,null],[&amp;quot;+2^31&amp;quot;,0,null],[&amp;quot;+2^32&amp;quot;,0,null],[&amp;quot;+2^33&amp;quot;,0,null],[&amp;quot;+2^34&amp;quot;,0,null],[&amp;quot;+2^35&amp;quot;,0,null],[&amp;quot;+2^36&amp;quot;,0,null],[&amp;quot;+2^37&amp;quot;,0,null],[&amp;quot;+2^38&amp;quot;,0,null],[&amp;quot;+2^39&amp;quot;,0,null],[&amp;quot;+2^40&amp;quot;,0,null],[&amp;quot;+2^41&amp;quot;,0,null],[&amp;quot;+2^42&amp;quot;,0,null],[&amp;quot;+2^43&amp;quot;,0,null],[&amp;quot;+2^44&amp;quot;,0,null],[&amp;quot;+2^45&amp;quot;,0,null],[&amp;quot;+2^46&amp;quot;,0,null],[&amp;quot;+2^47&amp;quot;,0,null],[&amp;quot;+2^48&amp;quot;,0,null],[&amp;quot;+2^49&amp;quot;,0,null],[&amp;quot;+2^50&amp;quot;,0,null],[&amp;quot;+2^51&amp;quot;,0,null],[&amp;quot;+2^52&amp;quot;,0,null],[&amp;quot;+2^53&amp;quot;,0,null],[&amp;quot;+2^54&amp;quot;,0,null],[&amp;quot;+2^55&amp;quot;,0,null],[&amp;quot;+2^56&amp;quot;,0,null],[&amp;quot;+2^57&amp;quot;,0,null],[&amp;quot;+2^58&amp;quot;,0,null],[&amp;quot;+2^59&amp;quot;,0,null],[&amp;quot;+2^60&amp;quot;,0,null],[&amp;quot;+2^61&amp;quot;,0,null],[&amp;quot;+2^62&amp;quot;,0,null],[&amp;quot;+2^63&amp;quot;,0,null],[&amp;quot;+2^64&amp;quot;,0,null],[&amp;quot;+2^65&amp;quot;,0,null],[&amp;quot;+2^66&amp;quot;,0,null],[&amp;quot;+2^67&amp;quot;,0,null],[&amp;quot;+2^68&amp;quot;,0,null],[&amp;quot;+2^69&amp;quot;,0,null],[&amp;quot;+2^70&amp;quot;,0,null],[&amp;quot;+2^71&amp;quot;,0,null],[&amp;quot;+2^72&amp;quot;,0,null],[&amp;quot;+2^73&amp;quot;,0,null],[&amp;quot;+2^74&amp;quot;,0,null],[&amp;quot;+2^75&amp;quot;,0,null],[&amp;quot;+2^76&amp;quot;,0,null],[&amp;quot;+2^77&amp;quot;,0,null],[&amp;quot;+2^78&amp;quot;,0,null],[&amp;quot;+2^79&amp;quot;,0,null],[&amp;quot;+2^80&amp;quot;,0,null],[&amp;quot;+2^81&amp;quot;,0,null],[&amp;quot;+2^82&amp;quot;,0,null],[&amp;quot;+2^83&amp;quot;,0,null],[&amp;quot;+2^84&amp;quot;,0,null],[&amp;quot;+2^85&amp;quot;,0,null],[&amp;quot;+2^86&amp;quot;,0,null],[&amp;quot;+2^87&amp;quot;,0,null],[&amp;quot;+2^88&amp;quot;,0,null],[&amp;quot;+2^89&amp;quot;,0,null],[&amp;quot;+2^90&amp;quot;,0,null],[&amp;quot;+2^91&amp;quot;,0,null],[&amp;quot;+2^92&amp;quot;,0,null],[&amp;quot;+2^93&amp;quot;,0,null],[&amp;quot;+2^94&amp;quot;,0,null],[&amp;quot;+2^95&amp;quot;,0,null],[&amp;quot;+2^96&amp;quot;,0,null],[&amp;quot;+2^97&amp;quot;,0,null],[&amp;quot;+2^98&amp;quot;,0,null],[&amp;quot;+2^99&amp;quot;,0,null],[&amp;quot;+2^100&amp;quot;,0,null],[&amp;quot;+2^101&amp;quot;,0,null],[&amp;quot;+2^102&amp;quot;,0,null],[&amp;quot;+2^103&amp;quot;,0,null],[&amp;quot;+2^104&amp;quot;,0,null],[&amp;quot;+2^105&amp;quot;,0,null],[&amp;quot;+2^106&amp;quot;,0,null],[&amp;quot;+2^107&amp;quot;,0,null],[&amp;quot;+2^108&amp;quot;,0,null],[&amp;quot;+2^109&amp;quot;,0,null],[&amp;quot;+2^110&amp;quot;,0,null],[&amp;quot;+2^111&amp;quot;,0,null],[&amp;quot;+2^112&amp;quot;,0,null],[&amp;quot;+2^113&amp;quot;,0,null],[&amp;quot;+2^114&amp;quot;,0,null],[&amp;quot;+2^115&amp;quot;,0,null],[&amp;quot;+2^116&amp;quot;,0,null],[&amp;quot;+2^117&amp;quot;,0,null],[&amp;quot;+2^118&amp;quot;,0,null],[&amp;quot;+2^119&amp;quot;,0,null],[&amp;quot;+2^120&amp;quot;,0,null],[&amp;quot;+2^121&amp;quot;,0,null],[&amp;quot;+2^122&amp;quot;,0,null],[&amp;quot;+2^123&amp;quot;,0,null],[&amp;quot;+2^124&amp;quot;,0,null],[&amp;quot;+2^125&amp;quot;,0,null],[&amp;quot;+2^126&amp;quot;,0,null],[&amp;quot;+2^127&amp;quot;,0,null] ]&lt;br /&gt;
 }}&lt;br /&gt;
&lt;br /&gt;
==Taylor==&lt;br /&gt;
&lt;br /&gt;
{{#widget:FloatErrorGraph|id=taylor&lt;br /&gt;
  |data=[ [&amp;quot;-2^127&amp;quot;,0,0],[&amp;quot;-2^126&amp;quot;,0,0],[&amp;quot;-2^125&amp;quot;,0,0],[&amp;quot;-2^124&amp;quot;,0,0],[&amp;quot;-2^123&amp;quot;,0,0],[&amp;quot;-2^122&amp;quot;,0,0],[&amp;quot;-2^121&amp;quot;,0,0],[&amp;quot;-2^120&amp;quot;,0,0],[&amp;quot;-2^119&amp;quot;,0,0],[&amp;quot;-2^118&amp;quot;,0,0],[&amp;quot;-2^117&amp;quot;,0,0],[&amp;quot;-2^116&amp;quot;,0,0],[&amp;quot;-2^115&amp;quot;,0,0],[&amp;quot;-2^114&amp;quot;,0,0],[&amp;quot;-2^113&amp;quot;,0,0],[&amp;quot;-2^112&amp;quot;,0,0],[&amp;quot;-2^111&amp;quot;,0,0],[&amp;quot;-2^110&amp;quot;,0,0],[&amp;quot;-2^109&amp;quot;,0,0],[&amp;quot;-2^108&amp;quot;,0,0],[&amp;quot;-2^107&amp;quot;,0,0],[&amp;quot;-2^106&amp;quot;,0,0],[&amp;quot;-2^105&amp;quot;,0,0],[&amp;quot;-2^104&amp;quot;,0,0],[&amp;quot;-2^103&amp;quot;,0,0],[&amp;quot;-2^102&amp;quot;,0,0],[&amp;quot;-2^101&amp;quot;,0,0],[&amp;quot;-2^100&amp;quot;,0,0],[&amp;quot;-2^99&amp;quot;,0,0],[&amp;quot;-2^98&amp;quot;,0,0],[&amp;quot;-2^97&amp;quot;,0,0],[&amp;quot;-2^96&amp;quot;,0,0],[&amp;quot;-2^95&amp;quot;,0,0],[&amp;quot;-2^94&amp;quot;,0,0],[&amp;quot;-2^93&amp;quot;,0,0],[&amp;quot;-2^92&amp;quot;,0,0],[&amp;quot;-2^91&amp;quot;,0,0],[&amp;quot;-2^90&amp;quot;,0,0],[&amp;quot;-2^89&amp;quot;,0,0],[&amp;quot;-2^88&amp;quot;,0,0],[&amp;quot;-2^87&amp;quot;,0,0],[&amp;quot;-2^86&amp;quot;,0,0],[&amp;quot;-2^85&amp;quot;,0,0],[&amp;quot;-2^84&amp;quot;,0,0],[&amp;quot;-2^83&amp;quot;,0,0],[&amp;quot;-2^82&amp;quot;,0,0],[&amp;quot;-2^81&amp;quot;,0,0],[&amp;quot;-2^80&amp;quot;,0,0],[&amp;quot;-2^79&amp;quot;,0,0],[&amp;quot;-2^78&amp;quot;,0,0],[&amp;quot;-2^77&amp;quot;,0,0],[&amp;quot;-2^76&amp;quot;,0,0],[&amp;quot;-2^75&amp;quot;,0,0],[&amp;quot;-2^74&amp;quot;,0,0],[&amp;quot;-2^73&amp;quot;,0,0],[&amp;quot;-2^72&amp;quot;,0,0],[&amp;quot;-2^71&amp;quot;,0,0],[&amp;quot;-2^70&amp;quot;,0,0],[&amp;quot;-2^69&amp;quot;,0,0],[&amp;quot;-2^68&amp;quot;,0,0],[&amp;quot;-2^67&amp;quot;,0,0],[&amp;quot;-2^66&amp;quot;,0,0],[&amp;quot;-2^65&amp;quot;,0,0],[&amp;quot;-2^64&amp;quot;,0,0],[&amp;quot;-2^63&amp;quot;,1.84467e+19,3.09485e+26],[&amp;quot;-2^62&amp;quot;,9.22336e+18,1.54742e+26],[&amp;quot;-2^61&amp;quot;,4.61168e+18,7.73712e+25],[&amp;quot;-2^60&amp;quot;,2.30584e+18,3.86856e+25],[&amp;quot;-2^59&amp;quot;,1.15292e+18,1.93428e+25],[&amp;quot;-2^58&amp;quot;,5.7646e+17,9.6714e+24],[&amp;quot;-2^57&amp;quot;,2.8823e+17,4.8357e+24],[&amp;quot;-2^56&amp;quot;,1.44115e+17,2.41785e+24],[&amp;quot;-2^55&amp;quot;,7.20575e+16,1.20892e+24],[&amp;quot;-2^54&amp;quot;,3.60288e+16,6.04462e+23],[&amp;quot;-2^53&amp;quot;,1.80144e+16,3.02231e+23],[&amp;quot;-2^52&amp;quot;,9.00719e+15,1.51116e+23],[&amp;quot;-2^51&amp;quot;,4.5036e+15,7.55578e+22],[&amp;quot;-2^50&amp;quot;,2.2518e+15,3.77789e+22],[&amp;quot;-2^49&amp;quot;,1.1259e+15,1.88894e+22],[&amp;quot;-2^48&amp;quot;,5.62949e+14,9.44472e+21],[&amp;quot;-2^47&amp;quot;,2.81475e+14,4.72236e+21],[&amp;quot;-2^46&amp;quot;,1.40737e+14,2.36118e+21],[&amp;quot;-2^45&amp;quot;,7.03687e+13,1.18059e+21],[&amp;quot;-2^44&amp;quot;,3.51843e+13,5.90295e+20],[&amp;quot;-2^43&amp;quot;,1.75922e+13,2.95148e+20],[&amp;quot;-2^42&amp;quot;,8.79608e+12,1.47574e+20],[&amp;quot;-2^41&amp;quot;,4.39804e+12,7.37869e+19],[&amp;quot;-2^40&amp;quot;,2.19902e+12,3.68935e+19],[&amp;quot;-2^39&amp;quot;,1.09951e+12,1.84467e+19],[&amp;quot;-2^38&amp;quot;,5.49755e+11,9.22336e+18],[&amp;quot;-2^37&amp;quot;,2.74878e+11,4.61168e+18],[&amp;quot;-2^36&amp;quot;,1.37439e+11,2.30584e+18],[&amp;quot;-2^35&amp;quot;,6.87194e+10,1.15292e+18],[&amp;quot;-2^34&amp;quot;,3.43597e+10,5.7646e+17],[&amp;quot;-2^33&amp;quot;,1.71799e+10,2.8823e+17],[&amp;quot;-2^32&amp;quot;,8.58993e+09,1.44115e+17],[&amp;quot;-2^31&amp;quot;,4.29085e+09,7.19885e+16],[&amp;quot;-2^30&amp;quot;,2.14542e+09,3.59943e+16],[&amp;quot;-2^29&amp;quot;,1.07271e+09,1.79971e+16],[&amp;quot;-2^28&amp;quot;,5.36356e+08,8.99856e+15],[&amp;quot;-2^27&amp;quot;,2.68178e+08,4.49928e+15],[&amp;quot;-2^26&amp;quot;,1.34089e+08,2.24964e+15],[&amp;quot;-2^25&amp;quot;,6.70445e+07,1.12482e+15],[&amp;quot;-2^24&amp;quot;,3.35223e+07,5.6241e+14],[&amp;quot;-2^23&amp;quot;,1.67611e+07,2.81205e+14],[&amp;quot;-2^22&amp;quot;,8.38056e+06,1.40603e+14],[&amp;quot;-2^21&amp;quot;,4.19028e+06,7.03013e+13],[&amp;quot;-2^20&amp;quot;,2.09514e+06,3.51506e+13],[&amp;quot;-2^19&amp;quot;,1.04757e+06,1.75753e+13],[&amp;quot;-2^18&amp;quot;,523784,8.78764e+12],[&amp;quot;-2^17&amp;quot;,261892,4.39381e+12],[&amp;quot;-2^16&amp;quot;,130945,2.1969e+12],[&amp;quot;-2^15&amp;quot;,65472.2,1.09844e+12],[&amp;quot;-2^14&amp;quot;,32735.6,5.49212e+11],[&amp;quot;-2^13&amp;quot;,16367.3,2.74598e+11],[&amp;quot;-2^12&amp;quot;,8183.15,1.3729e+11],[&amp;quot;-2^11&amp;quot;,4091.07,6.86368e+10],[&amp;quot;-2^10&amp;quot;,2045.04,3.431e+10],[&amp;quot;-2^9&amp;quot;,1022.02,1.71466e+10],[&amp;quot;-2^8&amp;quot;,510.509,8.56492e+09],[&amp;quot;-2^7&amp;quot;,254.754,4.27406e+09],[&amp;quot;-2^6&amp;quot;,126.876,2.12862e+09],[&amp;quot;-2^5&amp;quot;,62.9354,1.05588e+09],[&amp;quot;-2^4&amp;quot;,30.9629,5.19471e+08],[&amp;quot;-2^3&amp;quot;,14.9718,2.51185e+08],[&amp;quot;-2^2&amp;quot;,6.96672,1.16882e+08],[&amp;quot;-2^1&amp;quot;,2.94633,4.94312e+07],[&amp;quot;-2^0&amp;quot;,0.940715,1.57826e+07],[&amp;quot;-2^-1&amp;quot;,0.127635,2.14135e+06],[&amp;quot;-2^-2&amp;quot;,0.00318735,106950],[&amp;quot;-2^-3&amp;quot;,3.11475e-05,2090.28],[&amp;quot;-2^-4&amp;quot;,2.57737e-07,34.5929],[&amp;quot;-2^-5&amp;quot;,3.89213e-09,1.04478],[&amp;quot;-2^-6&amp;quot;,9.51472e-10,0.510818],[&amp;quot;-2^-7&amp;quot;,4.66402e-10,0.500795],[&amp;quot;-2^-8&amp;quot;,2.32917e-10,0.500185],[&amp;quot;-2^-9&amp;quot;,1.16423e-10,0.500033],[&amp;quot;-2^-10&amp;quot;,5.82082e-11,0.500004],[&amp;quot;-2^-11&amp;quot;,2.91037e-11,0.499998],[&amp;quot;-2^-12&amp;quot;,1.45519e-11,0.5],[&amp;quot;-2^-13&amp;quot;,4.85062e-12,0.333332],[&amp;quot;-2^-14&amp;quot;,6.06328e-13,0.0833331],[&amp;quot;-2^-15&amp;quot;,7.5791e-14,0.0208333],[&amp;quot;-2^-16&amp;quot;,9.47387e-15,0.00520832],[&amp;quot;-2^-17&amp;quot;,1.18424e-15,0.00130208],[&amp;quot;-2^-18&amp;quot;,1.48029e-16,0.00032552],[&amp;quot;-2^-19&amp;quot;,1.85039e-17,8.13808e-05],[&amp;quot;-2^-20&amp;quot;,2.31304e-18,2.03457e-05],[&amp;quot;-2^-21&amp;quot;,2.89156e-19,5.08688e-06],[&amp;quot;-2^-22&amp;quot;,3.61577e-20,1.27219e-06],[&amp;quot;-2^-23&amp;quot;,4.52633e-21,3.18512e-07],[&amp;quot;-2^-24&amp;quot;,5.691e-22,8.00937e-08],[&amp;quot;-2^-25&amp;quot;,7.27919e-23,2.04891e-08],[&amp;quot;-2^-26&amp;quot;,9.92617e-24,5.58794e-09],[&amp;quot;-2^-27&amp;quot;,1.65436e-24,1.86265e-09],[&amp;quot;-2^-28&amp;quot;,8.27181e-25,1.86265e-09],[&amp;quot;-2^-29&amp;quot;,4.1359e-25,1.86265e-09],[&amp;quot;-2^-30&amp;quot;,2.06795e-25,1.86265e-09],[&amp;quot;-2^-31&amp;quot;,1.03398e-25,1.86265e-09],[&amp;quot;-2^-32&amp;quot;,5.16988e-26,1.86265e-09],[&amp;quot;-2^-33&amp;quot;,2.58494e-26,1.86265e-09],[&amp;quot;-2^-34&amp;quot;,1.29247e-26,1.86265e-09],[&amp;quot;-2^-35&amp;quot;,6.46235e-27,1.86265e-09],[&amp;quot;-2^-36&amp;quot;,3.23117e-27,1.86265e-09],[&amp;quot;-2^-37&amp;quot;,1.61559e-27,1.86265e-09],[&amp;quot;-2^-38&amp;quot;,8.07794e-28,1.86265e-09],[&amp;quot;-2^-39&amp;quot;,4.03897e-28,1.86265e-09],[&amp;quot;-2^-40&amp;quot;,2.01948e-28,1.86265e-09],[&amp;quot;-2^-41&amp;quot;,1.00974e-28,1.86265e-09],[&amp;quot;-2^-42&amp;quot;,5.04871e-29,1.86265e-09],[&amp;quot;-2^-43&amp;quot;,2.52435e-29,1.86265e-09],[&amp;quot;-2^-44&amp;quot;,1.26218e-29,1.86265e-09],[&amp;quot;-2^-45&amp;quot;,6.31089e-30,1.86265e-09],[&amp;quot;-2^-46&amp;quot;,3.15544e-30,1.86265e-09],[&amp;quot;-2^-47&amp;quot;,1.57772e-30,1.86265e-09],[&amp;quot;-2^-48&amp;quot;,7.88861e-31,1.86265e-09],[&amp;quot;-2^-49&amp;quot;,3.9443e-31,1.86265e-09],[&amp;quot;-2^-50&amp;quot;,1.97215e-31,1.86265e-09],[&amp;quot;-2^-51&amp;quot;,9.86076e-32,1.86265e-09],[&amp;quot;-2^-52&amp;quot;,4.93038e-32,1.86265e-09],[&amp;quot;-2^-53&amp;quot;,2.46519e-32,1.86265e-09],[&amp;quot;-2^-54&amp;quot;,1.2326e-32,1.86265e-09],[&amp;quot;-2^-55&amp;quot;,0,0],[&amp;quot;-2^-56&amp;quot;,0,0],[&amp;quot;-2^-57&amp;quot;,0,0],[&amp;quot;-2^-58&amp;quot;,0,0],[&amp;quot;-2^-59&amp;quot;,0,0],[&amp;quot;-2^-60&amp;quot;,0,0],[&amp;quot;-2^-61&amp;quot;,0,0],[&amp;quot;-2^-62&amp;quot;,0,0],[&amp;quot;-2^-63&amp;quot;,0,0],[&amp;quot;-2^-64&amp;quot;,0,0],[&amp;quot;-2^-65&amp;quot;,0,0],[&amp;quot;-2^-66&amp;quot;,0,0],[&amp;quot;-2^-67&amp;quot;,0,0],[&amp;quot;-2^-68&amp;quot;,0,0],[&amp;quot;-2^-69&amp;quot;,0,0],[&amp;quot;-2^-70&amp;quot;,0,0],[&amp;quot;-2^-71&amp;quot;,0,0],[&amp;quot;-2^-72&amp;quot;,0,0],[&amp;quot;-2^-73&amp;quot;,0,0],[&amp;quot;-2^-74&amp;quot;,0,0],[&amp;quot;-2^-75&amp;quot;,0,0],[&amp;quot;-2^-76&amp;quot;,0,0],[&amp;quot;-2^-77&amp;quot;,0,0],[&amp;quot;-2^-78&amp;quot;,0,0],[&amp;quot;-2^-79&amp;quot;,0,0],[&amp;quot;-2^-80&amp;quot;,0,0],[&amp;quot;-2^-81&amp;quot;,0,0],[&amp;quot;-2^-82&amp;quot;,0,0],[&amp;quot;-2^-83&amp;quot;,0,0],[&amp;quot;-2^-84&amp;quot;,0,0],[&amp;quot;-2^-85&amp;quot;,0,0],[&amp;quot;-2^-86&amp;quot;,0,0],[&amp;quot;-2^-87&amp;quot;,0,0],[&amp;quot;-2^-88&amp;quot;,0,0],[&amp;quot;-2^-89&amp;quot;,0,0],[&amp;quot;-2^-90&amp;quot;,0,0],[&amp;quot;-2^-91&amp;quot;,0,0],[&amp;quot;-2^-92&amp;quot;,0,0],[&amp;quot;-2^-93&amp;quot;,0,0],[&amp;quot;-2^-94&amp;quot;,0,0],[&amp;quot;-2^-95&amp;quot;,0,0],[&amp;quot;-2^-96&amp;quot;,0,0],[&amp;quot;-2^-97&amp;quot;,0,0],[&amp;quot;-2^-98&amp;quot;,0,0],[&amp;quot;-2^-99&amp;quot;,0,0],[&amp;quot;-2^-100&amp;quot;,0,0],[&amp;quot;-2^-101&amp;quot;,0,0],[&amp;quot;-2^-102&amp;quot;,0,0],[&amp;quot;-2^-103&amp;quot;,0,0],[&amp;quot;-2^-104&amp;quot;,0,0],[&amp;quot;-2^-105&amp;quot;,0,0],[&amp;quot;-2^-106&amp;quot;,0,0],[&amp;quot;-2^-107&amp;quot;,0,0],[&amp;quot;-2^-108&amp;quot;,0,0],[&amp;quot;-2^-109&amp;quot;,0,0],[&amp;quot;-2^-110&amp;quot;,0,0],[&amp;quot;-2^-111&amp;quot;,0,0],[&amp;quot;-2^-112&amp;quot;,0,0],[&amp;quot;-2^-113&amp;quot;,0,0],[&amp;quot;-2^-114&amp;quot;,0,0],[&amp;quot;-2^-115&amp;quot;,0,0],[&amp;quot;-2^-116&amp;quot;,0,0],[&amp;quot;-2^-117&amp;quot;,0,0],[&amp;quot;-2^-118&amp;quot;,0,0],[&amp;quot;-2^-119&amp;quot;,0,0],[&amp;quot;-2^-120&amp;quot;,0,0],[&amp;quot;-2^-121&amp;quot;,0,0],[&amp;quot;-2^-122&amp;quot;,0,0],[&amp;quot;-2^-123&amp;quot;,0,0],[&amp;quot;-2^-124&amp;quot;,0,0],[&amp;quot;-2^-125&amp;quot;,0,0],[&amp;quot;-2^-126&amp;quot;,0,0],[&amp;quot;-SN&amp;quot;,0,0],[&amp;quot;0&amp;quot;,0,0],[&amp;quot;+SN&amp;quot;,0,0],[&amp;quot;+2^-126&amp;quot;,0,0],[&amp;quot;+2^-125&amp;quot;,0,0],[&amp;quot;+2^-124&amp;quot;,0,0],[&amp;quot;+2^-123&amp;quot;,0,0],[&amp;quot;+2^-122&amp;quot;,0,0],[&amp;quot;+2^-121&amp;quot;,0,0],[&amp;quot;+2^-120&amp;quot;,0,0],[&amp;quot;+2^-119&amp;quot;,0,0],[&amp;quot;+2^-118&amp;quot;,0,0],[&amp;quot;+2^-117&amp;quot;,0,0],[&amp;quot;+2^-116&amp;quot;,0,0],[&amp;quot;+2^-115&amp;quot;,0,0],[&amp;quot;+2^-114&amp;quot;,0,0],[&amp;quot;+2^-113&amp;quot;,0,0],[&amp;quot;+2^-112&amp;quot;,0,0],[&amp;quot;+2^-111&amp;quot;,0,0],[&amp;quot;+2^-110&amp;quot;,0,0],[&amp;quot;+2^-109&amp;quot;,0,0],[&amp;quot;+2^-108&amp;quot;,0,0],[&amp;quot;+2^-107&amp;quot;,0,0],[&amp;quot;+2^-106&amp;quot;,0,0],[&amp;quot;+2^-105&amp;quot;,0,0],[&amp;quot;+2^-104&amp;quot;,0,0],[&amp;quot;+2^-103&amp;quot;,0,0],[&amp;quot;+2^-102&amp;quot;,0,0],[&amp;quot;+2^-101&amp;quot;,0,0],[&amp;quot;+2^-100&amp;quot;,0,0],[&amp;quot;+2^-99&amp;quot;,0,0],[&amp;quot;+2^-98&amp;quot;,0,0],[&amp;quot;+2^-97&amp;quot;,0,0],[&amp;quot;+2^-96&amp;quot;,0,0],[&amp;quot;+2^-95&amp;quot;,0,0],[&amp;quot;+2^-94&amp;quot;,0,0],[&amp;quot;+2^-93&amp;quot;,0,0],[&amp;quot;+2^-92&amp;quot;,0,0],[&amp;quot;+2^-91&amp;quot;,0,0],[&amp;quot;+2^-90&amp;quot;,0,0],[&amp;quot;+2^-89&amp;quot;,0,0],[&amp;quot;+2^-88&amp;quot;,0,0],[&amp;quot;+2^-87&amp;quot;,0,0],[&amp;quot;+2^-86&amp;quot;,0,0],[&amp;quot;+2^-85&amp;quot;,0,0],[&amp;quot;+2^-84&amp;quot;,0,0],[&amp;quot;+2^-83&amp;quot;,0,0],[&amp;quot;+2^-82&amp;quot;,0,0],[&amp;quot;+2^-81&amp;quot;,0,0],[&amp;quot;+2^-80&amp;quot;,0,0],[&amp;quot;+2^-79&amp;quot;,0,0],[&amp;quot;+2^-78&amp;quot;,0,0],[&amp;quot;+2^-77&amp;quot;,0,0],[&amp;quot;+2^-76&amp;quot;,0,0],[&amp;quot;+2^-75&amp;quot;,0,0],[&amp;quot;+2^-74&amp;quot;,0,0],[&amp;quot;+2^-73&amp;quot;,0,0],[&amp;quot;+2^-72&amp;quot;,0,0],[&amp;quot;+2^-71&amp;quot;,0,0],[&amp;quot;+2^-70&amp;quot;,0,0],[&amp;quot;+2^-69&amp;quot;,0,0],[&amp;quot;+2^-68&amp;quot;,0,0],[&amp;quot;+2^-67&amp;quot;,0,0],[&amp;quot;+2^-66&amp;quot;,0,0],[&amp;quot;+2^-65&amp;quot;,0,0],[&amp;quot;+2^-64&amp;quot;,0,0],[&amp;quot;+2^-63&amp;quot;,0,0],[&amp;quot;+2^-62&amp;quot;,0,0],[&amp;quot;+2^-61&amp;quot;,0,0],[&amp;quot;+2^-60&amp;quot;,0,0],[&amp;quot;+2^-59&amp;quot;,0,0],[&amp;quot;+2^-58&amp;quot;,0,0],[&amp;quot;+2^-57&amp;quot;,0,0],[&amp;quot;+2^-56&amp;quot;,0,0],[&amp;quot;+2^-55&amp;quot;,0,0],[&amp;quot;+2^-54&amp;quot;,1.2326e-32,1.86265e-09],[&amp;quot;+2^-53&amp;quot;,2.46519e-32,1.86265e-09],[&amp;quot;+2^-52&amp;quot;,4.93038e-32,1.86265e-09],[&amp;quot;+2^-51&amp;quot;,9.86076e-32,1.86265e-09],[&amp;quot;+2^-50&amp;quot;,1.97215e-31,1.86265e-09],[&amp;quot;+2^-49&amp;quot;,3.9443e-31,1.86265e-09],[&amp;quot;+2^-48&amp;quot;,7.88861e-31,1.86265e-09],[&amp;quot;+2^-47&amp;quot;,1.57772e-30,1.86265e-09],[&amp;quot;+2^-46&amp;quot;,3.15544e-30,1.86265e-09],[&amp;quot;+2^-45&amp;quot;,6.31089e-30,1.86265e-09],[&amp;quot;+2^-44&amp;quot;,1.26218e-29,1.86265e-09],[&amp;quot;+2^-43&amp;quot;,2.52435e-29,1.86265e-09],[&amp;quot;+2^-42&amp;quot;,5.04871e-29,1.86265e-09],[&amp;quot;+2^-41&amp;quot;,1.00974e-28,1.86265e-09],[&amp;quot;+2^-40&amp;quot;,2.01948e-28,1.86265e-09],[&amp;quot;+2^-39&amp;quot;,4.03897e-28,1.86265e-09],[&amp;quot;+2^-38&amp;quot;,8.07794e-28,1.86265e-09],[&amp;quot;+2^-37&amp;quot;,1.61559e-27,1.86265e-09],[&amp;quot;+2^-36&amp;quot;,3.23117e-27,1.86265e-09],[&amp;quot;+2^-35&amp;quot;,6.46235e-27,1.86265e-09],[&amp;quot;+2^-34&amp;quot;,1.29247e-26,1.86265e-09],[&amp;quot;+2^-33&amp;quot;,2.58494e-26,1.86265e-09],[&amp;quot;+2^-32&amp;quot;,5.16988e-26,1.86265e-09],[&amp;quot;+2^-31&amp;quot;,1.03398e-25,1.86265e-09],[&amp;quot;+2^-30&amp;quot;,2.06795e-25,1.86265e-09],[&amp;quot;+2^-29&amp;quot;,4.1359e-25,1.86265e-09],[&amp;quot;+2^-28&amp;quot;,8.27181e-25,1.86265e-09],[&amp;quot;+2^-27&amp;quot;,1.65436e-24,1.86265e-09],[&amp;quot;+2^-26&amp;quot;,9.92617e-24,5.58794e-09],[&amp;quot;+2^-25&amp;quot;,7.27919e-23,2.04891e-08],[&amp;quot;+2^-24&amp;quot;,5.691e-22,8.00937e-08],[&amp;quot;+2^-23&amp;quot;,4.52633e-21,3.18512e-07],[&amp;quot;+2^-22&amp;quot;,3.61577e-20,1.27219e-06],[&amp;quot;+2^-21&amp;quot;,2.89156e-19,5.08688e-06],[&amp;quot;+2^-20&amp;quot;,2.31304e-18,2.03457e-05],[&amp;quot;+2^-19&amp;quot;,1.85039e-17,8.13808e-05],[&amp;quot;+2^-18&amp;quot;,1.48029e-16,0.00032552],[&amp;quot;+2^-17&amp;quot;,1.18424e-15,0.00130208],[&amp;quot;+2^-16&amp;quot;,9.47387e-15,0.00520832],[&amp;quot;+2^-15&amp;quot;,7.5791e-14,0.0208333],[&amp;quot;+2^-14&amp;quot;,6.06328e-13,0.0833331],[&amp;quot;+2^-13&amp;quot;,4.85062e-12,0.333332],[&amp;quot;+2^-12&amp;quot;,1.45519e-11,0.5],[&amp;quot;+2^-11&amp;quot;,2.91037e-11,0.499998],[&amp;quot;+2^-10&amp;quot;,5.82082e-11,0.500004],[&amp;quot;+2^-9&amp;quot;,1.16423e-10,0.500033],[&amp;quot;+2^-8&amp;quot;,2.32917e-10,0.500185],[&amp;quot;+2^-7&amp;quot;,4.66402e-10,0.500795],[&amp;quot;+2^-6&amp;quot;,9.51472e-10,0.510818],[&amp;quot;+2^-5&amp;quot;,3.89213e-09,1.04478],[&amp;quot;+2^-4&amp;quot;,2.57737e-07,34.5929],[&amp;quot;+2^-3&amp;quot;,3.11475e-05,2090.28],[&amp;quot;+2^-2&amp;quot;,0.00318735,106950],[&amp;quot;+2^-1&amp;quot;,0.127635,2.14135e+06],[&amp;quot;+2^0&amp;quot;,0.940715,1.57826e+07],[&amp;quot;+2^1&amp;quot;,2.94633,4.94312e+07],[&amp;quot;+2^2&amp;quot;,6.96672,1.16882e+08],[&amp;quot;+2^3&amp;quot;,14.9718,1.33874e+08],[&amp;quot;+2^4&amp;quot;,30.9629,2.59736e+08],[&amp;quot;+2^5&amp;quot;,62.9354,5.27941e+08],[&amp;quot;+2^6&amp;quot;,126.876,1.06431e+09],[&amp;quot;+2^7&amp;quot;,254.754,2.13703e+09],[&amp;quot;+2^8&amp;quot;,510.509,4.28246e+09],[&amp;quot;+2^9&amp;quot;,1022.02,8.57331e+09],[&amp;quot;+2^10&amp;quot;,2045.04,1.7155e+10],[&amp;quot;+2^11&amp;quot;,4091.07,3.43184e+10],[&amp;quot;+2^12&amp;quot;,8183.15,6.86452e+10],[&amp;quot;+2^13&amp;quot;,16367.3,1.37299e+11],[&amp;quot;+2^14&amp;quot;,32735.6,2.74606e+11],[&amp;quot;+2^15&amp;quot;,65472.2,5.4922e+11],[&amp;quot;+2^16&amp;quot;,130945,1.09845e+12],[&amp;quot;+2^17&amp;quot;,261892,2.19691e+12],[&amp;quot;+2^18&amp;quot;,523784,4.39382e+12],[&amp;quot;+2^19&amp;quot;,1.04757e+06,8.78765e+12],[&amp;quot;+2^20&amp;quot;,2.09514e+06,1.75753e+13],[&amp;quot;+2^21&amp;quot;,4.19028e+06,3.51506e+13],[&amp;quot;+2^22&amp;quot;,8.38056e+06,7.03013e+13],[&amp;quot;+2^23&amp;quot;,1.67611e+07,1.40603e+14],[&amp;quot;+2^24&amp;quot;,3.35223e+07,2.81205e+14],[&amp;quot;+2^25&amp;quot;,6.70445e+07,5.6241e+14],[&amp;quot;+2^26&amp;quot;,1.34089e+08,1.12482e+15],[&amp;quot;+2^27&amp;quot;,2.68178e+08,2.24964e+15],[&amp;quot;+2^28&amp;quot;,5.36356e+08,4.49928e+15],[&amp;quot;+2^29&amp;quot;,1.07271e+09,8.99856e+15],[&amp;quot;+2^30&amp;quot;,2.14542e+09,1.79971e+16],[&amp;quot;+2^31&amp;quot;,4.29085e+09,3.59943e+16],[&amp;quot;+2^32&amp;quot;,8.58993e+09,7.20575e+16],[&amp;quot;+2^33&amp;quot;,1.71799e+10,1.44115e+17],[&amp;quot;+2^34&amp;quot;,3.43597e+10,2.8823e+17],[&amp;quot;+2^35&amp;quot;,6.87194e+10,5.7646e+17],[&amp;quot;+2^36&amp;quot;,1.37439e+11,1.15292e+18],[&amp;quot;+2^37&amp;quot;,2.74878e+11,2.30584e+18],[&amp;quot;+2^38&amp;quot;,5.49755e+11,4.61168e+18],[&amp;quot;+2^39&amp;quot;,1.09951e+12,9.22336e+18],[&amp;quot;+2^40&amp;quot;,2.19902e+12,1.84467e+19],[&amp;quot;+2^41&amp;quot;,4.39804e+12,3.68935e+19],[&amp;quot;+2^42&amp;quot;,8.79608e+12,7.37869e+19],[&amp;quot;+2^43&amp;quot;,1.75922e+13,1.47574e+20],[&amp;quot;+2^44&amp;quot;,3.51843e+13,2.95148e+20],[&amp;quot;+2^45&amp;quot;,7.03687e+13,5.90295e+20],[&amp;quot;+2^46&amp;quot;,1.40737e+14,1.18059e+21],[&amp;quot;+2^47&amp;quot;,2.81475e+14,2.36118e+21],[&amp;quot;+2^48&amp;quot;,5.62949e+14,4.72236e+21],[&amp;quot;+2^49&amp;quot;,1.1259e+15,9.44472e+21],[&amp;quot;+2^50&amp;quot;,2.2518e+15,1.88894e+22],[&amp;quot;+2^51&amp;quot;,4.5036e+15,3.77789e+22],[&amp;quot;+2^52&amp;quot;,9.00719e+15,7.55578e+22],[&amp;quot;+2^53&amp;quot;,1.80144e+16,1.51116e+23],[&amp;quot;+2^54&amp;quot;,3.60288e+16,3.02231e+23],[&amp;quot;+2^55&amp;quot;,7.20575e+16,6.04462e+23],[&amp;quot;+2^56&amp;quot;,1.44115e+17,1.20892e+24],[&amp;quot;+2^57&amp;quot;,2.8823e+17,2.41785e+24],[&amp;quot;+2^58&amp;quot;,5.7646e+17,4.8357e+24],[&amp;quot;+2^59&amp;quot;,1.15292e+18,9.6714e+24],[&amp;quot;+2^60&amp;quot;,2.30584e+18,1.93428e+25],[&amp;quot;+2^61&amp;quot;,4.61168e+18,3.86856e+25],[&amp;quot;+2^62&amp;quot;,9.22336e+18,7.73712e+25],[&amp;quot;+2^63&amp;quot;,1.84467e+19,1.54742e+26],[&amp;quot;+2^64&amp;quot;,0,0],[&amp;quot;+2^65&amp;quot;,0,0],[&amp;quot;+2^66&amp;quot;,0,0],[&amp;quot;+2^67&amp;quot;,0,0],[&amp;quot;+2^68&amp;quot;,0,0],[&amp;quot;+2^69&amp;quot;,0,0],[&amp;quot;+2^70&amp;quot;,0,0],[&amp;quot;+2^71&amp;quot;,0,0],[&amp;quot;+2^72&amp;quot;,0,0],[&amp;quot;+2^73&amp;quot;,0,0],[&amp;quot;+2^74&amp;quot;,0,0],[&amp;quot;+2^75&amp;quot;,0,0],[&amp;quot;+2^76&amp;quot;,0,0],[&amp;quot;+2^77&amp;quot;,0,0],[&amp;quot;+2^78&amp;quot;,0,0],[&amp;quot;+2^79&amp;quot;,0,0],[&amp;quot;+2^80&amp;quot;,0,0],[&amp;quot;+2^81&amp;quot;,0,0],[&amp;quot;+2^82&amp;quot;,0,0],[&amp;quot;+2^83&amp;quot;,0,0],[&amp;quot;+2^84&amp;quot;,0,0],[&amp;quot;+2^85&amp;quot;,0,0],[&amp;quot;+2^86&amp;quot;,0,0],[&amp;quot;+2^87&amp;quot;,0,0],[&amp;quot;+2^88&amp;quot;,0,0],[&amp;quot;+2^89&amp;quot;,0,0],[&amp;quot;+2^90&amp;quot;,0,0],[&amp;quot;+2^91&amp;quot;,0,0],[&amp;quot;+2^92&amp;quot;,0,0],[&amp;quot;+2^93&amp;quot;,0,0],[&amp;quot;+2^94&amp;quot;,0,0],[&amp;quot;+2^95&amp;quot;,0,0],[&amp;quot;+2^96&amp;quot;,0,0],[&amp;quot;+2^97&amp;quot;,0,0],[&amp;quot;+2^98&amp;quot;,0,0],[&amp;quot;+2^99&amp;quot;,0,0],[&amp;quot;+2^100&amp;quot;,0,0],[&amp;quot;+2^101&amp;quot;,0,0],[&amp;quot;+2^102&amp;quot;,0,0],[&amp;quot;+2^103&amp;quot;,0,0],[&amp;quot;+2^104&amp;quot;,0,0],[&amp;quot;+2^105&amp;quot;,0,0],[&amp;quot;+2^106&amp;quot;,0,0],[&amp;quot;+2^107&amp;quot;,0,0],[&amp;quot;+2^108&amp;quot;,0,0],[&amp;quot;+2^109&amp;quot;,0,0],[&amp;quot;+2^110&amp;quot;,0,0],[&amp;quot;+2^111&amp;quot;,0,0],[&amp;quot;+2^112&amp;quot;,0,0],[&amp;quot;+2^113&amp;quot;,0,0],[&amp;quot;+2^114&amp;quot;,0,0],[&amp;quot;+2^115&amp;quot;,0,0],[&amp;quot;+2^116&amp;quot;,0,0],[&amp;quot;+2^117&amp;quot;,0,0],[&amp;quot;+2^118&amp;quot;,0,0],[&amp;quot;+2^119&amp;quot;,0,0],[&amp;quot;+2^120&amp;quot;,0,0],[&amp;quot;+2^121&amp;quot;,0,0],[&amp;quot;+2^122&amp;quot;,0,0],[&amp;quot;+2^123&amp;quot;,0,0],[&amp;quot;+2^124&amp;quot;,0,0],[&amp;quot;+2^125&amp;quot;,0,0],[&amp;quot;+2^126&amp;quot;,0,0],[&amp;quot;+2^127&amp;quot;,0,0] ]&lt;br /&gt;
 }}&lt;br /&gt;
&lt;br /&gt;
== Localized Approximation Functions ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
!Id&lt;br /&gt;
!Formula&lt;br /&gt;
!ULP &amp;lt;= 2.0&lt;br /&gt;
!NAN&lt;br /&gt;
|-&lt;br /&gt;
|signum&lt;br /&gt;
|&amp;lt;math&amp;gt;\sgn{x}&amp;lt;/math&amp;gt;&lt;br /&gt;
| -inf &amp;lt; x &amp;lt; -8.31763&lt;br /&gt;
8.31763 &amp;lt; x &amp;lt; inf&lt;br /&gt;
| -&lt;br /&gt;
|-&lt;br /&gt;
|poly1&lt;br /&gt;
|&amp;lt;math&amp;gt;x&amp;lt;/math&amp;gt;&lt;br /&gt;
| -0.000704229 &amp;lt; x &amp;lt; 0.000704229&lt;br /&gt;
| -&lt;br /&gt;
|-&lt;br /&gt;
|poly3&lt;br /&gt;
|&amp;lt;math&amp;gt;x - \frac{1}{3} x^3&amp;lt;/math&amp;gt;&lt;br /&gt;
| -0.0291781 &amp;lt; x &amp;lt; 0.0291781 &lt;br /&gt;
| -&lt;br /&gt;
|-&lt;br /&gt;
|lampert7&lt;br /&gt;
|&amp;lt;math&amp;gt;\frac{x \cdot (135135 + x^2 \cdot (17325 + x^2 \cdot (378 + x^2)))}{135135 + x^2 \cdot (62370 + x^2 \cdot (3150 + 28 \cdot x^2))}&lt;br /&gt;
&amp;lt;/math&amp;gt;&lt;br /&gt;
| -0.000947416 &amp;lt; x &amp;lt; 0.000947207&lt;br /&gt;
| -inf &amp;lt; x &amp;lt; -1.51629e+06&lt;br /&gt;
1.51629e+06 &amp;lt; x &amp;lt; inf&lt;br /&gt;
|-&lt;br /&gt;
|pade&lt;br /&gt;
|&amp;lt;math&amp;gt;x + x \cdot \frac{x^2 \cdot \left( p_1 \cdot x^2 + p_0 \right)}{\left( x^2 + q_1 \right) \cdot x^2 + q_0} &lt;br /&gt;
&lt;br /&gt;
&amp;lt;/math&amp;gt; &amp;lt;math&amp;gt;\begin{align}&lt;br /&gt;
p_0 &amp;amp;= -0.2059432032 \\&lt;br /&gt;
p_1 &amp;amp;= -0.0009577527 \\&lt;br /&gt;
q_0 &amp;amp;= 0.6178299136 \\ &lt;br /&gt;
q_1 &amp;amp;= 0.25&lt;br /&gt;
\end{align}&lt;br /&gt;
&amp;lt;/math&amp;gt;&lt;br /&gt;
| -0.0016973 &amp;lt; x &amp;lt; 0.0016973 &lt;br /&gt;
| -inf &amp;lt; x &amp;lt; -6.14896e+18&lt;br /&gt;
6.14896e+18 &amp;lt; x &amp;lt; inf&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== External Links ==&lt;br /&gt;
&lt;br /&gt;
* https://yaikhom.com/2020-04-28-localised-approximation-of-hyperbolic-tangents.html#mjx-eqn-eqn7-th%20degree%20lambert%20approximant&lt;br /&gt;
*https://github.com/llvm/llvm-project/blob/main/libc/src/math/generic/tanhf.cpp&lt;br /&gt;
*https://forums.developer.nvidia.com/t/hardware-accelerated-tanh-on-turing/173291&lt;br /&gt;
[[Category:Numerical Precision]]&lt;/div&gt;</summary>
		<author><name>Timo.stripf</name></author>
	</entry>
	<entry>
		<id>https://www.emmtrix.com/w139/index.php?title=tanh_Software_Implementation&amp;diff=2925</id>
		<title>tanh Software Implementation</title>
		<link rel="alternate" type="text/html" href="https://www.emmtrix.com/w139/index.php?title=tanh_Software_Implementation&amp;diff=2925"/>
		<updated>2026-02-04T11:45:10Z</updated>

		<summary type="html">&lt;p&gt;Timo.stripf: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;[[File:Hyperbolic Tangent.svg|thumb]]&lt;br /&gt;
The hyperbolic tangent function, commonly referred to as &amp;lt;code&amp;gt;tanh&amp;lt;/code&amp;gt;, is a mathematical function that maps real numbers to the range &amp;lt;math&amp;gt;(-1, 1)&amp;lt;/math&amp;gt;. It is defined as:&lt;br /&gt;
&lt;br /&gt;
:&amp;lt;math&amp;gt;\tanh(x) = \frac{e^x - e^{-x}}{e^x + e^{-x}}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
From a software implementation perspective, &amp;lt;code&amp;gt;tanh&amp;lt;/code&amp;gt; is a numerically challenging function due to the use of exponential functions, which can easily overflow for large inputs and suffer from catastrophic cancellation for very small values of &amp;lt;math&amp;gt;x&amp;lt;/math&amp;gt;. These issues are particularly relevant in single-precision floating-point (&amp;lt;code&amp;gt;float32&amp;lt;/code&amp;gt;) implementations, where both accuracy and performance are critical.&lt;br /&gt;
&lt;br /&gt;
In machine learning and deep learning, &amp;lt;code&amp;gt;tanh&amp;lt;/code&amp;gt; is widely used as an activation function because it introduces non-linearity and produces outputs centered around zero, which can improve gradient-based optimization. As a result, &amp;lt;code&amp;gt;tanh&amp;lt;/code&amp;gt; is often evaluated billions of times during training and inference, making efficient and numerically stable implementations essential.&lt;br /&gt;
&lt;br /&gt;
This article focuses on practical software implementations of &amp;lt;code&amp;gt;tanh&amp;lt;/code&amp;gt;, analyzing different computational formulations with respect to floating-point accuracy, ULP error, overflow behavior, and NaN generation. The goal is to provide a foundation for designing robust &amp;lt;code&amp;gt;tanh&amp;lt;/code&amp;gt; implementations suitable for performance-critical environments such as numerical libraries, embedded systems, and AI workloads.&lt;br /&gt;
&lt;br /&gt;
== Simple Approximation with Exponential Math Function ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+&lt;br /&gt;
!Id&lt;br /&gt;
!Formula&lt;br /&gt;
!Highest ULP Error&lt;br /&gt;
!ULP &amp;lt;= 2.0&lt;br /&gt;
!NaN&lt;br /&gt;
|-&lt;br /&gt;
|exp_v1&lt;br /&gt;
|&amp;lt;math&amp;gt;\frac{e^x - e^{-x}}{e^x + e^{-x}}&amp;lt;/math&amp;gt;&lt;br /&gt;
|x &amp;lt; 0: 8.55638e+08 at -2.98023e-08 with 0x0p+0 instead of -0x1p-25&lt;br /&gt;
&lt;br /&gt;
x &amp;gt; 0: 8.55638e+08 at 2.98023e-08 with 0x0p+0 instead of 0x1p-25&lt;br /&gt;
| -88.7246 &amp;lt; x &amp;lt; -7.14197&lt;br /&gt;
7.14197 &amp;lt; x &amp;lt; 88.7246&lt;br /&gt;
| -inf &amp;lt; x &amp;lt; -88.7246&lt;br /&gt;
88.7246 &amp;lt; x &amp;lt; inf&lt;br /&gt;
|-&lt;br /&gt;
|exp_v2&lt;br /&gt;
|&amp;lt;math&amp;gt;\frac{e^{2x} - 1}{e^{2x} + 1}&amp;lt;/math&amp;gt;&lt;br /&gt;
|x &amp;lt; 0: 8.47249e+08 at -1.49012e-08 with 0x0p+0 instead of -0x1p-26&lt;br /&gt;
x &amp;gt; 0: 8.55638e+08 at 2.98023e-08 with 0x0p+0 instead of 0x1.fffffep-26&lt;br /&gt;
| -inf &amp;lt; x &amp;lt; -0.547821&lt;br /&gt;
0.254822 &amp;lt; x &amp;lt; 8.31787&lt;br /&gt;
&lt;br /&gt;
8.38379 &amp;lt; x &amp;lt; 44.3623&lt;br /&gt;
|44.3623 &amp;lt; x &amp;lt; inf&lt;br /&gt;
|-&lt;br /&gt;
|exp_v3&lt;br /&gt;
|&amp;lt;math&amp;gt;1 - \frac{2}{e^{2x} + 1}&amp;lt;/math&amp;gt;&lt;br /&gt;
|x &amp;lt; 0: 8.59832e+08 at -4.47035e-08 with 0x0p+0 instead of -0x1.8p-25&lt;br /&gt;
x &amp;gt; 0: 8.68221e+08 at 8.9407e-08 with 0x0p+0 instead of 0x1.7ffffcp-24&lt;br /&gt;
| -inf &amp;lt; x &amp;lt; -6.26233&lt;br /&gt;
0.346382 &amp;lt; x &amp;lt; inf&lt;br /&gt;
| -&lt;br /&gt;
|-&lt;br /&gt;
|expm1&lt;br /&gt;
|&amp;lt;math&amp;gt;\frac{expm1(2x)}{expm1(2x) + 2}&amp;lt;/math&amp;gt; &lt;br /&gt;
|x &amp;lt; 0: 2.49585 at -3.95627 with -0x1.ffa00ap-1 instead of -0x1.ffa00ep-1&lt;br /&gt;
x &amp;gt; 0: 2.41657 at 0.0155837 with 0x1.fe9b66p-7 instead of 0x1.fe9b62p-7&lt;br /&gt;
| -inf &amp;lt; x &amp;lt; -5.57227&lt;br /&gt;
-0.42981 &amp;lt; x &amp;lt; 0.00777948&lt;br /&gt;
&lt;br /&gt;
0.125351 &amp;lt; x &amp;lt; 44.3623&lt;br /&gt;
|44.3623 &amp;lt; x &amp;lt; inf&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
The first equation is the definition of the hyperbolic tangent function. This equation requires two exponentials with different values. Since exponentials are expensive to compute, we can use the second equation to reduce the number of exponentials to one.&lt;br /&gt;
&lt;br /&gt;
The second equation has the drawback that both the numerator and the denominator can become infinity for large values of x which results in NaN. That happens for &amp;lt;math&amp;gt;|x| &amp;gt; \log(FLT\_MAX)/2 \approx 44&amp;lt;/math&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
The third equation solves this problem but has a slightly higher error for &amp;lt;math&amp;gt;x \approx 2^{-25}&amp;lt;/math&amp;gt;. In the following graph you can see the error chart for equation 3. The error charts for the other two equations are very similar. All three equations have an error of up to 16 mio ULPs for &amp;lt;math&amp;gt;x &amp;lt; 2^{-5}&amp;lt;/math&amp;gt; which means that up to 24 bits could be wrong.&lt;br /&gt;
&lt;br /&gt;
=== exp_v2 ===&lt;br /&gt;
&lt;br /&gt;
{{#widget:FloatErrorGraph|id=tanhf_exp_v2&lt;br /&gt;
  |data=[ [&amp;quot;-2^127&amp;quot;,0,1.17549e-38],[&amp;quot;-2^126&amp;quot;,0,1.17549e-38],[&amp;quot;-2^125&amp;quot;,0,1.17549e-38],[&amp;quot;-2^124&amp;quot;,0,1.17549e-38],[&amp;quot;-2^123&amp;quot;,0,1.17549e-38],[&amp;quot;-2^122&amp;quot;,0,1.17549e-38],[&amp;quot;-2^121&amp;quot;,0,1.17549e-38],[&amp;quot;-2^120&amp;quot;,0,1.17549e-38],[&amp;quot;-2^119&amp;quot;,0,1.17549e-38],[&amp;quot;-2^118&amp;quot;,0,1.17549e-38],[&amp;quot;-2^117&amp;quot;,0,1.17549e-38],[&amp;quot;-2^116&amp;quot;,0,1.17549e-38],[&amp;quot;-2^115&amp;quot;,0,1.17549e-38],[&amp;quot;-2^114&amp;quot;,0,1.17549e-38],[&amp;quot;-2^113&amp;quot;,0,1.17549e-38],[&amp;quot;-2^112&amp;quot;,0,1.17549e-38],[&amp;quot;-2^111&amp;quot;,0,1.17549e-38],[&amp;quot;-2^110&amp;quot;,0,1.17549e-38],[&amp;quot;-2^109&amp;quot;,0,1.17549e-38],[&amp;quot;-2^108&amp;quot;,0,1.17549e-38],[&amp;quot;-2^107&amp;quot;,0,1.17549e-38],[&amp;quot;-2^106&amp;quot;,0,1.17549e-38],[&amp;quot;-2^105&amp;quot;,0,1.17549e-38],[&amp;quot;-2^104&amp;quot;,0,1.17549e-38],[&amp;quot;-2^103&amp;quot;,0,1.17549e-38],[&amp;quot;-2^102&amp;quot;,0,1.17549e-38],[&amp;quot;-2^101&amp;quot;,0,1.17549e-38],[&amp;quot;-2^100&amp;quot;,0,1.17549e-38],[&amp;quot;-2^99&amp;quot;,0,1.17549e-38],[&amp;quot;-2^98&amp;quot;,0,1.17549e-38],[&amp;quot;-2^97&amp;quot;,0,1.17549e-38],[&amp;quot;-2^96&amp;quot;,0,1.17549e-38],[&amp;quot;-2^95&amp;quot;,0,1.17549e-38],[&amp;quot;-2^94&amp;quot;,0,1.17549e-38],[&amp;quot;-2^93&amp;quot;,0,1.17549e-38],[&amp;quot;-2^92&amp;quot;,0,1.17549e-38],[&amp;quot;-2^91&amp;quot;,0,1.17549e-38],[&amp;quot;-2^90&amp;quot;,0,1.17549e-38],[&amp;quot;-2^89&amp;quot;,0,1.17549e-38],[&amp;quot;-2^88&amp;quot;,0,1.17549e-38],[&amp;quot;-2^87&amp;quot;,0,1.17549e-38],[&amp;quot;-2^86&amp;quot;,0,1.17549e-38],[&amp;quot;-2^85&amp;quot;,0,1.17549e-38],[&amp;quot;-2^84&amp;quot;,0,1.17549e-38],[&amp;quot;-2^83&amp;quot;,0,1.17549e-38],[&amp;quot;-2^82&amp;quot;,0,1.17549e-38],[&amp;quot;-2^81&amp;quot;,0,1.17549e-38],[&amp;quot;-2^80&amp;quot;,0,1.17549e-38],[&amp;quot;-2^79&amp;quot;,0,1.17549e-38],[&amp;quot;-2^78&amp;quot;,0,1.17549e-38],[&amp;quot;-2^77&amp;quot;,0,1.17549e-38],[&amp;quot;-2^76&amp;quot;,0,1.17549e-38],[&amp;quot;-2^75&amp;quot;,0,1.17549e-38],[&amp;quot;-2^74&amp;quot;,0,1.17549e-38],[&amp;quot;-2^73&amp;quot;,0,1.17549e-38],[&amp;quot;-2^72&amp;quot;,0,1.17549e-38],[&amp;quot;-2^71&amp;quot;,0,1.17549e-38],[&amp;quot;-2^70&amp;quot;,0,1.17549e-38],[&amp;quot;-2^69&amp;quot;,0,1.17549e-38],[&amp;quot;-2^68&amp;quot;,0,1.17549e-38],[&amp;quot;-2^67&amp;quot;,0,1.17549e-38],[&amp;quot;-2^66&amp;quot;,0,1.17549e-38],[&amp;quot;-2^65&amp;quot;,0,1.17549e-38],[&amp;quot;-2^64&amp;quot;,0,1.17549e-38],[&amp;quot;-2^63&amp;quot;,0,1.17549e-38],[&amp;quot;-2^62&amp;quot;,0,1.17549e-38],[&amp;quot;-2^61&amp;quot;,0,1.17549e-38],[&amp;quot;-2^60&amp;quot;,0,1.17549e-38],[&amp;quot;-2^59&amp;quot;,0,1.17549e-38],[&amp;quot;-2^58&amp;quot;,0,1.17549e-38],[&amp;quot;-2^57&amp;quot;,0,1.17549e-38],[&amp;quot;-2^56&amp;quot;,0,1.17549e-38],[&amp;quot;-2^55&amp;quot;,0,1.17549e-38],[&amp;quot;-2^54&amp;quot;,0,1.17549e-38],[&amp;quot;-2^53&amp;quot;,0,1.17549e-38],[&amp;quot;-2^52&amp;quot;,0,1.17549e-38],[&amp;quot;-2^51&amp;quot;,0,1.17549e-38],[&amp;quot;-2^50&amp;quot;,0,1.17549e-38],[&amp;quot;-2^49&amp;quot;,0,1.17549e-38],[&amp;quot;-2^48&amp;quot;,0,1.17549e-38],[&amp;quot;-2^47&amp;quot;,0,1.17549e-38],[&amp;quot;-2^46&amp;quot;,0,1.17549e-38],[&amp;quot;-2^45&amp;quot;,0,1.17549e-38],[&amp;quot;-2^44&amp;quot;,0,1.17549e-38],[&amp;quot;-2^43&amp;quot;,0,1.17549e-38],[&amp;quot;-2^42&amp;quot;,0,1.17549e-38],[&amp;quot;-2^41&amp;quot;,0,1.17549e-38],[&amp;quot;-2^40&amp;quot;,0,1.17549e-38],[&amp;quot;-2^39&amp;quot;,0,1.17549e-38],[&amp;quot;-2^38&amp;quot;,0,1.17549e-38],[&amp;quot;-2^37&amp;quot;,0,1.17549e-38],[&amp;quot;-2^36&amp;quot;,0,1.17549e-38],[&amp;quot;-2^35&amp;quot;,0,1.17549e-38],[&amp;quot;-2^34&amp;quot;,0,1.17549e-38],[&amp;quot;-2^33&amp;quot;,0,1.17549e-38],[&amp;quot;-2^32&amp;quot;,0,1.17549e-38],[&amp;quot;-2^31&amp;quot;,0,1.17549e-38],[&amp;quot;-2^30&amp;quot;,0,1.17549e-38],[&amp;quot;-2^29&amp;quot;,0,1.17549e-38],[&amp;quot;-2^28&amp;quot;,0,1.17549e-38],[&amp;quot;-2^27&amp;quot;,0,1.17549e-38],[&amp;quot;-2^26&amp;quot;,0,1.17549e-38],[&amp;quot;-2^25&amp;quot;,0,1.17549e-38],[&amp;quot;-2^24&amp;quot;,0,1.17549e-38],[&amp;quot;-2^23&amp;quot;,0,1.17549e-38],[&amp;quot;-2^22&amp;quot;,0,1.17549e-38],[&amp;quot;-2^21&amp;quot;,0,1.17549e-38],[&amp;quot;-2^20&amp;quot;,0,1.17549e-38],[&amp;quot;-2^19&amp;quot;,0,1.17549e-38],[&amp;quot;-2^18&amp;quot;,0,1.17549e-38],[&amp;quot;-2^17&amp;quot;,0,1.17549e-38],[&amp;quot;-2^16&amp;quot;,0,1.17549e-38],[&amp;quot;-2^15&amp;quot;,0,1.17549e-38],[&amp;quot;-2^14&amp;quot;,0,1.17549e-38],[&amp;quot;-2^13&amp;quot;,0,1.17549e-38],[&amp;quot;-2^12&amp;quot;,0,1.17549e-38],[&amp;quot;-2^11&amp;quot;,0,1.17549e-38],[&amp;quot;-2^10&amp;quot;,0,1.17549e-38],[&amp;quot;-2^9&amp;quot;,0,1.17549e-38],[&amp;quot;-2^8&amp;quot;,0,1.17549e-38],[&amp;quot;-2^7&amp;quot;,0,1.17549e-38],[&amp;quot;-2^6&amp;quot;,0,1.17549e-38],[&amp;quot;-2^5&amp;quot;,0,1.17549e-38],[&amp;quot;-2^4&amp;quot;,2.53131e-14,4.24683e-07],[&amp;quot;-2^3&amp;quot;,5.95888e-08,0.999734],[&amp;quot;-2^2&amp;quot;,8.86696e-08,1.48763],[&amp;quot;-2^1&amp;quot;,8.91609e-08,1.49587],[&amp;quot;-2^0&amp;quot;,8.73743e-08,1.4659],[&amp;quot;-2^-1&amp;quot;,8.65382e-08,2.13222],[&amp;quot;-2^-2&amp;quot;,6.18481e-08,2.61428],[&amp;quot;-2^-3&amp;quot;,3.88071e-08,3.08594],[&amp;quot;-2^-4&amp;quot;,2.6234e-08,4.23594],[&amp;quot;-2^-5&amp;quot;,2.02799e-08,8.19726],[&amp;quot;-2^-6&amp;quot;,1.75183e-08,12.4495],[&amp;quot;-2^-7&amp;quot;,1.61937e-08,17.3878],[&amp;quot;-2^-8&amp;quot;,1.5566e-08,33.4278],[&amp;quot;-2^-9&amp;quot;,1.51688e-08,65.1495],[&amp;quot;-2^-10&amp;quot;,1.50281e-08,129.09],[&amp;quot;-2^-11&amp;quot;,1.49613e-08,257.033],[&amp;quot;-2^-12&amp;quot;,1.49296e-08,512.977],[&amp;quot;-2^-13&amp;quot;,1.49174e-08,1025.12],[&amp;quot;-2^-14&amp;quot;,1.4909e-08,2049.08],[&amp;quot;-2^-15&amp;quot;,1.49049e-08,4097.02],[&amp;quot;-2^-16&amp;quot;,1.4903e-08,8193],[&amp;quot;-2^-17&amp;quot;,1.49021e-08,16385],[&amp;quot;-2^-18&amp;quot;,1.48862e-08,32735],[&amp;quot;-2^-19&amp;quot;,1.48973e-08,65519],[&amp;quot;-2^-20&amp;quot;,1.49001e-08,131063],[&amp;quot;-2^-21&amp;quot;,1.49009e-08,262139],[&amp;quot;-2^-22&amp;quot;,1.49011e-08,524285],[&amp;quot;-2^-23&amp;quot;,1.49011e-08,1.04857e+06],[&amp;quot;-2^-24&amp;quot;,1.49011e-08,2.09715e+06],[&amp;quot;-2^-25&amp;quot;,1.49012e-08,4.1943e+06],[&amp;quot;-2^-26&amp;quot;,1.49007e-08,8.38835e+06],[&amp;quot;-2^-27&amp;quot;,1.49012e-08,8.47249e+08],[&amp;quot;-2^-28&amp;quot;,7.45058e-09,8.38861e+08],[&amp;quot;-2^-29&amp;quot;,3.72529e-09,8.30472e+08],[&amp;quot;-2^-30&amp;quot;,1.86265e-09,8.22084e+08],[&amp;quot;-2^-31&amp;quot;,9.31323e-10,8.13695e+08],[&amp;quot;-2^-32&amp;quot;,4.65661e-10,8.05306e+08],[&amp;quot;-2^-33&amp;quot;,2.32831e-10,7.96918e+08],[&amp;quot;-2^-34&amp;quot;,1.16415e-10,7.88529e+08],[&amp;quot;-2^-35&amp;quot;,5.82077e-11,7.80141e+08],[&amp;quot;-2^-36&amp;quot;,2.91038e-11,7.71752e+08],[&amp;quot;-2^-37&amp;quot;,1.45519e-11,7.63363e+08],[&amp;quot;-2^-38&amp;quot;,7.27596e-12,7.54975e+08],[&amp;quot;-2^-39&amp;quot;,3.63798e-12,7.46586e+08],[&amp;quot;-2^-40&amp;quot;,1.81899e-12,7.38198e+08],[&amp;quot;-2^-41&amp;quot;,9.09495e-13,7.29809e+08],[&amp;quot;-2^-42&amp;quot;,4.54747e-13,7.2142e+08],[&amp;quot;-2^-43&amp;quot;,2.27374e-13,7.13032e+08],[&amp;quot;-2^-44&amp;quot;,1.13687e-13,7.04643e+08],[&amp;quot;-2^-45&amp;quot;,5.68434e-14,6.96254e+08],[&amp;quot;-2^-46&amp;quot;,2.84217e-14,6.87866e+08],[&amp;quot;-2^-47&amp;quot;,1.42109e-14,6.79477e+08],[&amp;quot;-2^-48&amp;quot;,7.10543e-15,6.71089e+08],[&amp;quot;-2^-49&amp;quot;,3.55271e-15,6.627e+08],[&amp;quot;-2^-50&amp;quot;,1.77636e-15,6.54311e+08],[&amp;quot;-2^-51&amp;quot;,8.88178e-16,6.45923e+08],[&amp;quot;-2^-52&amp;quot;,4.44089e-16,6.37534e+08],[&amp;quot;-2^-53&amp;quot;,2.22045e-16,6.29146e+08],[&amp;quot;-2^-54&amp;quot;,1.11022e-16,6.20757e+08],[&amp;quot;-2^-55&amp;quot;,5.55111e-17,6.12368e+08],[&amp;quot;-2^-56&amp;quot;,2.77556e-17,6.0398e+08],[&amp;quot;-2^-57&amp;quot;,1.38778e-17,5.95591e+08],[&amp;quot;-2^-58&amp;quot;,6.93889e-18,5.87203e+08],[&amp;quot;-2^-59&amp;quot;,3.46945e-18,5.78814e+08],[&amp;quot;-2^-60&amp;quot;,1.73472e-18,5.70425e+08],[&amp;quot;-2^-61&amp;quot;,8.67362e-19,5.62037e+08],[&amp;quot;-2^-62&amp;quot;,4.33681e-19,5.53648e+08],[&amp;quot;-2^-63&amp;quot;,2.1684e-19,5.4526e+08],[&amp;quot;-2^-64&amp;quot;,1.0842e-19,5.36871e+08],[&amp;quot;-2^-65&amp;quot;,5.42101e-20,5.28482e+08],[&amp;quot;-2^-66&amp;quot;,2.71051e-20,5.20094e+08],[&amp;quot;-2^-67&amp;quot;,1.35525e-20,5.11705e+08],[&amp;quot;-2^-68&amp;quot;,6.77626e-21,5.03316e+08],[&amp;quot;-2^-69&amp;quot;,3.38813e-21,4.94928e+08],[&amp;quot;-2^-70&amp;quot;,1.69407e-21,4.86539e+08],[&amp;quot;-2^-71&amp;quot;,8.47033e-22,4.78151e+08],[&amp;quot;-2^-72&amp;quot;,4.23516e-22,4.69762e+08],[&amp;quot;-2^-73&amp;quot;,2.11758e-22,4.61373e+08],[&amp;quot;-2^-74&amp;quot;,1.05879e-22,4.52985e+08],[&amp;quot;-2^-75&amp;quot;,5.29396e-23,4.44596e+08],[&amp;quot;-2^-76&amp;quot;,2.64698e-23,4.36208e+08],[&amp;quot;-2^-77&amp;quot;,1.32349e-23,4.27819e+08],[&amp;quot;-2^-78&amp;quot;,6.61744e-24,4.1943e+08],[&amp;quot;-2^-79&amp;quot;,3.30872e-24,4.11042e+08],[&amp;quot;-2^-80&amp;quot;,1.65436e-24,4.02653e+08],[&amp;quot;-2^-81&amp;quot;,8.27181e-25,3.94265e+08],[&amp;quot;-2^-82&amp;quot;,4.1359e-25,3.85876e+08],[&amp;quot;-2^-83&amp;quot;,2.06795e-25,3.77487e+08],[&amp;quot;-2^-84&amp;quot;,1.03398e-25,3.69099e+08],[&amp;quot;-2^-85&amp;quot;,5.16988e-26,3.6071e+08],[&amp;quot;-2^-86&amp;quot;,2.58494e-26,3.52322e+08],[&amp;quot;-2^-87&amp;quot;,1.29247e-26,3.43933e+08],[&amp;quot;-2^-88&amp;quot;,6.46235e-27,3.35544e+08],[&amp;quot;-2^-89&amp;quot;,3.23117e-27,3.27156e+08],[&amp;quot;-2^-90&amp;quot;,1.61559e-27,3.18767e+08],[&amp;quot;-2^-91&amp;quot;,8.07794e-28,3.10378e+08],[&amp;quot;-2^-92&amp;quot;,4.03897e-28,3.0199e+08],[&amp;quot;-2^-93&amp;quot;,2.01948e-28,2.93601e+08],[&amp;quot;-2^-94&amp;quot;,1.00974e-28,2.85213e+08],[&amp;quot;-2^-95&amp;quot;,5.04871e-29,2.76824e+08],[&amp;quot;-2^-96&amp;quot;,2.52435e-29,2.68435e+08],[&amp;quot;-2^-97&amp;quot;,1.26218e-29,2.60047e+08],[&amp;quot;-2^-98&amp;quot;,6.31089e-30,2.51658e+08],[&amp;quot;-2^-99&amp;quot;,3.15544e-30,2.4327e+08],[&amp;quot;-2^-100&amp;quot;,1.57772e-30,2.34881e+08],[&amp;quot;-2^-101&amp;quot;,7.88861e-31,2.26492e+08],[&amp;quot;-2^-102&amp;quot;,3.9443e-31,2.18104e+08],[&amp;quot;-2^-103&amp;quot;,1.97215e-31,2.09715e+08],[&amp;quot;-2^-104&amp;quot;,9.86076e-32,2.01327e+08],[&amp;quot;-2^-105&amp;quot;,4.93038e-32,1.92938e+08],[&amp;quot;-2^-106&amp;quot;,2.46519e-32,1.84549e+08],[&amp;quot;-2^-107&amp;quot;,1.2326e-32,1.76161e+08],[&amp;quot;-2^-108&amp;quot;,6.16298e-33,1.67772e+08],[&amp;quot;-2^-109&amp;quot;,3.08149e-33,1.59384e+08],[&amp;quot;-2^-110&amp;quot;,1.54074e-33,1.50995e+08],[&amp;quot;-2^-111&amp;quot;,7.70372e-34,1.42606e+08],[&amp;quot;-2^-112&amp;quot;,3.85186e-34,1.34218e+08],[&amp;quot;-2^-113&amp;quot;,1.92593e-34,1.25829e+08],[&amp;quot;-2^-114&amp;quot;,9.62965e-35,1.17441e+08],[&amp;quot;-2^-115&amp;quot;,4.81482e-35,1.09052e+08],[&amp;quot;-2^-116&amp;quot;,2.40741e-35,1.00663e+08],[&amp;quot;-2^-117&amp;quot;,1.20371e-35,9.22747e+07],[&amp;quot;-2^-118&amp;quot;,6.01853e-36,8.38861e+07],[&amp;quot;-2^-119&amp;quot;,3.00927e-36,7.54975e+07],[&amp;quot;-2^-120&amp;quot;,1.50463e-36,6.71089e+07],[&amp;quot;-2^-121&amp;quot;,7.52316e-37,5.87203e+07],[&amp;quot;-2^-122&amp;quot;,3.76158e-37,5.03316e+07],[&amp;quot;-2^-123&amp;quot;,1.88079e-37,4.1943e+07],[&amp;quot;-2^-124&amp;quot;,9.40395e-38,3.35544e+07],[&amp;quot;-2^-125&amp;quot;,4.70198e-38,2.51658e+07],[&amp;quot;-2^-126&amp;quot;,2.35099e-38,1.67772e+07],[&amp;quot;-SN&amp;quot;,1.17546e-38,8.38835e+06],[&amp;quot;0&amp;quot;,0,1.17549e-38],[&amp;quot;+SN&amp;quot;,1.17546e-38,8.38835e+06],[&amp;quot;+2^-126&amp;quot;,2.35095e-38,1.6777e+07],[&amp;quot;+2^-125&amp;quot;,4.70191e-38,2.51656e+07],[&amp;quot;+2^-124&amp;quot;,9.40381e-38,3.35542e+07],[&amp;quot;+2^-123&amp;quot;,1.88076e-37,4.19428e+07],[&amp;quot;+2^-122&amp;quot;,3.76152e-37,5.03314e+07],[&amp;quot;+2^-121&amp;quot;,7.52305e-37,5.872e+07],[&amp;quot;+2^-120&amp;quot;,1.50461e-36,6.71086e+07],[&amp;quot;+2^-119&amp;quot;,3.00922e-36,7.54972e+07],[&amp;quot;+2^-118&amp;quot;,6.01844e-36,8.38858e+07],[&amp;quot;+2^-117&amp;quot;,1.20369e-35,9.22744e+07],[&amp;quot;+2^-116&amp;quot;,2.40738e-35,1.00663e+08],[&amp;quot;+2^-115&amp;quot;,4.81475e-35,1.09052e+08],[&amp;quot;+2^-114&amp;quot;,9.6295e-35,1.1744e+08],[&amp;quot;+2^-113&amp;quot;,1.9259e-34,1.25829e+08],[&amp;quot;+2^-112&amp;quot;,3.8518e-34,1.34217e+08],[&amp;quot;+2^-111&amp;quot;,7.7036e-34,1.42606e+08],[&amp;quot;+2^-110&amp;quot;,1.54072e-33,1.50995e+08],[&amp;quot;+2^-109&amp;quot;,3.08144e-33,1.59383e+08],[&amp;quot;+2^-108&amp;quot;,6.16288e-33,1.67772e+08],[&amp;quot;+2^-107&amp;quot;,1.23258e-32,1.76161e+08],[&amp;quot;+2^-106&amp;quot;,2.46515e-32,1.84549e+08],[&amp;quot;+2^-105&amp;quot;,4.93031e-32,1.92938e+08],[&amp;quot;+2^-104&amp;quot;,9.86061e-32,2.01326e+08],[&amp;quot;+2^-103&amp;quot;,1.97212e-31,2.09715e+08],[&amp;quot;+2^-102&amp;quot;,3.94424e-31,2.18104e+08],[&amp;quot;+2^-101&amp;quot;,7.88849e-31,2.26492e+08],[&amp;quot;+2^-100&amp;quot;,1.5777e-30,2.34881e+08],[&amp;quot;+2^-99&amp;quot;,3.1554e-30,2.43269e+08],[&amp;quot;+2^-98&amp;quot;,6.31079e-30,2.51658e+08],[&amp;quot;+2^-97&amp;quot;,1.26216e-29,2.60047e+08],[&amp;quot;+2^-96&amp;quot;,2.52432e-29,2.68435e+08],[&amp;quot;+2^-95&amp;quot;,5.04863e-29,2.76824e+08],[&amp;quot;+2^-94&amp;quot;,1.00973e-28,2.85212e+08],[&amp;quot;+2^-93&amp;quot;,2.01945e-28,2.93601e+08],[&amp;quot;+2^-92&amp;quot;,4.03891e-28,3.0199e+08],[&amp;quot;+2^-91&amp;quot;,8.07781e-28,3.10378e+08],[&amp;quot;+2^-90&amp;quot;,1.61556e-27,3.18767e+08],[&amp;quot;+2^-89&amp;quot;,3.23112e-27,3.27155e+08],[&amp;quot;+2^-88&amp;quot;,6.46225e-27,3.35544e+08],[&amp;quot;+2^-87&amp;quot;,1.29245e-26,3.43933e+08],[&amp;quot;+2^-86&amp;quot;,2.5849e-26,3.52321e+08],[&amp;quot;+2^-85&amp;quot;,5.1698e-26,3.6071e+08],[&amp;quot;+2^-84&amp;quot;,1.03396e-25,3.69098e+08],[&amp;quot;+2^-83&amp;quot;,2.06792e-25,3.77487e+08],[&amp;quot;+2^-82&amp;quot;,4.13584e-25,3.85876e+08],[&amp;quot;+2^-81&amp;quot;,8.27168e-25,3.94264e+08],[&amp;quot;+2^-80&amp;quot;,1.65434e-24,4.02653e+08],[&amp;quot;+2^-79&amp;quot;,3.30867e-24,4.11042e+08],[&amp;quot;+2^-78&amp;quot;,6.61734e-24,4.1943e+08],[&amp;quot;+2^-77&amp;quot;,1.32347e-23,4.27819e+08],[&amp;quot;+2^-76&amp;quot;,2.64694e-23,4.36207e+08],[&amp;quot;+2^-75&amp;quot;,5.29388e-23,4.44596e+08],[&amp;quot;+2^-74&amp;quot;,1.05878e-22,4.52985e+08],[&amp;quot;+2^-73&amp;quot;,2.11755e-22,4.61373e+08],[&amp;quot;+2^-72&amp;quot;,4.2351e-22,4.69762e+08],[&amp;quot;+2^-71&amp;quot;,8.4702e-22,4.7815e+08],[&amp;quot;+2^-70&amp;quot;,1.69404e-21,4.86539e+08],[&amp;quot;+2^-69&amp;quot;,3.38808e-21,4.94928e+08],[&amp;quot;+2^-68&amp;quot;,6.77616e-21,5.03316e+08],[&amp;quot;+2^-67&amp;quot;,1.35523e-20,5.11705e+08],[&amp;quot;+2^-66&amp;quot;,2.71046e-20,5.20093e+08],[&amp;quot;+2^-65&amp;quot;,5.42093e-20,5.28482e+08],[&amp;quot;+2^-64&amp;quot;,1.08419e-19,5.36871e+08],[&amp;quot;+2^-63&amp;quot;,2.16837e-19,5.45259e+08],[&amp;quot;+2^-62&amp;quot;,4.33674e-19,5.53648e+08],[&amp;quot;+2^-61&amp;quot;,8.67349e-19,5.62036e+08],[&amp;quot;+2^-60&amp;quot;,1.7347e-18,5.70425e+08],[&amp;quot;+2^-59&amp;quot;,3.46939e-18,5.78814e+08],[&amp;quot;+2^-58&amp;quot;,6.93879e-18,5.87202e+08],[&amp;quot;+2^-57&amp;quot;,1.38776e-17,5.95591e+08],[&amp;quot;+2^-56&amp;quot;,2.77552e-17,6.0398e+08],[&amp;quot;+2^-55&amp;quot;,5.55103e-17,6.12368e+08],[&amp;quot;+2^-54&amp;quot;,1.11021e-16,6.20757e+08],[&amp;quot;+2^-53&amp;quot;,2.22041e-16,6.29145e+08],[&amp;quot;+2^-52&amp;quot;,4.44082e-16,6.37534e+08],[&amp;quot;+2^-51&amp;quot;,8.88165e-16,6.45923e+08],[&amp;quot;+2^-50&amp;quot;,1.77633e-15,6.54311e+08],[&amp;quot;+2^-49&amp;quot;,3.55266e-15,6.627e+08],[&amp;quot;+2^-48&amp;quot;,7.10532e-15,6.71088e+08],[&amp;quot;+2^-47&amp;quot;,1.42106e-14,6.79477e+08],[&amp;quot;+2^-46&amp;quot;,2.84213e-14,6.87866e+08],[&amp;quot;+2^-45&amp;quot;,5.68426e-14,6.96254e+08],[&amp;quot;+2^-44&amp;quot;,1.13685e-13,7.04643e+08],[&amp;quot;+2^-43&amp;quot;,2.2737e-13,7.13031e+08],[&amp;quot;+2^-42&amp;quot;,4.5474e-13,7.2142e+08],[&amp;quot;+2^-41&amp;quot;,9.09481e-13,7.29809e+08],[&amp;quot;+2^-40&amp;quot;,1.81896e-12,7.38197e+08],[&amp;quot;+2^-39&amp;quot;,3.63792e-12,7.46586e+08],[&amp;quot;+2^-38&amp;quot;,7.27585e-12,7.54974e+08],[&amp;quot;+2^-37&amp;quot;,1.45517e-11,7.63363e+08],[&amp;quot;+2^-36&amp;quot;,2.91034e-11,7.71752e+08],[&amp;quot;+2^-35&amp;quot;,5.82068e-11,7.8014e+08],[&amp;quot;+2^-34&amp;quot;,1.16414e-10,7.88529e+08],[&amp;quot;+2^-33&amp;quot;,2.32827e-10,7.96918e+08],[&amp;quot;+2^-32&amp;quot;,4.65654e-10,8.05306e+08],[&amp;quot;+2^-31&amp;quot;,9.31308e-10,8.13695e+08],[&amp;quot;+2^-30&amp;quot;,1.86262e-09,8.22083e+08],[&amp;quot;+2^-29&amp;quot;,3.72523e-09,8.30472e+08],[&amp;quot;+2^-28&amp;quot;,7.45047e-09,8.38861e+08],[&amp;quot;+2^-27&amp;quot;,1.49009e-08,8.47249e+08],[&amp;quot;+2^-26&amp;quot;,2.98019e-08,8.55638e+08],[&amp;quot;+2^-25&amp;quot;,2.98023e-08,8.38861e+06],[&amp;quot;+2^-24&amp;quot;,2.98023e-08,4.1943e+06],[&amp;quot;+2^-23&amp;quot;,2.98023e-08,2.09715e+06],[&amp;quot;+2^-22&amp;quot;,2.98023e-08,1.04857e+06],[&amp;quot;+2^-21&amp;quot;,2.98021e-08,524284],[&amp;quot;+2^-20&amp;quot;,2.98014e-08,262136],[&amp;quot;+2^-19&amp;quot;,2.97987e-08,131056],[&amp;quot;+2^-18&amp;quot;,2.97878e-08,65504],[&amp;quot;+2^-17&amp;quot;,2.98014e-08,32767],[&amp;quot;+2^-16&amp;quot;,2.98005e-08,16384],[&amp;quot;+2^-15&amp;quot;,2.98023e-08,8192],[&amp;quot;+2^-14&amp;quot;,2.98025e-08,4351.99],[&amp;quot;+2^-13&amp;quot;,2.98044e-08,2815.96],[&amp;quot;+2^-12&amp;quot;,2.98091e-08,1024.23],[&amp;quot;+2^-11&amp;quot;,2.9812e-08,512.167],[&amp;quot;+2^-10&amp;quot;,2.98154e-08,256.113],[&amp;quot;+2^-9&amp;quot;,2.98591e-08,128.244],[&amp;quot;+2^-8&amp;quot;,3.00204e-08,85.3328],[&amp;quot;+2^-7&amp;quot;,3.01947e-08,43.675],[&amp;quot;+2^-6&amp;quot;,3.05713e-08,30.45],[&amp;quot;+2^-5&amp;quot;,3.14319e-08,8.43743],[&amp;quot;+2^-4&amp;quot;,3.31291e-08,7.77058],[&amp;quot;+2^-3&amp;quot;,3.58597e-08,3.99659],[&amp;quot;+2^-2&amp;quot;,4.14639e-08,2.29246],[&amp;quot;+2^-1&amp;quot;,8.02409e-08,1.34622],[&amp;quot;+2^0&amp;quot;,8.66201e-08,1.45324],[&amp;quot;+2^1&amp;quot;,8.8654e-08,1.48737],[&amp;quot;+2^2&amp;quot;,6.7976e-08,1.14045],[&amp;quot;+2^3&amp;quot;,1.33955e-07,2.2474],[&amp;quot;+2^4&amp;quot;,2.53131e-14,4.24683e-07],[&amp;quot;+2^5&amp;quot;,0,null],[&amp;quot;+2^6&amp;quot;,0,null],[&amp;quot;+2^7&amp;quot;,0,null],[&amp;quot;+2^8&amp;quot;,0,null],[&amp;quot;+2^9&amp;quot;,0,null],[&amp;quot;+2^10&amp;quot;,0,null],[&amp;quot;+2^11&amp;quot;,0,null],[&amp;quot;+2^12&amp;quot;,0,null],[&amp;quot;+2^13&amp;quot;,0,null],[&amp;quot;+2^14&amp;quot;,0,null],[&amp;quot;+2^15&amp;quot;,0,null],[&amp;quot;+2^16&amp;quot;,0,null],[&amp;quot;+2^17&amp;quot;,0,null],[&amp;quot;+2^18&amp;quot;,0,null],[&amp;quot;+2^19&amp;quot;,0,null],[&amp;quot;+2^20&amp;quot;,0,null],[&amp;quot;+2^21&amp;quot;,0,null],[&amp;quot;+2^22&amp;quot;,0,null],[&amp;quot;+2^23&amp;quot;,0,null],[&amp;quot;+2^24&amp;quot;,0,null],[&amp;quot;+2^25&amp;quot;,0,null],[&amp;quot;+2^26&amp;quot;,0,null],[&amp;quot;+2^27&amp;quot;,0,null],[&amp;quot;+2^28&amp;quot;,0,null],[&amp;quot;+2^29&amp;quot;,0,null],[&amp;quot;+2^30&amp;quot;,0,null],[&amp;quot;+2^31&amp;quot;,0,null],[&amp;quot;+2^32&amp;quot;,0,null],[&amp;quot;+2^33&amp;quot;,0,null],[&amp;quot;+2^34&amp;quot;,0,null],[&amp;quot;+2^35&amp;quot;,0,null],[&amp;quot;+2^36&amp;quot;,0,null],[&amp;quot;+2^37&amp;quot;,0,null],[&amp;quot;+2^38&amp;quot;,0,null],[&amp;quot;+2^39&amp;quot;,0,null],[&amp;quot;+2^40&amp;quot;,0,null],[&amp;quot;+2^41&amp;quot;,0,null],[&amp;quot;+2^42&amp;quot;,0,null],[&amp;quot;+2^43&amp;quot;,0,null],[&amp;quot;+2^44&amp;quot;,0,null],[&amp;quot;+2^45&amp;quot;,0,null],[&amp;quot;+2^46&amp;quot;,0,null],[&amp;quot;+2^47&amp;quot;,0,null],[&amp;quot;+2^48&amp;quot;,0,null],[&amp;quot;+2^49&amp;quot;,0,null],[&amp;quot;+2^50&amp;quot;,0,null],[&amp;quot;+2^51&amp;quot;,0,null],[&amp;quot;+2^52&amp;quot;,0,null],[&amp;quot;+2^53&amp;quot;,0,null],[&amp;quot;+2^54&amp;quot;,0,null],[&amp;quot;+2^55&amp;quot;,0,null],[&amp;quot;+2^56&amp;quot;,0,null],[&amp;quot;+2^57&amp;quot;,0,null],[&amp;quot;+2^58&amp;quot;,0,null],[&amp;quot;+2^59&amp;quot;,0,null],[&amp;quot;+2^60&amp;quot;,0,null],[&amp;quot;+2^61&amp;quot;,0,null],[&amp;quot;+2^62&amp;quot;,0,null],[&amp;quot;+2^63&amp;quot;,0,null],[&amp;quot;+2^64&amp;quot;,0,null],[&amp;quot;+2^65&amp;quot;,0,null],[&amp;quot;+2^66&amp;quot;,0,null],[&amp;quot;+2^67&amp;quot;,0,null],[&amp;quot;+2^68&amp;quot;,0,null],[&amp;quot;+2^69&amp;quot;,0,null],[&amp;quot;+2^70&amp;quot;,0,null],[&amp;quot;+2^71&amp;quot;,0,null],[&amp;quot;+2^72&amp;quot;,0,null],[&amp;quot;+2^73&amp;quot;,0,null],[&amp;quot;+2^74&amp;quot;,0,null],[&amp;quot;+2^75&amp;quot;,0,null],[&amp;quot;+2^76&amp;quot;,0,null],[&amp;quot;+2^77&amp;quot;,0,null],[&amp;quot;+2^78&amp;quot;,0,null],[&amp;quot;+2^79&amp;quot;,0,null],[&amp;quot;+2^80&amp;quot;,0,null],[&amp;quot;+2^81&amp;quot;,0,null],[&amp;quot;+2^82&amp;quot;,0,null],[&amp;quot;+2^83&amp;quot;,0,null],[&amp;quot;+2^84&amp;quot;,0,null],[&amp;quot;+2^85&amp;quot;,0,null],[&amp;quot;+2^86&amp;quot;,0,null],[&amp;quot;+2^87&amp;quot;,0,null],[&amp;quot;+2^88&amp;quot;,0,null],[&amp;quot;+2^89&amp;quot;,0,null],[&amp;quot;+2^90&amp;quot;,0,null],[&amp;quot;+2^91&amp;quot;,0,null],[&amp;quot;+2^92&amp;quot;,0,null],[&amp;quot;+2^93&amp;quot;,0,null],[&amp;quot;+2^94&amp;quot;,0,null],[&amp;quot;+2^95&amp;quot;,0,null],[&amp;quot;+2^96&amp;quot;,0,null],[&amp;quot;+2^97&amp;quot;,0,null],[&amp;quot;+2^98&amp;quot;,0,null],[&amp;quot;+2^99&amp;quot;,0,null],[&amp;quot;+2^100&amp;quot;,0,null],[&amp;quot;+2^101&amp;quot;,0,null],[&amp;quot;+2^102&amp;quot;,0,null],[&amp;quot;+2^103&amp;quot;,0,null],[&amp;quot;+2^104&amp;quot;,0,null],[&amp;quot;+2^105&amp;quot;,0,null],[&amp;quot;+2^106&amp;quot;,0,null],[&amp;quot;+2^107&amp;quot;,0,null],[&amp;quot;+2^108&amp;quot;,0,null],[&amp;quot;+2^109&amp;quot;,0,null],[&amp;quot;+2^110&amp;quot;,0,null],[&amp;quot;+2^111&amp;quot;,0,null],[&amp;quot;+2^112&amp;quot;,0,null],[&amp;quot;+2^113&amp;quot;,0,null],[&amp;quot;+2^114&amp;quot;,0,null],[&amp;quot;+2^115&amp;quot;,0,null],[&amp;quot;+2^116&amp;quot;,0,null],[&amp;quot;+2^117&amp;quot;,0,null],[&amp;quot;+2^118&amp;quot;,0,null],[&amp;quot;+2^119&amp;quot;,0,null],[&amp;quot;+2^120&amp;quot;,0,null],[&amp;quot;+2^121&amp;quot;,0,null],[&amp;quot;+2^122&amp;quot;,0,null],[&amp;quot;+2^123&amp;quot;,0,null],[&amp;quot;+2^124&amp;quot;,0,null],[&amp;quot;+2^125&amp;quot;,0,null],[&amp;quot;+2^126&amp;quot;,0,null],[&amp;quot;+2^127&amp;quot;,0,null] ]&lt;br /&gt;
 }}&lt;br /&gt;
=== expm1 ===&lt;br /&gt;
&lt;br /&gt;
{{#widget:FloatErrorGraph|id=tanhf_expm1&lt;br /&gt;
  |data=[ [&amp;quot;-2^127&amp;quot;,0,1.17549e-38],[&amp;quot;-2^126&amp;quot;,0,1.17549e-38],[&amp;quot;-2^125&amp;quot;,0,1.17549e-38],[&amp;quot;-2^124&amp;quot;,0,1.17549e-38],[&amp;quot;-2^123&amp;quot;,0,1.17549e-38],[&amp;quot;-2^122&amp;quot;,0,1.17549e-38],[&amp;quot;-2^121&amp;quot;,0,1.17549e-38],[&amp;quot;-2^120&amp;quot;,0,1.17549e-38],[&amp;quot;-2^119&amp;quot;,0,1.17549e-38],[&amp;quot;-2^118&amp;quot;,0,1.17549e-38],[&amp;quot;-2^117&amp;quot;,0,1.17549e-38],[&amp;quot;-2^116&amp;quot;,0,1.17549e-38],[&amp;quot;-2^115&amp;quot;,0,1.17549e-38],[&amp;quot;-2^114&amp;quot;,0,1.17549e-38],[&amp;quot;-2^113&amp;quot;,0,1.17549e-38],[&amp;quot;-2^112&amp;quot;,0,1.17549e-38],[&amp;quot;-2^111&amp;quot;,0,1.17549e-38],[&amp;quot;-2^110&amp;quot;,0,1.17549e-38],[&amp;quot;-2^109&amp;quot;,0,1.17549e-38],[&amp;quot;-2^108&amp;quot;,0,1.17549e-38],[&amp;quot;-2^107&amp;quot;,0,1.17549e-38],[&amp;quot;-2^106&amp;quot;,0,1.17549e-38],[&amp;quot;-2^105&amp;quot;,0,1.17549e-38],[&amp;quot;-2^104&amp;quot;,0,1.17549e-38],[&amp;quot;-2^103&amp;quot;,0,1.17549e-38],[&amp;quot;-2^102&amp;quot;,0,1.17549e-38],[&amp;quot;-2^101&amp;quot;,0,1.17549e-38],[&amp;quot;-2^100&amp;quot;,0,1.17549e-38],[&amp;quot;-2^99&amp;quot;,0,1.17549e-38],[&amp;quot;-2^98&amp;quot;,0,1.17549e-38],[&amp;quot;-2^97&amp;quot;,0,1.17549e-38],[&amp;quot;-2^96&amp;quot;,0,1.17549e-38],[&amp;quot;-2^95&amp;quot;,0,1.17549e-38],[&amp;quot;-2^94&amp;quot;,0,1.17549e-38],[&amp;quot;-2^93&amp;quot;,0,1.17549e-38],[&amp;quot;-2^92&amp;quot;,0,1.17549e-38],[&amp;quot;-2^91&amp;quot;,0,1.17549e-38],[&amp;quot;-2^90&amp;quot;,0,1.17549e-38],[&amp;quot;-2^89&amp;quot;,0,1.17549e-38],[&amp;quot;-2^88&amp;quot;,0,1.17549e-38],[&amp;quot;-2^87&amp;quot;,0,1.17549e-38],[&amp;quot;-2^86&amp;quot;,0,1.17549e-38],[&amp;quot;-2^85&amp;quot;,0,1.17549e-38],[&amp;quot;-2^84&amp;quot;,0,1.17549e-38],[&amp;quot;-2^83&amp;quot;,0,1.17549e-38],[&amp;quot;-2^82&amp;quot;,0,1.17549e-38],[&amp;quot;-2^81&amp;quot;,0,1.17549e-38],[&amp;quot;-2^80&amp;quot;,0,1.17549e-38],[&amp;quot;-2^79&amp;quot;,0,1.17549e-38],[&amp;quot;-2^78&amp;quot;,0,1.17549e-38],[&amp;quot;-2^77&amp;quot;,0,1.17549e-38],[&amp;quot;-2^76&amp;quot;,0,1.17549e-38],[&amp;quot;-2^75&amp;quot;,0,1.17549e-38],[&amp;quot;-2^74&amp;quot;,0,1.17549e-38],[&amp;quot;-2^73&amp;quot;,0,1.17549e-38],[&amp;quot;-2^72&amp;quot;,0,1.17549e-38],[&amp;quot;-2^71&amp;quot;,0,1.17549e-38],[&amp;quot;-2^70&amp;quot;,0,1.17549e-38],[&amp;quot;-2^69&amp;quot;,0,1.17549e-38],[&amp;quot;-2^68&amp;quot;,0,1.17549e-38],[&amp;quot;-2^67&amp;quot;,0,1.17549e-38],[&amp;quot;-2^66&amp;quot;,0,1.17549e-38],[&amp;quot;-2^65&amp;quot;,0,1.17549e-38],[&amp;quot;-2^64&amp;quot;,0,1.17549e-38],[&amp;quot;-2^63&amp;quot;,0,1.17549e-38],[&amp;quot;-2^62&amp;quot;,0,1.17549e-38],[&amp;quot;-2^61&amp;quot;,0,1.17549e-38],[&amp;quot;-2^60&amp;quot;,0,1.17549e-38],[&amp;quot;-2^59&amp;quot;,0,1.17549e-38],[&amp;quot;-2^58&amp;quot;,0,1.17549e-38],[&amp;quot;-2^57&amp;quot;,0,1.17549e-38],[&amp;quot;-2^56&amp;quot;,0,1.17549e-38],[&amp;quot;-2^55&amp;quot;,0,1.17549e-38],[&amp;quot;-2^54&amp;quot;,0,1.17549e-38],[&amp;quot;-2^53&amp;quot;,0,1.17549e-38],[&amp;quot;-2^52&amp;quot;,0,1.17549e-38],[&amp;quot;-2^51&amp;quot;,0,1.17549e-38],[&amp;quot;-2^50&amp;quot;,0,1.17549e-38],[&amp;quot;-2^49&amp;quot;,0,1.17549e-38],[&amp;quot;-2^48&amp;quot;,0,1.17549e-38],[&amp;quot;-2^47&amp;quot;,0,1.17549e-38],[&amp;quot;-2^46&amp;quot;,0,1.17549e-38],[&amp;quot;-2^45&amp;quot;,0,1.17549e-38],[&amp;quot;-2^44&amp;quot;,0,1.17549e-38],[&amp;quot;-2^43&amp;quot;,0,1.17549e-38],[&amp;quot;-2^42&amp;quot;,0,1.17549e-38],[&amp;quot;-2^41&amp;quot;,0,1.17549e-38],[&amp;quot;-2^40&amp;quot;,0,1.17549e-38],[&amp;quot;-2^39&amp;quot;,0,1.17549e-38],[&amp;quot;-2^38&amp;quot;,0,1.17549e-38],[&amp;quot;-2^37&amp;quot;,0,1.17549e-38],[&amp;quot;-2^36&amp;quot;,0,1.17549e-38],[&amp;quot;-2^35&amp;quot;,0,1.17549e-38],[&amp;quot;-2^34&amp;quot;,0,1.17549e-38],[&amp;quot;-2^33&amp;quot;,0,1.17549e-38],[&amp;quot;-2^32&amp;quot;,0,1.17549e-38],[&amp;quot;-2^31&amp;quot;,0,1.17549e-38],[&amp;quot;-2^30&amp;quot;,0,1.17549e-38],[&amp;quot;-2^29&amp;quot;,0,1.17549e-38],[&amp;quot;-2^28&amp;quot;,0,1.17549e-38],[&amp;quot;-2^27&amp;quot;,0,1.17549e-38],[&amp;quot;-2^26&amp;quot;,0,1.17549e-38],[&amp;quot;-2^25&amp;quot;,0,1.17549e-38],[&amp;quot;-2^24&amp;quot;,0,1.17549e-38],[&amp;quot;-2^23&amp;quot;,0,1.17549e-38],[&amp;quot;-2^22&amp;quot;,0,1.17549e-38],[&amp;quot;-2^21&amp;quot;,0,1.17549e-38],[&amp;quot;-2^20&amp;quot;,0,1.17549e-38],[&amp;quot;-2^19&amp;quot;,0,1.17549e-38],[&amp;quot;-2^18&amp;quot;,0,1.17549e-38],[&amp;quot;-2^17&amp;quot;,0,1.17549e-38],[&amp;quot;-2^16&amp;quot;,0,1.17549e-38],[&amp;quot;-2^15&amp;quot;,0,1.17549e-38],[&amp;quot;-2^14&amp;quot;,0,1.17549e-38],[&amp;quot;-2^13&amp;quot;,0,1.17549e-38],[&amp;quot;-2^12&amp;quot;,0,1.17549e-38],[&amp;quot;-2^11&amp;quot;,0,1.17549e-38],[&amp;quot;-2^10&amp;quot;,0,1.17549e-38],[&amp;quot;-2^9&amp;quot;,0,1.17549e-38],[&amp;quot;-2^8&amp;quot;,0,1.17549e-38],[&amp;quot;-2^7&amp;quot;,0,1.17549e-38],[&amp;quot;-2^6&amp;quot;,0,1.17549e-38],[&amp;quot;-2^5&amp;quot;,0,1.17549e-38],[&amp;quot;-2^4&amp;quot;,2.53131e-14,4.24683e-07],[&amp;quot;-2^3&amp;quot;,1.19138e-07,1.9988],[&amp;quot;-2^2&amp;quot;,1.46791e-07,2.46274],[&amp;quot;-2^1&amp;quot;,1.47723e-07,2.47839],[&amp;quot;-2^0&amp;quot;,1.40758e-07,2.36153],[&amp;quot;-2^-1&amp;quot;,1.13422e-07,2.23445],[&amp;quot;-2^-2&amp;quot;,6.38012e-08,2.14081],[&amp;quot;-2^-3&amp;quot;,2.69538e-08,1.80884],[&amp;quot;-2^-4&amp;quot;,1.21308e-08,1.62817],[&amp;quot;-2^-5&amp;quot;,5.59535e-09,1.50199],[&amp;quot;-2^-6&amp;quot;,2.69177e-09,1.44513],[&amp;quot;-2^-7&amp;quot;,1.31289e-09,1.4097],[&amp;quot;-2^-8&amp;quot;,6.49688e-10,1.3952],[&amp;quot;-2^-9&amp;quot;,3.32043e-10,1.42611],[&amp;quot;-2^-10&amp;quot;,1.66275e-10,1.4283],[&amp;quot;-2^-11&amp;quot;,8.08595e-11,1.38916],[&amp;quot;-2^-12&amp;quot;,3.8939e-11,1.33794],[&amp;quot;-2^-13&amp;quot;,1.9401e-11,1.33323],[&amp;quot;-2^-14&amp;quot;,7.88212e-12,1.08331],[&amp;quot;-2^-15&amp;quot;,3.71372e-12,1.02082],[&amp;quot;-2^-16&amp;quot;,1.82845e-12,1.0052],[&amp;quot;-2^-17&amp;quot;,9.10676e-13,1.0013],[&amp;quot;-2^-18&amp;quot;,4.54895e-13,1.00032],[&amp;quot;-2^-19&amp;quot;,2.27392e-13,1.00008],[&amp;quot;-2^-20&amp;quot;,1.13689e-13,1.00002],[&amp;quot;-2^-21&amp;quot;,5.68437e-14,1],[&amp;quot;-2^-22&amp;quot;,2.84217e-14,1],[&amp;quot;-2^-23&amp;quot;,1.42109e-14,1],[&amp;quot;-2^-24&amp;quot;,7.10543e-15,1],[&amp;quot;-2^-25&amp;quot;,3.55271e-15,1],[&amp;quot;-2^-26&amp;quot;,9.92617e-24,5.58794e-09],[&amp;quot;-2^-27&amp;quot;,1.65436e-24,1.86265e-09],[&amp;quot;-2^-28&amp;quot;,8.27181e-25,1.86265e-09],[&amp;quot;-2^-29&amp;quot;,4.1359e-25,1.86265e-09],[&amp;quot;-2^-30&amp;quot;,2.06795e-25,1.86265e-09],[&amp;quot;-2^-31&amp;quot;,1.03398e-25,1.86265e-09],[&amp;quot;-2^-32&amp;quot;,5.16988e-26,1.86265e-09],[&amp;quot;-2^-33&amp;quot;,2.58494e-26,1.86265e-09],[&amp;quot;-2^-34&amp;quot;,1.29247e-26,1.86265e-09],[&amp;quot;-2^-35&amp;quot;,6.46235e-27,1.86265e-09],[&amp;quot;-2^-36&amp;quot;,3.23117e-27,1.86265e-09],[&amp;quot;-2^-37&amp;quot;,1.61559e-27,1.86265e-09],[&amp;quot;-2^-38&amp;quot;,8.07794e-28,1.86265e-09],[&amp;quot;-2^-39&amp;quot;,4.03897e-28,1.86265e-09],[&amp;quot;-2^-40&amp;quot;,2.01948e-28,1.86265e-09],[&amp;quot;-2^-41&amp;quot;,1.00974e-28,1.86265e-09],[&amp;quot;-2^-42&amp;quot;,5.04871e-29,1.86265e-09],[&amp;quot;-2^-43&amp;quot;,2.52435e-29,1.86265e-09],[&amp;quot;-2^-44&amp;quot;,1.26218e-29,1.86265e-09],[&amp;quot;-2^-45&amp;quot;,6.31089e-30,1.86265e-09],[&amp;quot;-2^-46&amp;quot;,3.15544e-30,1.86265e-09],[&amp;quot;-2^-47&amp;quot;,1.57772e-30,1.86265e-09],[&amp;quot;-2^-48&amp;quot;,7.88861e-31,1.86265e-09],[&amp;quot;-2^-49&amp;quot;,3.9443e-31,1.86265e-09],[&amp;quot;-2^-50&amp;quot;,1.97215e-31,1.86265e-09],[&amp;quot;-2^-51&amp;quot;,9.86076e-32,1.86265e-09],[&amp;quot;-2^-52&amp;quot;,4.93038e-32,1.86265e-09],[&amp;quot;-2^-53&amp;quot;,2.46519e-32,1.86265e-09],[&amp;quot;-2^-54&amp;quot;,1.2326e-32,1.86265e-09],[&amp;quot;-2^-55&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-56&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-57&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-58&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-59&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-60&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-61&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-62&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-63&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-64&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-65&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-66&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-67&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-68&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-69&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-70&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-71&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-72&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-73&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-74&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-75&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-76&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-77&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-78&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-79&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-80&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-81&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-82&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-83&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-84&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-85&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-86&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-87&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-88&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-89&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-90&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-91&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-92&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-93&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-94&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-95&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-96&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-97&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-98&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-99&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-100&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-101&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-102&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-103&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-104&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-105&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-106&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-107&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-108&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-109&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-110&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-111&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-112&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-113&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-114&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-115&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-116&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-117&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-118&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-119&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-120&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-121&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-122&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-123&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-124&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-125&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-126&amp;quot;,0,1.17549e-38],[&amp;quot;-SN&amp;quot;,0,1.17549e-38],[&amp;quot;0&amp;quot;,0,1.17549e-38],[&amp;quot;+SN&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-126&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-125&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-124&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-123&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-122&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-121&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-120&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-119&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-118&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-117&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-116&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-115&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-114&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-113&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-112&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-111&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-110&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-109&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-108&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-107&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-106&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-105&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-104&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-103&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-102&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-101&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-100&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-99&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-98&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-97&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-96&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-95&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-94&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-93&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-92&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-91&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-90&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-89&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-88&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-87&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-86&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-85&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-84&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-83&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-82&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-81&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-80&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-79&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-78&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-77&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-76&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-75&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-74&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-73&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-72&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-71&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-70&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-69&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-68&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-67&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-66&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-65&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-64&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-63&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-62&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-61&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-60&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-59&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-58&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-57&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-56&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-55&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-54&amp;quot;,1.2326e-32,1.86265e-09],[&amp;quot;+2^-53&amp;quot;,2.46519e-32,1.86265e-09],[&amp;quot;+2^-52&amp;quot;,4.93038e-32,1.86265e-09],[&amp;quot;+2^-51&amp;quot;,9.86076e-32,1.86265e-09],[&amp;quot;+2^-50&amp;quot;,1.97215e-31,1.86265e-09],[&amp;quot;+2^-49&amp;quot;,3.9443e-31,1.86265e-09],[&amp;quot;+2^-48&amp;quot;,7.88861e-31,1.86265e-09],[&amp;quot;+2^-47&amp;quot;,1.57772e-30,1.86265e-09],[&amp;quot;+2^-46&amp;quot;,3.15544e-30,1.86265e-09],[&amp;quot;+2^-45&amp;quot;,6.31089e-30,1.86265e-09],[&amp;quot;+2^-44&amp;quot;,1.26218e-29,1.86265e-09],[&amp;quot;+2^-43&amp;quot;,2.52435e-29,1.86265e-09],[&amp;quot;+2^-42&amp;quot;,5.04871e-29,1.86265e-09],[&amp;quot;+2^-41&amp;quot;,1.00974e-28,1.86265e-09],[&amp;quot;+2^-40&amp;quot;,2.01948e-28,1.86265e-09],[&amp;quot;+2^-39&amp;quot;,4.03897e-28,1.86265e-09],[&amp;quot;+2^-38&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-37&amp;quot;,1.61559e-27,1.86265e-09],[&amp;quot;+2^-36&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-35&amp;quot;,6.46235e-27,1.86265e-09],[&amp;quot;+2^-34&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-33&amp;quot;,2.58494e-26,1.86265e-09],[&amp;quot;+2^-32&amp;quot;,5.16988e-26,1.86265e-09],[&amp;quot;+2^-31&amp;quot;,1.03398e-25,1.86265e-09],[&amp;quot;+2^-30&amp;quot;,2.06795e-25,1.86265e-09],[&amp;quot;+2^-29&amp;quot;,4.1359e-25,1.86265e-09],[&amp;quot;+2^-28&amp;quot;,8.27181e-25,1.86265e-09],[&amp;quot;+2^-27&amp;quot;,1.65436e-24,1.86265e-09],[&amp;quot;+2^-26&amp;quot;,9.92617e-24,5.58794e-09],[&amp;quot;+2^-25&amp;quot;,3.55271e-15,1],[&amp;quot;+2^-24&amp;quot;,7.10543e-15,1],[&amp;quot;+2^-23&amp;quot;,1.42109e-14,1],[&amp;quot;+2^-22&amp;quot;,2.84217e-14,1],[&amp;quot;+2^-21&amp;quot;,5.68437e-14,1],[&amp;quot;+2^-20&amp;quot;,1.13689e-13,1.00002],[&amp;quot;+2^-19&amp;quot;,2.27391e-13,1.00008],[&amp;quot;+2^-18&amp;quot;,4.54892e-13,1.00032],[&amp;quot;+2^-17&amp;quot;,9.10665e-13,1.00129],[&amp;quot;+2^-16&amp;quot;,1.82841e-12,1.00518],[&amp;quot;+2^-15&amp;quot;,3.71353e-12,1.02077],[&amp;quot;+2^-14&amp;quot;,1.39798e-11,1.92136],[&amp;quot;+2^-13&amp;quot;,2.63239e-11,1.80896],[&amp;quot;+2^-12&amp;quot;,5.37712e-11,1.84757],[&amp;quot;+2^-11&amp;quot;,1.09932e-10,1.88861],[&amp;quot;+2^-10&amp;quot;,2.24787e-10,1.93091],[&amp;quot;+2^-9&amp;quot;,4.3452e-10,1.86625],[&amp;quot;+2^-8&amp;quot;,1.01783e-09,2.18576],[&amp;quot;+2^-7&amp;quot;,2.10562e-09,2.26089],[&amp;quot;+2^-6&amp;quot;,4.03954e-09,2.16871],[&amp;quot;+2^-5&amp;quot;,8.15746e-09,2.18975],[&amp;quot;+2^-4&amp;quot;,1.5545e-08,2.08641],[&amp;quot;+2^-3&amp;quot;,2.62153e-08,2.05864],[&amp;quot;+2^-2&amp;quot;,4.09295e-08,1.73804],[&amp;quot;+2^-1&amp;quot;,8.02409e-08,1.34622],[&amp;quot;+2^0&amp;quot;,8.66201e-08,1.45324],[&amp;quot;+2^1&amp;quot;,8.8654e-08,1.48737],[&amp;quot;+2^2&amp;quot;,6.7976e-08,1.14045],[&amp;quot;+2^3&amp;quot;,7.44117e-08,1.24842],[&amp;quot;+2^4&amp;quot;,2.53131e-14,4.24683e-07],[&amp;quot;+2^5&amp;quot;,0,null],[&amp;quot;+2^6&amp;quot;,0,null],[&amp;quot;+2^7&amp;quot;,0,null],[&amp;quot;+2^8&amp;quot;,0,null],[&amp;quot;+2^9&amp;quot;,0,null],[&amp;quot;+2^10&amp;quot;,0,null],[&amp;quot;+2^11&amp;quot;,0,null],[&amp;quot;+2^12&amp;quot;,0,null],[&amp;quot;+2^13&amp;quot;,0,null],[&amp;quot;+2^14&amp;quot;,0,null],[&amp;quot;+2^15&amp;quot;,0,null],[&amp;quot;+2^16&amp;quot;,0,null],[&amp;quot;+2^17&amp;quot;,0,null],[&amp;quot;+2^18&amp;quot;,0,null],[&amp;quot;+2^19&amp;quot;,0,null],[&amp;quot;+2^20&amp;quot;,0,null],[&amp;quot;+2^21&amp;quot;,0,null],[&amp;quot;+2^22&amp;quot;,0,null],[&amp;quot;+2^23&amp;quot;,0,null],[&amp;quot;+2^24&amp;quot;,0,null],[&amp;quot;+2^25&amp;quot;,0,null],[&amp;quot;+2^26&amp;quot;,0,null],[&amp;quot;+2^27&amp;quot;,0,null],[&amp;quot;+2^28&amp;quot;,0,null],[&amp;quot;+2^29&amp;quot;,0,null],[&amp;quot;+2^30&amp;quot;,0,null],[&amp;quot;+2^31&amp;quot;,0,null],[&amp;quot;+2^32&amp;quot;,0,null],[&amp;quot;+2^33&amp;quot;,0,null],[&amp;quot;+2^34&amp;quot;,0,null],[&amp;quot;+2^35&amp;quot;,0,null],[&amp;quot;+2^36&amp;quot;,0,null],[&amp;quot;+2^37&amp;quot;,0,null],[&amp;quot;+2^38&amp;quot;,0,null],[&amp;quot;+2^39&amp;quot;,0,null],[&amp;quot;+2^40&amp;quot;,0,null],[&amp;quot;+2^41&amp;quot;,0,null],[&amp;quot;+2^42&amp;quot;,0,null],[&amp;quot;+2^43&amp;quot;,0,null],[&amp;quot;+2^44&amp;quot;,0,null],[&amp;quot;+2^45&amp;quot;,0,null],[&amp;quot;+2^46&amp;quot;,0,null],[&amp;quot;+2^47&amp;quot;,0,null],[&amp;quot;+2^48&amp;quot;,0,null],[&amp;quot;+2^49&amp;quot;,0,null],[&amp;quot;+2^50&amp;quot;,0,null],[&amp;quot;+2^51&amp;quot;,0,null],[&amp;quot;+2^52&amp;quot;,0,null],[&amp;quot;+2^53&amp;quot;,0,null],[&amp;quot;+2^54&amp;quot;,0,null],[&amp;quot;+2^55&amp;quot;,0,null],[&amp;quot;+2^56&amp;quot;,0,null],[&amp;quot;+2^57&amp;quot;,0,null],[&amp;quot;+2^58&amp;quot;,0,null],[&amp;quot;+2^59&amp;quot;,0,null],[&amp;quot;+2^60&amp;quot;,0,null],[&amp;quot;+2^61&amp;quot;,0,null],[&amp;quot;+2^62&amp;quot;,0,null],[&amp;quot;+2^63&amp;quot;,0,null],[&amp;quot;+2^64&amp;quot;,0,null],[&amp;quot;+2^65&amp;quot;,0,null],[&amp;quot;+2^66&amp;quot;,0,null],[&amp;quot;+2^67&amp;quot;,0,null],[&amp;quot;+2^68&amp;quot;,0,null],[&amp;quot;+2^69&amp;quot;,0,null],[&amp;quot;+2^70&amp;quot;,0,null],[&amp;quot;+2^71&amp;quot;,0,null],[&amp;quot;+2^72&amp;quot;,0,null],[&amp;quot;+2^73&amp;quot;,0,null],[&amp;quot;+2^74&amp;quot;,0,null],[&amp;quot;+2^75&amp;quot;,0,null],[&amp;quot;+2^76&amp;quot;,0,null],[&amp;quot;+2^77&amp;quot;,0,null],[&amp;quot;+2^78&amp;quot;,0,null],[&amp;quot;+2^79&amp;quot;,0,null],[&amp;quot;+2^80&amp;quot;,0,null],[&amp;quot;+2^81&amp;quot;,0,null],[&amp;quot;+2^82&amp;quot;,0,null],[&amp;quot;+2^83&amp;quot;,0,null],[&amp;quot;+2^84&amp;quot;,0,null],[&amp;quot;+2^85&amp;quot;,0,null],[&amp;quot;+2^86&amp;quot;,0,null],[&amp;quot;+2^87&amp;quot;,0,null],[&amp;quot;+2^88&amp;quot;,0,null],[&amp;quot;+2^89&amp;quot;,0,null],[&amp;quot;+2^90&amp;quot;,0,null],[&amp;quot;+2^91&amp;quot;,0,null],[&amp;quot;+2^92&amp;quot;,0,null],[&amp;quot;+2^93&amp;quot;,0,null],[&amp;quot;+2^94&amp;quot;,0,null],[&amp;quot;+2^95&amp;quot;,0,null],[&amp;quot;+2^96&amp;quot;,0,null],[&amp;quot;+2^97&amp;quot;,0,null],[&amp;quot;+2^98&amp;quot;,0,null],[&amp;quot;+2^99&amp;quot;,0,null],[&amp;quot;+2^100&amp;quot;,0,null],[&amp;quot;+2^101&amp;quot;,0,null],[&amp;quot;+2^102&amp;quot;,0,null],[&amp;quot;+2^103&amp;quot;,0,null],[&amp;quot;+2^104&amp;quot;,0,null],[&amp;quot;+2^105&amp;quot;,0,null],[&amp;quot;+2^106&amp;quot;,0,null],[&amp;quot;+2^107&amp;quot;,0,null],[&amp;quot;+2^108&amp;quot;,0,null],[&amp;quot;+2^109&amp;quot;,0,null],[&amp;quot;+2^110&amp;quot;,0,null],[&amp;quot;+2^111&amp;quot;,0,null],[&amp;quot;+2^112&amp;quot;,0,null],[&amp;quot;+2^113&amp;quot;,0,null],[&amp;quot;+2^114&amp;quot;,0,null],[&amp;quot;+2^115&amp;quot;,0,null],[&amp;quot;+2^116&amp;quot;,0,null],[&amp;quot;+2^117&amp;quot;,0,null],[&amp;quot;+2^118&amp;quot;,0,null],[&amp;quot;+2^119&amp;quot;,0,null],[&amp;quot;+2^120&amp;quot;,0,null],[&amp;quot;+2^121&amp;quot;,0,null],[&amp;quot;+2^122&amp;quot;,0,null],[&amp;quot;+2^123&amp;quot;,0,null],[&amp;quot;+2^124&amp;quot;,0,null],[&amp;quot;+2^125&amp;quot;,0,null],[&amp;quot;+2^126&amp;quot;,0,null],[&amp;quot;+2^127&amp;quot;,0,null] ]&lt;br /&gt;
 }}&lt;br /&gt;
&lt;br /&gt;
==Taylor==&lt;br /&gt;
&lt;br /&gt;
{{#widget:FloatErrorGraph|id=taylor&lt;br /&gt;
  |data=[ [&amp;quot;-2^127&amp;quot;,0,0],[&amp;quot;-2^126&amp;quot;,0,0],[&amp;quot;-2^125&amp;quot;,0,0],[&amp;quot;-2^124&amp;quot;,0,0],[&amp;quot;-2^123&amp;quot;,0,0],[&amp;quot;-2^122&amp;quot;,0,0],[&amp;quot;-2^121&amp;quot;,0,0],[&amp;quot;-2^120&amp;quot;,0,0],[&amp;quot;-2^119&amp;quot;,0,0],[&amp;quot;-2^118&amp;quot;,0,0],[&amp;quot;-2^117&amp;quot;,0,0],[&amp;quot;-2^116&amp;quot;,0,0],[&amp;quot;-2^115&amp;quot;,0,0],[&amp;quot;-2^114&amp;quot;,0,0],[&amp;quot;-2^113&amp;quot;,0,0],[&amp;quot;-2^112&amp;quot;,0,0],[&amp;quot;-2^111&amp;quot;,0,0],[&amp;quot;-2^110&amp;quot;,0,0],[&amp;quot;-2^109&amp;quot;,0,0],[&amp;quot;-2^108&amp;quot;,0,0],[&amp;quot;-2^107&amp;quot;,0,0],[&amp;quot;-2^106&amp;quot;,0,0],[&amp;quot;-2^105&amp;quot;,0,0],[&amp;quot;-2^104&amp;quot;,0,0],[&amp;quot;-2^103&amp;quot;,0,0],[&amp;quot;-2^102&amp;quot;,0,0],[&amp;quot;-2^101&amp;quot;,0,0],[&amp;quot;-2^100&amp;quot;,0,0],[&amp;quot;-2^99&amp;quot;,0,0],[&amp;quot;-2^98&amp;quot;,0,0],[&amp;quot;-2^97&amp;quot;,0,0],[&amp;quot;-2^96&amp;quot;,0,0],[&amp;quot;-2^95&amp;quot;,0,0],[&amp;quot;-2^94&amp;quot;,0,0],[&amp;quot;-2^93&amp;quot;,0,0],[&amp;quot;-2^92&amp;quot;,0,0],[&amp;quot;-2^91&amp;quot;,0,0],[&amp;quot;-2^90&amp;quot;,0,0],[&amp;quot;-2^89&amp;quot;,0,0],[&amp;quot;-2^88&amp;quot;,0,0],[&amp;quot;-2^87&amp;quot;,0,0],[&amp;quot;-2^86&amp;quot;,0,0],[&amp;quot;-2^85&amp;quot;,0,0],[&amp;quot;-2^84&amp;quot;,0,0],[&amp;quot;-2^83&amp;quot;,0,0],[&amp;quot;-2^82&amp;quot;,0,0],[&amp;quot;-2^81&amp;quot;,0,0],[&amp;quot;-2^80&amp;quot;,0,0],[&amp;quot;-2^79&amp;quot;,0,0],[&amp;quot;-2^78&amp;quot;,0,0],[&amp;quot;-2^77&amp;quot;,0,0],[&amp;quot;-2^76&amp;quot;,0,0],[&amp;quot;-2^75&amp;quot;,0,0],[&amp;quot;-2^74&amp;quot;,0,0],[&amp;quot;-2^73&amp;quot;,0,0],[&amp;quot;-2^72&amp;quot;,0,0],[&amp;quot;-2^71&amp;quot;,0,0],[&amp;quot;-2^70&amp;quot;,0,0],[&amp;quot;-2^69&amp;quot;,0,0],[&amp;quot;-2^68&amp;quot;,0,0],[&amp;quot;-2^67&amp;quot;,0,0],[&amp;quot;-2^66&amp;quot;,0,0],[&amp;quot;-2^65&amp;quot;,0,0],[&amp;quot;-2^64&amp;quot;,0,0],[&amp;quot;-2^63&amp;quot;,1.84467e+19,3.09485e+26],[&amp;quot;-2^62&amp;quot;,9.22336e+18,1.54742e+26],[&amp;quot;-2^61&amp;quot;,4.61168e+18,7.73712e+25],[&amp;quot;-2^60&amp;quot;,2.30584e+18,3.86856e+25],[&amp;quot;-2^59&amp;quot;,1.15292e+18,1.93428e+25],[&amp;quot;-2^58&amp;quot;,5.7646e+17,9.6714e+24],[&amp;quot;-2^57&amp;quot;,2.8823e+17,4.8357e+24],[&amp;quot;-2^56&amp;quot;,1.44115e+17,2.41785e+24],[&amp;quot;-2^55&amp;quot;,7.20575e+16,1.20892e+24],[&amp;quot;-2^54&amp;quot;,3.60288e+16,6.04462e+23],[&amp;quot;-2^53&amp;quot;,1.80144e+16,3.02231e+23],[&amp;quot;-2^52&amp;quot;,9.00719e+15,1.51116e+23],[&amp;quot;-2^51&amp;quot;,4.5036e+15,7.55578e+22],[&amp;quot;-2^50&amp;quot;,2.2518e+15,3.77789e+22],[&amp;quot;-2^49&amp;quot;,1.1259e+15,1.88894e+22],[&amp;quot;-2^48&amp;quot;,5.62949e+14,9.44472e+21],[&amp;quot;-2^47&amp;quot;,2.81475e+14,4.72236e+21],[&amp;quot;-2^46&amp;quot;,1.40737e+14,2.36118e+21],[&amp;quot;-2^45&amp;quot;,7.03687e+13,1.18059e+21],[&amp;quot;-2^44&amp;quot;,3.51843e+13,5.90295e+20],[&amp;quot;-2^43&amp;quot;,1.75922e+13,2.95148e+20],[&amp;quot;-2^42&amp;quot;,8.79608e+12,1.47574e+20],[&amp;quot;-2^41&amp;quot;,4.39804e+12,7.37869e+19],[&amp;quot;-2^40&amp;quot;,2.19902e+12,3.68935e+19],[&amp;quot;-2^39&amp;quot;,1.09951e+12,1.84467e+19],[&amp;quot;-2^38&amp;quot;,5.49755e+11,9.22336e+18],[&amp;quot;-2^37&amp;quot;,2.74878e+11,4.61168e+18],[&amp;quot;-2^36&amp;quot;,1.37439e+11,2.30584e+18],[&amp;quot;-2^35&amp;quot;,6.87194e+10,1.15292e+18],[&amp;quot;-2^34&amp;quot;,3.43597e+10,5.7646e+17],[&amp;quot;-2^33&amp;quot;,1.71799e+10,2.8823e+17],[&amp;quot;-2^32&amp;quot;,8.58993e+09,1.44115e+17],[&amp;quot;-2^31&amp;quot;,4.29085e+09,7.19885e+16],[&amp;quot;-2^30&amp;quot;,2.14542e+09,3.59943e+16],[&amp;quot;-2^29&amp;quot;,1.07271e+09,1.79971e+16],[&amp;quot;-2^28&amp;quot;,5.36356e+08,8.99856e+15],[&amp;quot;-2^27&amp;quot;,2.68178e+08,4.49928e+15],[&amp;quot;-2^26&amp;quot;,1.34089e+08,2.24964e+15],[&amp;quot;-2^25&amp;quot;,6.70445e+07,1.12482e+15],[&amp;quot;-2^24&amp;quot;,3.35223e+07,5.6241e+14],[&amp;quot;-2^23&amp;quot;,1.67611e+07,2.81205e+14],[&amp;quot;-2^22&amp;quot;,8.38056e+06,1.40603e+14],[&amp;quot;-2^21&amp;quot;,4.19028e+06,7.03013e+13],[&amp;quot;-2^20&amp;quot;,2.09514e+06,3.51506e+13],[&amp;quot;-2^19&amp;quot;,1.04757e+06,1.75753e+13],[&amp;quot;-2^18&amp;quot;,523784,8.78764e+12],[&amp;quot;-2^17&amp;quot;,261892,4.39381e+12],[&amp;quot;-2^16&amp;quot;,130945,2.1969e+12],[&amp;quot;-2^15&amp;quot;,65472.2,1.09844e+12],[&amp;quot;-2^14&amp;quot;,32735.6,5.49212e+11],[&amp;quot;-2^13&amp;quot;,16367.3,2.74598e+11],[&amp;quot;-2^12&amp;quot;,8183.15,1.3729e+11],[&amp;quot;-2^11&amp;quot;,4091.07,6.86368e+10],[&amp;quot;-2^10&amp;quot;,2045.04,3.431e+10],[&amp;quot;-2^9&amp;quot;,1022.02,1.71466e+10],[&amp;quot;-2^8&amp;quot;,510.509,8.56492e+09],[&amp;quot;-2^7&amp;quot;,254.754,4.27406e+09],[&amp;quot;-2^6&amp;quot;,126.876,2.12862e+09],[&amp;quot;-2^5&amp;quot;,62.9354,1.05588e+09],[&amp;quot;-2^4&amp;quot;,30.9629,5.19471e+08],[&amp;quot;-2^3&amp;quot;,14.9718,2.51185e+08],[&amp;quot;-2^2&amp;quot;,6.96672,1.16882e+08],[&amp;quot;-2^1&amp;quot;,2.94633,4.94312e+07],[&amp;quot;-2^0&amp;quot;,0.940715,1.57826e+07],[&amp;quot;-2^-1&amp;quot;,0.127635,2.14135e+06],[&amp;quot;-2^-2&amp;quot;,0.00318735,106950],[&amp;quot;-2^-3&amp;quot;,3.11475e-05,2090.28],[&amp;quot;-2^-4&amp;quot;,2.57737e-07,34.5929],[&amp;quot;-2^-5&amp;quot;,3.89213e-09,1.04478],[&amp;quot;-2^-6&amp;quot;,9.51472e-10,0.510818],[&amp;quot;-2^-7&amp;quot;,4.66402e-10,0.500795],[&amp;quot;-2^-8&amp;quot;,2.32917e-10,0.500185],[&amp;quot;-2^-9&amp;quot;,1.16423e-10,0.500033],[&amp;quot;-2^-10&amp;quot;,5.82082e-11,0.500004],[&amp;quot;-2^-11&amp;quot;,2.91037e-11,0.499998],[&amp;quot;-2^-12&amp;quot;,1.45519e-11,0.5],[&amp;quot;-2^-13&amp;quot;,4.85062e-12,0.333332],[&amp;quot;-2^-14&amp;quot;,6.06328e-13,0.0833331],[&amp;quot;-2^-15&amp;quot;,7.5791e-14,0.0208333],[&amp;quot;-2^-16&amp;quot;,9.47387e-15,0.00520832],[&amp;quot;-2^-17&amp;quot;,1.18424e-15,0.00130208],[&amp;quot;-2^-18&amp;quot;,1.48029e-16,0.00032552],[&amp;quot;-2^-19&amp;quot;,1.85039e-17,8.13808e-05],[&amp;quot;-2^-20&amp;quot;,2.31304e-18,2.03457e-05],[&amp;quot;-2^-21&amp;quot;,2.89156e-19,5.08688e-06],[&amp;quot;-2^-22&amp;quot;,3.61577e-20,1.27219e-06],[&amp;quot;-2^-23&amp;quot;,4.52633e-21,3.18512e-07],[&amp;quot;-2^-24&amp;quot;,5.691e-22,8.00937e-08],[&amp;quot;-2^-25&amp;quot;,7.27919e-23,2.04891e-08],[&amp;quot;-2^-26&amp;quot;,9.92617e-24,5.58794e-09],[&amp;quot;-2^-27&amp;quot;,1.65436e-24,1.86265e-09],[&amp;quot;-2^-28&amp;quot;,8.27181e-25,1.86265e-09],[&amp;quot;-2^-29&amp;quot;,4.1359e-25,1.86265e-09],[&amp;quot;-2^-30&amp;quot;,2.06795e-25,1.86265e-09],[&amp;quot;-2^-31&amp;quot;,1.03398e-25,1.86265e-09],[&amp;quot;-2^-32&amp;quot;,5.16988e-26,1.86265e-09],[&amp;quot;-2^-33&amp;quot;,2.58494e-26,1.86265e-09],[&amp;quot;-2^-34&amp;quot;,1.29247e-26,1.86265e-09],[&amp;quot;-2^-35&amp;quot;,6.46235e-27,1.86265e-09],[&amp;quot;-2^-36&amp;quot;,3.23117e-27,1.86265e-09],[&amp;quot;-2^-37&amp;quot;,1.61559e-27,1.86265e-09],[&amp;quot;-2^-38&amp;quot;,8.07794e-28,1.86265e-09],[&amp;quot;-2^-39&amp;quot;,4.03897e-28,1.86265e-09],[&amp;quot;-2^-40&amp;quot;,2.01948e-28,1.86265e-09],[&amp;quot;-2^-41&amp;quot;,1.00974e-28,1.86265e-09],[&amp;quot;-2^-42&amp;quot;,5.04871e-29,1.86265e-09],[&amp;quot;-2^-43&amp;quot;,2.52435e-29,1.86265e-09],[&amp;quot;-2^-44&amp;quot;,1.26218e-29,1.86265e-09],[&amp;quot;-2^-45&amp;quot;,6.31089e-30,1.86265e-09],[&amp;quot;-2^-46&amp;quot;,3.15544e-30,1.86265e-09],[&amp;quot;-2^-47&amp;quot;,1.57772e-30,1.86265e-09],[&amp;quot;-2^-48&amp;quot;,7.88861e-31,1.86265e-09],[&amp;quot;-2^-49&amp;quot;,3.9443e-31,1.86265e-09],[&amp;quot;-2^-50&amp;quot;,1.97215e-31,1.86265e-09],[&amp;quot;-2^-51&amp;quot;,9.86076e-32,1.86265e-09],[&amp;quot;-2^-52&amp;quot;,4.93038e-32,1.86265e-09],[&amp;quot;-2^-53&amp;quot;,2.46519e-32,1.86265e-09],[&amp;quot;-2^-54&amp;quot;,1.2326e-32,1.86265e-09],[&amp;quot;-2^-55&amp;quot;,0,0],[&amp;quot;-2^-56&amp;quot;,0,0],[&amp;quot;-2^-57&amp;quot;,0,0],[&amp;quot;-2^-58&amp;quot;,0,0],[&amp;quot;-2^-59&amp;quot;,0,0],[&amp;quot;-2^-60&amp;quot;,0,0],[&amp;quot;-2^-61&amp;quot;,0,0],[&amp;quot;-2^-62&amp;quot;,0,0],[&amp;quot;-2^-63&amp;quot;,0,0],[&amp;quot;-2^-64&amp;quot;,0,0],[&amp;quot;-2^-65&amp;quot;,0,0],[&amp;quot;-2^-66&amp;quot;,0,0],[&amp;quot;-2^-67&amp;quot;,0,0],[&amp;quot;-2^-68&amp;quot;,0,0],[&amp;quot;-2^-69&amp;quot;,0,0],[&amp;quot;-2^-70&amp;quot;,0,0],[&amp;quot;-2^-71&amp;quot;,0,0],[&amp;quot;-2^-72&amp;quot;,0,0],[&amp;quot;-2^-73&amp;quot;,0,0],[&amp;quot;-2^-74&amp;quot;,0,0],[&amp;quot;-2^-75&amp;quot;,0,0],[&amp;quot;-2^-76&amp;quot;,0,0],[&amp;quot;-2^-77&amp;quot;,0,0],[&amp;quot;-2^-78&amp;quot;,0,0],[&amp;quot;-2^-79&amp;quot;,0,0],[&amp;quot;-2^-80&amp;quot;,0,0],[&amp;quot;-2^-81&amp;quot;,0,0],[&amp;quot;-2^-82&amp;quot;,0,0],[&amp;quot;-2^-83&amp;quot;,0,0],[&amp;quot;-2^-84&amp;quot;,0,0],[&amp;quot;-2^-85&amp;quot;,0,0],[&amp;quot;-2^-86&amp;quot;,0,0],[&amp;quot;-2^-87&amp;quot;,0,0],[&amp;quot;-2^-88&amp;quot;,0,0],[&amp;quot;-2^-89&amp;quot;,0,0],[&amp;quot;-2^-90&amp;quot;,0,0],[&amp;quot;-2^-91&amp;quot;,0,0],[&amp;quot;-2^-92&amp;quot;,0,0],[&amp;quot;-2^-93&amp;quot;,0,0],[&amp;quot;-2^-94&amp;quot;,0,0],[&amp;quot;-2^-95&amp;quot;,0,0],[&amp;quot;-2^-96&amp;quot;,0,0],[&amp;quot;-2^-97&amp;quot;,0,0],[&amp;quot;-2^-98&amp;quot;,0,0],[&amp;quot;-2^-99&amp;quot;,0,0],[&amp;quot;-2^-100&amp;quot;,0,0],[&amp;quot;-2^-101&amp;quot;,0,0],[&amp;quot;-2^-102&amp;quot;,0,0],[&amp;quot;-2^-103&amp;quot;,0,0],[&amp;quot;-2^-104&amp;quot;,0,0],[&amp;quot;-2^-105&amp;quot;,0,0],[&amp;quot;-2^-106&amp;quot;,0,0],[&amp;quot;-2^-107&amp;quot;,0,0],[&amp;quot;-2^-108&amp;quot;,0,0],[&amp;quot;-2^-109&amp;quot;,0,0],[&amp;quot;-2^-110&amp;quot;,0,0],[&amp;quot;-2^-111&amp;quot;,0,0],[&amp;quot;-2^-112&amp;quot;,0,0],[&amp;quot;-2^-113&amp;quot;,0,0],[&amp;quot;-2^-114&amp;quot;,0,0],[&amp;quot;-2^-115&amp;quot;,0,0],[&amp;quot;-2^-116&amp;quot;,0,0],[&amp;quot;-2^-117&amp;quot;,0,0],[&amp;quot;-2^-118&amp;quot;,0,0],[&amp;quot;-2^-119&amp;quot;,0,0],[&amp;quot;-2^-120&amp;quot;,0,0],[&amp;quot;-2^-121&amp;quot;,0,0],[&amp;quot;-2^-122&amp;quot;,0,0],[&amp;quot;-2^-123&amp;quot;,0,0],[&amp;quot;-2^-124&amp;quot;,0,0],[&amp;quot;-2^-125&amp;quot;,0,0],[&amp;quot;-2^-126&amp;quot;,0,0],[&amp;quot;-SN&amp;quot;,0,0],[&amp;quot;0&amp;quot;,0,0],[&amp;quot;+SN&amp;quot;,0,0],[&amp;quot;+2^-126&amp;quot;,0,0],[&amp;quot;+2^-125&amp;quot;,0,0],[&amp;quot;+2^-124&amp;quot;,0,0],[&amp;quot;+2^-123&amp;quot;,0,0],[&amp;quot;+2^-122&amp;quot;,0,0],[&amp;quot;+2^-121&amp;quot;,0,0],[&amp;quot;+2^-120&amp;quot;,0,0],[&amp;quot;+2^-119&amp;quot;,0,0],[&amp;quot;+2^-118&amp;quot;,0,0],[&amp;quot;+2^-117&amp;quot;,0,0],[&amp;quot;+2^-116&amp;quot;,0,0],[&amp;quot;+2^-115&amp;quot;,0,0],[&amp;quot;+2^-114&amp;quot;,0,0],[&amp;quot;+2^-113&amp;quot;,0,0],[&amp;quot;+2^-112&amp;quot;,0,0],[&amp;quot;+2^-111&amp;quot;,0,0],[&amp;quot;+2^-110&amp;quot;,0,0],[&amp;quot;+2^-109&amp;quot;,0,0],[&amp;quot;+2^-108&amp;quot;,0,0],[&amp;quot;+2^-107&amp;quot;,0,0],[&amp;quot;+2^-106&amp;quot;,0,0],[&amp;quot;+2^-105&amp;quot;,0,0],[&amp;quot;+2^-104&amp;quot;,0,0],[&amp;quot;+2^-103&amp;quot;,0,0],[&amp;quot;+2^-102&amp;quot;,0,0],[&amp;quot;+2^-101&amp;quot;,0,0],[&amp;quot;+2^-100&amp;quot;,0,0],[&amp;quot;+2^-99&amp;quot;,0,0],[&amp;quot;+2^-98&amp;quot;,0,0],[&amp;quot;+2^-97&amp;quot;,0,0],[&amp;quot;+2^-96&amp;quot;,0,0],[&amp;quot;+2^-95&amp;quot;,0,0],[&amp;quot;+2^-94&amp;quot;,0,0],[&amp;quot;+2^-93&amp;quot;,0,0],[&amp;quot;+2^-92&amp;quot;,0,0],[&amp;quot;+2^-91&amp;quot;,0,0],[&amp;quot;+2^-90&amp;quot;,0,0],[&amp;quot;+2^-89&amp;quot;,0,0],[&amp;quot;+2^-88&amp;quot;,0,0],[&amp;quot;+2^-87&amp;quot;,0,0],[&amp;quot;+2^-86&amp;quot;,0,0],[&amp;quot;+2^-85&amp;quot;,0,0],[&amp;quot;+2^-84&amp;quot;,0,0],[&amp;quot;+2^-83&amp;quot;,0,0],[&amp;quot;+2^-82&amp;quot;,0,0],[&amp;quot;+2^-81&amp;quot;,0,0],[&amp;quot;+2^-80&amp;quot;,0,0],[&amp;quot;+2^-79&amp;quot;,0,0],[&amp;quot;+2^-78&amp;quot;,0,0],[&amp;quot;+2^-77&amp;quot;,0,0],[&amp;quot;+2^-76&amp;quot;,0,0],[&amp;quot;+2^-75&amp;quot;,0,0],[&amp;quot;+2^-74&amp;quot;,0,0],[&amp;quot;+2^-73&amp;quot;,0,0],[&amp;quot;+2^-72&amp;quot;,0,0],[&amp;quot;+2^-71&amp;quot;,0,0],[&amp;quot;+2^-70&amp;quot;,0,0],[&amp;quot;+2^-69&amp;quot;,0,0],[&amp;quot;+2^-68&amp;quot;,0,0],[&amp;quot;+2^-67&amp;quot;,0,0],[&amp;quot;+2^-66&amp;quot;,0,0],[&amp;quot;+2^-65&amp;quot;,0,0],[&amp;quot;+2^-64&amp;quot;,0,0],[&amp;quot;+2^-63&amp;quot;,0,0],[&amp;quot;+2^-62&amp;quot;,0,0],[&amp;quot;+2^-61&amp;quot;,0,0],[&amp;quot;+2^-60&amp;quot;,0,0],[&amp;quot;+2^-59&amp;quot;,0,0],[&amp;quot;+2^-58&amp;quot;,0,0],[&amp;quot;+2^-57&amp;quot;,0,0],[&amp;quot;+2^-56&amp;quot;,0,0],[&amp;quot;+2^-55&amp;quot;,0,0],[&amp;quot;+2^-54&amp;quot;,1.2326e-32,1.86265e-09],[&amp;quot;+2^-53&amp;quot;,2.46519e-32,1.86265e-09],[&amp;quot;+2^-52&amp;quot;,4.93038e-32,1.86265e-09],[&amp;quot;+2^-51&amp;quot;,9.86076e-32,1.86265e-09],[&amp;quot;+2^-50&amp;quot;,1.97215e-31,1.86265e-09],[&amp;quot;+2^-49&amp;quot;,3.9443e-31,1.86265e-09],[&amp;quot;+2^-48&amp;quot;,7.88861e-31,1.86265e-09],[&amp;quot;+2^-47&amp;quot;,1.57772e-30,1.86265e-09],[&amp;quot;+2^-46&amp;quot;,3.15544e-30,1.86265e-09],[&amp;quot;+2^-45&amp;quot;,6.31089e-30,1.86265e-09],[&amp;quot;+2^-44&amp;quot;,1.26218e-29,1.86265e-09],[&amp;quot;+2^-43&amp;quot;,2.52435e-29,1.86265e-09],[&amp;quot;+2^-42&amp;quot;,5.04871e-29,1.86265e-09],[&amp;quot;+2^-41&amp;quot;,1.00974e-28,1.86265e-09],[&amp;quot;+2^-40&amp;quot;,2.01948e-28,1.86265e-09],[&amp;quot;+2^-39&amp;quot;,4.03897e-28,1.86265e-09],[&amp;quot;+2^-38&amp;quot;,8.07794e-28,1.86265e-09],[&amp;quot;+2^-37&amp;quot;,1.61559e-27,1.86265e-09],[&amp;quot;+2^-36&amp;quot;,3.23117e-27,1.86265e-09],[&amp;quot;+2^-35&amp;quot;,6.46235e-27,1.86265e-09],[&amp;quot;+2^-34&amp;quot;,1.29247e-26,1.86265e-09],[&amp;quot;+2^-33&amp;quot;,2.58494e-26,1.86265e-09],[&amp;quot;+2^-32&amp;quot;,5.16988e-26,1.86265e-09],[&amp;quot;+2^-31&amp;quot;,1.03398e-25,1.86265e-09],[&amp;quot;+2^-30&amp;quot;,2.06795e-25,1.86265e-09],[&amp;quot;+2^-29&amp;quot;,4.1359e-25,1.86265e-09],[&amp;quot;+2^-28&amp;quot;,8.27181e-25,1.86265e-09],[&amp;quot;+2^-27&amp;quot;,1.65436e-24,1.86265e-09],[&amp;quot;+2^-26&amp;quot;,9.92617e-24,5.58794e-09],[&amp;quot;+2^-25&amp;quot;,7.27919e-23,2.04891e-08],[&amp;quot;+2^-24&amp;quot;,5.691e-22,8.00937e-08],[&amp;quot;+2^-23&amp;quot;,4.52633e-21,3.18512e-07],[&amp;quot;+2^-22&amp;quot;,3.61577e-20,1.27219e-06],[&amp;quot;+2^-21&amp;quot;,2.89156e-19,5.08688e-06],[&amp;quot;+2^-20&amp;quot;,2.31304e-18,2.03457e-05],[&amp;quot;+2^-19&amp;quot;,1.85039e-17,8.13808e-05],[&amp;quot;+2^-18&amp;quot;,1.48029e-16,0.00032552],[&amp;quot;+2^-17&amp;quot;,1.18424e-15,0.00130208],[&amp;quot;+2^-16&amp;quot;,9.47387e-15,0.00520832],[&amp;quot;+2^-15&amp;quot;,7.5791e-14,0.0208333],[&amp;quot;+2^-14&amp;quot;,6.06328e-13,0.0833331],[&amp;quot;+2^-13&amp;quot;,4.85062e-12,0.333332],[&amp;quot;+2^-12&amp;quot;,1.45519e-11,0.5],[&amp;quot;+2^-11&amp;quot;,2.91037e-11,0.499998],[&amp;quot;+2^-10&amp;quot;,5.82082e-11,0.500004],[&amp;quot;+2^-9&amp;quot;,1.16423e-10,0.500033],[&amp;quot;+2^-8&amp;quot;,2.32917e-10,0.500185],[&amp;quot;+2^-7&amp;quot;,4.66402e-10,0.500795],[&amp;quot;+2^-6&amp;quot;,9.51472e-10,0.510818],[&amp;quot;+2^-5&amp;quot;,3.89213e-09,1.04478],[&amp;quot;+2^-4&amp;quot;,2.57737e-07,34.5929],[&amp;quot;+2^-3&amp;quot;,3.11475e-05,2090.28],[&amp;quot;+2^-2&amp;quot;,0.00318735,106950],[&amp;quot;+2^-1&amp;quot;,0.127635,2.14135e+06],[&amp;quot;+2^0&amp;quot;,0.940715,1.57826e+07],[&amp;quot;+2^1&amp;quot;,2.94633,4.94312e+07],[&amp;quot;+2^2&amp;quot;,6.96672,1.16882e+08],[&amp;quot;+2^3&amp;quot;,14.9718,1.33874e+08],[&amp;quot;+2^4&amp;quot;,30.9629,2.59736e+08],[&amp;quot;+2^5&amp;quot;,62.9354,5.27941e+08],[&amp;quot;+2^6&amp;quot;,126.876,1.06431e+09],[&amp;quot;+2^7&amp;quot;,254.754,2.13703e+09],[&amp;quot;+2^8&amp;quot;,510.509,4.28246e+09],[&amp;quot;+2^9&amp;quot;,1022.02,8.57331e+09],[&amp;quot;+2^10&amp;quot;,2045.04,1.7155e+10],[&amp;quot;+2^11&amp;quot;,4091.07,3.43184e+10],[&amp;quot;+2^12&amp;quot;,8183.15,6.86452e+10],[&amp;quot;+2^13&amp;quot;,16367.3,1.37299e+11],[&amp;quot;+2^14&amp;quot;,32735.6,2.74606e+11],[&amp;quot;+2^15&amp;quot;,65472.2,5.4922e+11],[&amp;quot;+2^16&amp;quot;,130945,1.09845e+12],[&amp;quot;+2^17&amp;quot;,261892,2.19691e+12],[&amp;quot;+2^18&amp;quot;,523784,4.39382e+12],[&amp;quot;+2^19&amp;quot;,1.04757e+06,8.78765e+12],[&amp;quot;+2^20&amp;quot;,2.09514e+06,1.75753e+13],[&amp;quot;+2^21&amp;quot;,4.19028e+06,3.51506e+13],[&amp;quot;+2^22&amp;quot;,8.38056e+06,7.03013e+13],[&amp;quot;+2^23&amp;quot;,1.67611e+07,1.40603e+14],[&amp;quot;+2^24&amp;quot;,3.35223e+07,2.81205e+14],[&amp;quot;+2^25&amp;quot;,6.70445e+07,5.6241e+14],[&amp;quot;+2^26&amp;quot;,1.34089e+08,1.12482e+15],[&amp;quot;+2^27&amp;quot;,2.68178e+08,2.24964e+15],[&amp;quot;+2^28&amp;quot;,5.36356e+08,4.49928e+15],[&amp;quot;+2^29&amp;quot;,1.07271e+09,8.99856e+15],[&amp;quot;+2^30&amp;quot;,2.14542e+09,1.79971e+16],[&amp;quot;+2^31&amp;quot;,4.29085e+09,3.59943e+16],[&amp;quot;+2^32&amp;quot;,8.58993e+09,7.20575e+16],[&amp;quot;+2^33&amp;quot;,1.71799e+10,1.44115e+17],[&amp;quot;+2^34&amp;quot;,3.43597e+10,2.8823e+17],[&amp;quot;+2^35&amp;quot;,6.87194e+10,5.7646e+17],[&amp;quot;+2^36&amp;quot;,1.37439e+11,1.15292e+18],[&amp;quot;+2^37&amp;quot;,2.74878e+11,2.30584e+18],[&amp;quot;+2^38&amp;quot;,5.49755e+11,4.61168e+18],[&amp;quot;+2^39&amp;quot;,1.09951e+12,9.22336e+18],[&amp;quot;+2^40&amp;quot;,2.19902e+12,1.84467e+19],[&amp;quot;+2^41&amp;quot;,4.39804e+12,3.68935e+19],[&amp;quot;+2^42&amp;quot;,8.79608e+12,7.37869e+19],[&amp;quot;+2^43&amp;quot;,1.75922e+13,1.47574e+20],[&amp;quot;+2^44&amp;quot;,3.51843e+13,2.95148e+20],[&amp;quot;+2^45&amp;quot;,7.03687e+13,5.90295e+20],[&amp;quot;+2^46&amp;quot;,1.40737e+14,1.18059e+21],[&amp;quot;+2^47&amp;quot;,2.81475e+14,2.36118e+21],[&amp;quot;+2^48&amp;quot;,5.62949e+14,4.72236e+21],[&amp;quot;+2^49&amp;quot;,1.1259e+15,9.44472e+21],[&amp;quot;+2^50&amp;quot;,2.2518e+15,1.88894e+22],[&amp;quot;+2^51&amp;quot;,4.5036e+15,3.77789e+22],[&amp;quot;+2^52&amp;quot;,9.00719e+15,7.55578e+22],[&amp;quot;+2^53&amp;quot;,1.80144e+16,1.51116e+23],[&amp;quot;+2^54&amp;quot;,3.60288e+16,3.02231e+23],[&amp;quot;+2^55&amp;quot;,7.20575e+16,6.04462e+23],[&amp;quot;+2^56&amp;quot;,1.44115e+17,1.20892e+24],[&amp;quot;+2^57&amp;quot;,2.8823e+17,2.41785e+24],[&amp;quot;+2^58&amp;quot;,5.7646e+17,4.8357e+24],[&amp;quot;+2^59&amp;quot;,1.15292e+18,9.6714e+24],[&amp;quot;+2^60&amp;quot;,2.30584e+18,1.93428e+25],[&amp;quot;+2^61&amp;quot;,4.61168e+18,3.86856e+25],[&amp;quot;+2^62&amp;quot;,9.22336e+18,7.73712e+25],[&amp;quot;+2^63&amp;quot;,1.84467e+19,1.54742e+26],[&amp;quot;+2^64&amp;quot;,0,0],[&amp;quot;+2^65&amp;quot;,0,0],[&amp;quot;+2^66&amp;quot;,0,0],[&amp;quot;+2^67&amp;quot;,0,0],[&amp;quot;+2^68&amp;quot;,0,0],[&amp;quot;+2^69&amp;quot;,0,0],[&amp;quot;+2^70&amp;quot;,0,0],[&amp;quot;+2^71&amp;quot;,0,0],[&amp;quot;+2^72&amp;quot;,0,0],[&amp;quot;+2^73&amp;quot;,0,0],[&amp;quot;+2^74&amp;quot;,0,0],[&amp;quot;+2^75&amp;quot;,0,0],[&amp;quot;+2^76&amp;quot;,0,0],[&amp;quot;+2^77&amp;quot;,0,0],[&amp;quot;+2^78&amp;quot;,0,0],[&amp;quot;+2^79&amp;quot;,0,0],[&amp;quot;+2^80&amp;quot;,0,0],[&amp;quot;+2^81&amp;quot;,0,0],[&amp;quot;+2^82&amp;quot;,0,0],[&amp;quot;+2^83&amp;quot;,0,0],[&amp;quot;+2^84&amp;quot;,0,0],[&amp;quot;+2^85&amp;quot;,0,0],[&amp;quot;+2^86&amp;quot;,0,0],[&amp;quot;+2^87&amp;quot;,0,0],[&amp;quot;+2^88&amp;quot;,0,0],[&amp;quot;+2^89&amp;quot;,0,0],[&amp;quot;+2^90&amp;quot;,0,0],[&amp;quot;+2^91&amp;quot;,0,0],[&amp;quot;+2^92&amp;quot;,0,0],[&amp;quot;+2^93&amp;quot;,0,0],[&amp;quot;+2^94&amp;quot;,0,0],[&amp;quot;+2^95&amp;quot;,0,0],[&amp;quot;+2^96&amp;quot;,0,0],[&amp;quot;+2^97&amp;quot;,0,0],[&amp;quot;+2^98&amp;quot;,0,0],[&amp;quot;+2^99&amp;quot;,0,0],[&amp;quot;+2^100&amp;quot;,0,0],[&amp;quot;+2^101&amp;quot;,0,0],[&amp;quot;+2^102&amp;quot;,0,0],[&amp;quot;+2^103&amp;quot;,0,0],[&amp;quot;+2^104&amp;quot;,0,0],[&amp;quot;+2^105&amp;quot;,0,0],[&amp;quot;+2^106&amp;quot;,0,0],[&amp;quot;+2^107&amp;quot;,0,0],[&amp;quot;+2^108&amp;quot;,0,0],[&amp;quot;+2^109&amp;quot;,0,0],[&amp;quot;+2^110&amp;quot;,0,0],[&amp;quot;+2^111&amp;quot;,0,0],[&amp;quot;+2^112&amp;quot;,0,0],[&amp;quot;+2^113&amp;quot;,0,0],[&amp;quot;+2^114&amp;quot;,0,0],[&amp;quot;+2^115&amp;quot;,0,0],[&amp;quot;+2^116&amp;quot;,0,0],[&amp;quot;+2^117&amp;quot;,0,0],[&amp;quot;+2^118&amp;quot;,0,0],[&amp;quot;+2^119&amp;quot;,0,0],[&amp;quot;+2^120&amp;quot;,0,0],[&amp;quot;+2^121&amp;quot;,0,0],[&amp;quot;+2^122&amp;quot;,0,0],[&amp;quot;+2^123&amp;quot;,0,0],[&amp;quot;+2^124&amp;quot;,0,0],[&amp;quot;+2^125&amp;quot;,0,0],[&amp;quot;+2^126&amp;quot;,0,0],[&amp;quot;+2^127&amp;quot;,0,0] ]&lt;br /&gt;
 }}&lt;br /&gt;
&lt;br /&gt;
== Localized Approximation Functions ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
!Id&lt;br /&gt;
!Formula&lt;br /&gt;
!ULP &amp;lt;= 2.0&lt;br /&gt;
!NAN&lt;br /&gt;
|-&lt;br /&gt;
|signum&lt;br /&gt;
|&amp;lt;math&amp;gt;\sgn{x}&amp;lt;/math&amp;gt;&lt;br /&gt;
| -inf &amp;lt; x &amp;lt; -8.31763&lt;br /&gt;
8.31763 &amp;lt; x &amp;lt; inf&lt;br /&gt;
| -&lt;br /&gt;
|-&lt;br /&gt;
|poly1&lt;br /&gt;
|&amp;lt;math&amp;gt;x&amp;lt;/math&amp;gt;&lt;br /&gt;
| -0.000704229 &amp;lt; x &amp;lt; 0.000704229&lt;br /&gt;
| -&lt;br /&gt;
|-&lt;br /&gt;
|poly3&lt;br /&gt;
|&amp;lt;math&amp;gt;x - \frac{1}{3} x^3&amp;lt;/math&amp;gt;&lt;br /&gt;
| -0.0291781 &amp;lt; x &amp;lt; 0.0291781 &lt;br /&gt;
| -&lt;br /&gt;
|-&lt;br /&gt;
|lampert7&lt;br /&gt;
|&amp;lt;math&amp;gt;\frac{x \cdot (135135 + x^2 \cdot (17325 + x^2 \cdot (378 + x^2)))}{135135 + x^2 \cdot (62370 + x^2 \cdot (3150 + 28 \cdot x^2))}&lt;br /&gt;
&amp;lt;/math&amp;gt;&lt;br /&gt;
| -0.000947416 &amp;lt; x &amp;lt; 0.000947207&lt;br /&gt;
| -inf &amp;lt; x &amp;lt; -1.51629e+06&lt;br /&gt;
1.51629e+06 &amp;lt; x &amp;lt; inf&lt;br /&gt;
|-&lt;br /&gt;
|pade&lt;br /&gt;
|&amp;lt;math&amp;gt;x + x \cdot \frac{x^2 \cdot \left( p_1 \cdot x^2 + p_0 \right)}{\left( x^2 + q_1 \right) \cdot x^2 + q_0} &lt;br /&gt;
&lt;br /&gt;
&amp;lt;/math&amp;gt; &amp;lt;math&amp;gt;\begin{align}&lt;br /&gt;
p_0 &amp;amp;= -0.2059432032 \\&lt;br /&gt;
p_1 &amp;amp;= -0.0009577527 \\&lt;br /&gt;
q_0 &amp;amp;= 0.6178299136 \\ &lt;br /&gt;
q_1 &amp;amp;= 0.25&lt;br /&gt;
\end{align}&lt;br /&gt;
&amp;lt;/math&amp;gt;&lt;br /&gt;
| -0.0016973 &amp;lt; x &amp;lt; 0.0016973 &lt;br /&gt;
| -inf &amp;lt; x &amp;lt; -6.14896e+18&lt;br /&gt;
6.14896e+18 &amp;lt; x &amp;lt; inf&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== External Links ==&lt;br /&gt;
&lt;br /&gt;
* https://yaikhom.com/2020-04-28-localised-approximation-of-hyperbolic-tangents.html#mjx-eqn-eqn7-th%20degree%20lambert%20approximant&lt;br /&gt;
*https://github.com/llvm/llvm-project/blob/main/libc/src/math/generic/tanhf.cpp&lt;br /&gt;
*https://forums.developer.nvidia.com/t/hardware-accelerated-tanh-on-turing/173291&lt;br /&gt;
[[Category:Numerical Precision]]&lt;/div&gt;</summary>
		<author><name>Timo.stripf</name></author>
	</entry>
	<entry>
		<id>https://www.emmtrix.com/w139/index.php?title=Main_Page&amp;diff=2924</id>
		<title>Main Page</title>
		<link rel="alternate" type="text/html" href="https://www.emmtrix.com/w139/index.php?title=Main_Page&amp;diff=2924"/>
		<updated>2026-02-04T11:42:20Z</updated>

		<summary type="html">&lt;p&gt;Timo.stripf: /* Other Topics */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;__NOTOC__&lt;br /&gt;
&lt;br /&gt;
Welcome to the emmtrix Technologies Wiki. As a company with a deep-rooted passion for compilers, we specialize in source-to-source compilers designed to analyze, optimize and transform your code. This Wiki aims to offer detailed, technical background information that complements the tools and resources available on our official website. Here, you&#039;ll find in-depth explanations, usage guidelines, and insights into the engineering behind our specialized software solutions. Whether you&#039;re a developer or a technically-inclined enthusiast, this space is designed to deepen your understanding of what makes our tools essential for your projects.&lt;br /&gt;
&lt;br /&gt;
{| border=&amp;quot;0&amp;quot; style=&amp;quot;padding: 10px&amp;quot;&lt;br /&gt;
| style=&amp;quot;vertical-align: top; padding: 10px;&amp;quot; |&lt;br /&gt;
=== emmtrix Products ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;!--T:7--&amp;gt;&lt;br /&gt;
* [[emmtrix Dependency Analyzer]]&lt;br /&gt;
* [[emmtrix Performance Estimator]]&lt;br /&gt;
* [[emmtrix Code Vectorizer]]&lt;br /&gt;
** [[emmtrix ONNX-to-C Code Generator]]&lt;br /&gt;
* [[emmtrix C++ to C Compiler]]&lt;br /&gt;
** [https://online-ecpp2c.emmtrix.com emmtrix C++ to C Compiler Online]&lt;br /&gt;
* [[emmtrix C to Rust Compiler]]&lt;br /&gt;
* emmtrix Parallel Studio&lt;br /&gt;
* Generic information&lt;br /&gt;
** [[emmtrix Studio Release Notes]]&lt;br /&gt;
** [[:Category:emmtrix Studio FAQ|FAQ]]&lt;br /&gt;
* Discontinued product&lt;br /&gt;
** [[emmtrix Code Generator]]&lt;br /&gt;
** [[emmtrix Model Code Generator]]&lt;br /&gt;
* Other Solutions&lt;br /&gt;
** [[TC4x PPU Coverage Analysis]]&lt;br /&gt;
** [[emmtrix Link Stubber]]&lt;br /&gt;
| style=&amp;quot;vertical-align: top; padding: 10px;&amp;quot; |&lt;br /&gt;
=== Supported Architectures ===&lt;br /&gt;
* [[TriCore Instruction Set Architecture]]&lt;br /&gt;
** [[Infineon AURIX TC2xx]]&lt;br /&gt;
** [[Infineon AURIX TC3xx]]&lt;br /&gt;
** [[Infineon AURIX TC4x]], including [[Infineon AURIX TC4x Parallel Processing Unit (PPU)|Parallel Processing Unit (PPU)]]&lt;br /&gt;
* ... and many more&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
|&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Compiler Background ===&lt;br /&gt;
&lt;br /&gt;
* [[Loop Transformations]]&lt;br /&gt;
* [[:Category:Code Transformation]]&lt;br /&gt;
*[[Demystifying C++]]&lt;br /&gt;
*[[The alias Attribute|The alias attribute]]&lt;br /&gt;
*[[Clang Diagnostics Overview]]&lt;br /&gt;
*[[:Category:Clang Diagnostics]]&lt;br /&gt;
&lt;br /&gt;
=== Other Topics ===&lt;br /&gt;
* [[C to Z3 Cheat Sheet]]&lt;br /&gt;
* [[Eclipse Xcore Cheat Sheet]]&lt;br /&gt;
* [[Logical Execution Time (LET)]]&lt;br /&gt;
* [[ULP Difference of Float Numbers]]&lt;br /&gt;
* [[Numerical Precision in ONNX and AI Inference]]&lt;br /&gt;
* [[Automatic C to Rust Translation]]&lt;br /&gt;
* [[tanh Software Implementation]]&lt;/div&gt;</summary>
		<author><name>Timo.stripf</name></author>
	</entry>
	<entry>
		<id>https://www.emmtrix.com/w139/index.php?title=emmtrix_ONNX-to-C_Code_Generator&amp;diff=2923</id>
		<title>emmtrix ONNX-to-C Code Generator</title>
		<link rel="alternate" type="text/html" href="https://www.emmtrix.com/w139/index.php?title=emmtrix_ONNX-to-C_Code_Generator&amp;diff=2923"/>
		<updated>2026-02-04T07:08:03Z</updated>

		<summary type="html">&lt;p&gt;Timo.stripf: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&#039;&#039;&#039;emmtrix ONNX-to-C Code Generator&#039;&#039;&#039; (emx-onnx-cgen) is an emmtrix-developed, open-source AI frontend compiler that translates ONNX models into deterministic, analyzable C code specifically designed for auto-vectorization and embedded target optimization.&lt;br /&gt;
&lt;br /&gt;
The primary goal of emx-onnx-cgen is not to perform any hardware-specific optimizations itself, but to generate high-quality C code that serves as an ideal input for the [[emmtrix Code Vectorizer|emmtrix Vectorizer]] and subsequent backend toolchains targeting embedded architectures.&lt;br /&gt;
&lt;br /&gt;
emx-onnx-cgen is particularly suited for safety-critical, resource-constrained, and bare-metal environments where transparency, determinism, and static analyzability are mandatory.&lt;br /&gt;
==Motivation==&lt;br /&gt;
Typical AI deployment solutions rely on runtime engines or architecture-specific code generators that:&lt;br /&gt;
*hide control flow and memory access patterns,&lt;br /&gt;
*rely on dynamic memory allocation,&lt;br /&gt;
*limit static analysis and certification,&lt;br /&gt;
*restrict the effectiveness of auto-vectorization.&lt;br /&gt;
emx-onnx-cgen follows a different approach:&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;ONNX → Clean C → Vectorizer → Target Architecture&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
By lowering AI models into a well-structured and explicit C representation, emx-onnx-cgen enables advanced compiler analyses and vectorization passes that would otherwise be impossible or unreliable.&lt;br /&gt;
==Role in the emmtrix Toolchain==&lt;br /&gt;
emx-onnx-cgen acts as the &#039;&#039;&#039;frontend&#039;&#039;&#039; in the emmtrix AI compilation pipeline.&lt;br /&gt;
;Pipeline Overview&lt;br /&gt;
*&#039;&#039;&#039;ONNX Model&#039;&#039;&#039;Imported neural network description&lt;br /&gt;
&lt;br /&gt;
*&#039;&#039;&#039;emx-onnx-cgen&#039;&#039;&#039;Normalization, lowering, and generation of vectorization-friendly C code&lt;br /&gt;
&lt;br /&gt;
*&#039;&#039;&#039;emmtrix Vectorizer&#039;&#039;&#039;Loop analysis, data-flow analysis, auto-vectorization&lt;br /&gt;
&lt;br /&gt;
*&#039;&#039;&#039;Backend / Toolchain&#039;&#039;&#039;Architecture-specific code generation (DSP, MCU, SoC)&lt;br /&gt;
&#039;&#039;&#039;Diagram suggestion:&#039;&#039;&#039;&amp;lt;pre&amp;gt;&lt;br /&gt;
ONNX Model&lt;br /&gt;
    |&lt;br /&gt;
    v&lt;br /&gt;
emx-onnx-cgen&lt;br /&gt;
    |&lt;br /&gt;
    v&lt;br /&gt;
Vectorization-Friendly C Code&lt;br /&gt;
    |&lt;br /&gt;
    v&lt;br /&gt;
emmtrix Vectorizer&lt;br /&gt;
    |&lt;br /&gt;
    v&lt;br /&gt;
Optimized Embedded Binary&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
==Design Goals==&lt;br /&gt;
emx-onnx-cgen is designed with the following goals:&lt;br /&gt;
*Deterministic code generation&lt;br /&gt;
*Fully static memory layout (no heap usage)&lt;br /&gt;
*Explicit loops and control flow&lt;br /&gt;
*Predictable memory access patterns&lt;br /&gt;
*Readable and auditable C code&lt;br /&gt;
*Maximal compatibility with auto-vectorization tools&lt;br /&gt;
*Suitability for embedded and safety-critical systems&lt;br /&gt;
==Non-Goals==&lt;br /&gt;
emx-onnx-cgen explicitly does &#039;&#039;&#039;not&#039;&#039;&#039; aim to:&lt;br /&gt;
*Act as a runtime inference engine&lt;br /&gt;
*Perform hand-written SIMD or target-specific optimizations&lt;br /&gt;
*Hide model execution behind opaque APIs&lt;br /&gt;
*Support training or backpropagation&lt;br /&gt;
==Code Generation Principles==&lt;br /&gt;
The generated C code follows strict structural rules to support static analysis and vectorization:&lt;br /&gt;
*Simple, canonical loop forms&lt;br /&gt;
*Linear array accesses&lt;br /&gt;
*No hidden pointer aliasing&lt;br /&gt;
*No dynamic dispatch&lt;br /&gt;
*No recursion&lt;br /&gt;
*No dynamic memory allocation&lt;br /&gt;
*Explicit tensor dimensions and strides&lt;br /&gt;
These properties allow the emmtrix Vectorizer to reliably detect vectorization opportunities and apply architecture-specific optimizations.&lt;br /&gt;
==Example==&lt;br /&gt;
===High-Level Operation===&lt;br /&gt;
An ONNX operator such as a vector addition is lowered into a simple C loop:&amp;lt;pre&amp;gt;&lt;br /&gt;
for (i = 0; i &amp;lt; N; ++i) {&lt;br /&gt;
    output[i] = input_a[i] + input_b[i];&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;This structure is intentionally chosen to be:&lt;br /&gt;
*easy to analyze,&lt;br /&gt;
*free of side effects,&lt;br /&gt;
*ideal for SIMD vectorization by downstream tools.&lt;br /&gt;
==Embedded Target Suitability==&lt;br /&gt;
emx-onnx-cgen is suitable for:&lt;br /&gt;
*Bare-metal systems&lt;br /&gt;
*RTOS-based systems&lt;br /&gt;
*Automotive ECUs&lt;br /&gt;
*Industrial controllers&lt;br /&gt;
*DSP-based platforms&lt;br /&gt;
The generated code depends only on standard C headers and can be integrated into existing embedded build systems without requiring a runtime framework.&lt;br /&gt;
==Verification and Determinism==&lt;br /&gt;
To ensure correctness, emx-onnx-cgen supports verification against reference ONNX execution.&lt;br /&gt;
&lt;br /&gt;
Key properties:&lt;br /&gt;
*Bit-stable code generation&lt;br /&gt;
*Reproducible builds&lt;br /&gt;
*Deterministic execution behavior&lt;br /&gt;
These properties are essential for certification, validation, and long-term maintenance of embedded AI software.&lt;br /&gt;
==Related Pages==&lt;br /&gt;
*[[emmtrix Code Vectorizer]]&lt;br /&gt;
*[[Numerical Precision in ONNX and AI Inference]]&lt;br /&gt;
==See Also==&lt;br /&gt;
*emx-onnx-cgen GitHub Repository - https://github.com/emmtrix/emx-onnx-cgen/&lt;br /&gt;
[[Category:emmtrix Tools]]&lt;/div&gt;</summary>
		<author><name>Timo.stripf</name></author>
	</entry>
	<entry>
		<id>https://www.emmtrix.com/w139/index.php?title=emmtrix_Performance_Estimator&amp;diff=2922</id>
		<title>emmtrix Performance Estimator</title>
		<link rel="alternate" type="text/html" href="https://www.emmtrix.com/w139/index.php?title=emmtrix_Performance_Estimator&amp;diff=2922"/>
		<updated>2026-02-04T02:21:41Z</updated>

		<summary type="html">&lt;p&gt;Timo.stripf: /* Estimation Methods */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;The [https://www.emmtrix.com/tools/emmtrix-performance-estimator &#039;&#039;&#039;emmtrix Performance Estimator (ePE)&#039;&#039;&#039;] is a static analysis tool for estimating the execution time of embedded C code. It determines timing characteristics of a software system without executing the code on the target hardware.&lt;br /&gt;
&lt;br /&gt;
Unlike measurement-based approaches, which rely on instrumented code, simulators, or real hardware, ePE performs a purely static analysis. The estimation is based on the program structure, control flow, and target-specific execution models. As a result, performance characteristics can be assessed early in the development process, even when the final hardware platform is not yet available.&lt;br /&gt;
&lt;br /&gt;
The primary goal of ePE is to provide reproducible and analyzable execution time estimates, including minimum, maximum, and average values. These estimates can be evaluated on different levels of granularity, ranging from individual functions up to complete applications.&lt;br /&gt;
&lt;br /&gt;
ePE is designed for embedded software development where timing behavior is a critical system property. Typical application domains include real-time and safety-critical systems, such as automotive ECUs and industrial control units. In particular, ePE is commonly used in projects targeting Infineon microcontroller families, including the &#039;&#039;&#039;Infineon&#039;s AURIX™ [[Infineon AURIX TC2xx|TC2xx]] / [[Infineon AURIX TC3xx|TC3xx]]/ [[Infineon AURIX TC4x|TC4x]] microcontroller family&#039;&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
The tool supports different estimation techniques with varying levels of accuracy, ranging from source-code-based analysis to assembly-level estimation with microarchitectural modeling. For supported Infineon architectures, this includes processor-specific timing models that reflect relevant architectural features.&lt;br /&gt;
&lt;br /&gt;
In addition, ePE integrates into automated build and verification workflows and can be combined with model-based development environments. This enables continuous performance assessment and early detection of timing regressions during software evolution.&lt;br /&gt;
==Problem Statement and Motivation==&lt;br /&gt;
In embedded and real-time software systems, execution time is a critical non-functional property. Tasks and functions must meet strict timing constraints in order to guarantee system correctness, schedulability, and safety. Violations of these constraints may result in missed deadlines, degraded system behavior, or safety hazards.&lt;br /&gt;
&lt;br /&gt;
[[File:DefectDetectionandRemediation.webp|524x524px|Cost of defect remediation across the software development life cycle|alt=Cost of defect remediation across the software development life cycle|thumb]]&lt;br /&gt;
&lt;br /&gt;
Traditional performance evaluation techniques are predominantly measurement-based. They rely on executing instrumented software on simulators, hardware-in-the-loop (HIL) setups, or real target hardware. While such approaches can provide accurate measurements, they also introduce several practical limitations:&lt;br /&gt;
*Target hardware is often unavailable or not yet representative in early development phases.&lt;br /&gt;
*Measurements are influenced by instrumentation overhead and runtime environment effects.&lt;br /&gt;
* Results may vary across toolchains, compiler versions, or hardware revisions.&lt;br /&gt;
*Performance issues are typically detected late in the software development life cycle.&lt;br /&gt;
Early development phases, however, require performance feedback at a time when software architecture, algorithms, and scheduling concepts are still subject to change. At this stage, late detection of execution time problems can lead to costly redesigns, as architectural modifications become increasingly difficult once system integration has progressed.&lt;br /&gt;
&lt;br /&gt;
This general principle is well known from software quality engineering. Defects are typically introduced early during development, but often detected much later, while the effort required to correct them increases significantly over time. Although originally formulated for functional defects, this observation applies analogously to timing- and performance-related issues in embedded systems. Late detection of timing violations frequently requires invasive changes, such as restructuring software components, modifying scheduling strategies, or adapting hardware configurations. In contrast, early performance estimation enables developers to assess timing risks at a point where corrective actions are comparatively inexpensive.&lt;br /&gt;
&lt;br /&gt;
Static execution time estimation addresses this challenge by analyzing software without executing it. By deriving execution frequencies from the program structure and combining them with target-specific execution models, performance characteristics can be estimated deterministically and reproducibly. This allows systematic performance assessment early in the development process and supports continuous evaluation as the software evolves.&lt;br /&gt;
&lt;br /&gt;
The emmtrix Performance Estimator (ePE) is designed to support this approach. It provides a static, analyzable, and reproducible method for estimating execution time, complementing measurement-based techniques used in later development stages and enabling early detection of timing-related risks.&lt;br /&gt;
&lt;br /&gt;
==Fundamental Estimation Model==&lt;br /&gt;
The emmtrix Performance Estimator (ePE) is based on a static execution time model that decomposes the overall runtime of a software system into two orthogonal components:&lt;br /&gt;
*the &#039;&#039;&#039;execution frequency&#039;&#039;&#039; of a code block, and&lt;br /&gt;
*the &#039;&#039;&#039;execution duration&#039;&#039;&#039; of a single execution of that block.&lt;br /&gt;
For a given code block &amp;lt;math&amp;gt;b&amp;lt;/math&amp;gt; (e.g. a basic block, function, or task), the estimated execution time &amp;lt;math&amp;gt;t_{exec}(b)&amp;lt;/math&amp;gt; is defined as:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;big&amp;gt;&amp;lt;math&amp;gt;&lt;br /&gt;
t_{exec}(b) = f(b) \cdot d(b)&lt;br /&gt;
&amp;lt;/math&amp;gt;&amp;lt;/big&amp;gt;&lt;br /&gt;
&lt;br /&gt;
where:&lt;br /&gt;
*&amp;lt;math&amp;gt;f(b)&amp;lt;/math&amp;gt; denotes the execution frequency of block &amp;lt;math&amp;gt;b&amp;lt;/math&amp;gt;, i.e. how often the block is executed during one invocation of the surrounding context (task, function, or application),&lt;br /&gt;
* &amp;lt;math&amp;gt;d(b)&amp;lt;/math&amp;gt; denotes the estimated execution duration of a single execution of block &amp;lt;math&amp;gt;b&amp;lt;/math&amp;gt;.&lt;br /&gt;
The overall execution time of a larger software entity is obtained by aggregating the execution times of its constituent blocks.&lt;br /&gt;
===Execution Frequency===&lt;br /&gt;
The execution frequency &amp;lt;math&amp;gt;f(b)&amp;lt;/math&amp;gt; is derived statically from the program structure and control flow. ePE analyzes the control flow graph (CFG) of the application and determines execution frequencies based on:&lt;br /&gt;
*loop bounds and iteration counts,&lt;br /&gt;
*conditional branches,&lt;br /&gt;
*call relationships between functions,&lt;br /&gt;
*structural properties of the control flow.&lt;br /&gt;
Whenever possible, loop bounds and conditions are evaluated using constant folding and static evaluation. This enables deterministic frequency estimates without executing the code. For blocks that are executed conditionally, frequency ranges or bounds may be derived depending on the available information.&lt;br /&gt;
&lt;br /&gt;
Execution frequencies form the structural part of the estimation model and are independent of the target architecture.&lt;br /&gt;
===Execution Duration ===&lt;br /&gt;
The execution duration &amp;lt;math&amp;gt;d(b)&amp;lt;/math&amp;gt; represents the time required to execute block &amp;lt;math&amp;gt;b&amp;lt;/math&amp;gt; once on a given target architecture. ePE determines this duration using target-specific execution models.&lt;br /&gt;
&lt;br /&gt;
At the most basic level, the duration is computed by mapping program instructions to abstract timing costs (e.g. processor cycles) and summing these costs over the block. More advanced estimation methods additionally consider:&lt;br /&gt;
*compiler optimizations,&lt;br /&gt;
*instruction scheduling,&lt;br /&gt;
*pipeline effects and microarchitectural behavior.&lt;br /&gt;
The level of detail used for duration estimation directly influences the accuracy of the result and depends on the selected estimation technique.&lt;br /&gt;
===Aggregation and Granularity===&lt;br /&gt;
The estimation model is compositional. Execution time estimates can be aggregated across different levels of granularity, including:&lt;br /&gt;
*individual basic blocks,&lt;br /&gt;
*functions,&lt;br /&gt;
*tasks or runnables,&lt;br /&gt;
*complete applications.&lt;br /&gt;
This enables performance analysis and comparison at multiple abstraction levels, supporting both detailed investigations and high-level assessments.&lt;br /&gt;
===Determinism and Reproducibility===&lt;br /&gt;
Since both execution frequencies and execution durations are derived statically, the estimation results produced by ePE are deterministic and reproducible. Given identical source code, configuration, and target model, repeated analyses yield identical results. This property is essential for systematic performance evaluation, regression analysis, and integration into automated build and verification workflows.&lt;br /&gt;
=== Estimation Methods ===&lt;br /&gt;
In general, the execution time of a task or block can be modelled as:&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;t&amp;lt;sub&amp;gt;exec&amp;lt;/sub&amp;gt; = execution_frequency&amp;lt;sub&amp;gt;block&amp;lt;/sub&amp;gt;* single_duration&amp;lt;sub&amp;gt;block&amp;lt;/sub&amp;gt;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
The C code based static code analysis derives the execution frequency by analyzing loop boundaries using constant folding. For the duration, each instruction in the code is modeled as the number of cycles of an abstract hardware model of the processor, and then summed.&lt;br /&gt;
&lt;br /&gt;
ePE offers three accuracy levels:&lt;br /&gt;
*analysis of C code&lt;br /&gt;
*generically compiler-optimized code&lt;br /&gt;
*assembly code from the target compiler.&lt;br /&gt;
Method 1 yields results with minimum effort while method 3 takes the timing of the processor pipeline into account. All methods offer excellent reliability when tracking the tendency of changes in software runtimes e.g. when used in a continuous integration environment.&lt;br /&gt;
&lt;br /&gt;
==== Static Code Analysis: ====&lt;br /&gt;
[[File:Static Code Analysis.jpg|frameless|800x800px]]&lt;br /&gt;
&lt;br /&gt;
==== Processor Pipeline: ====&lt;br /&gt;
[[File:ePE Processor Pipeline.png|frameless|950x950px]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
[https://www.emmtrix.com/tools/emmtrix-performance-estimator emmtrix Performance Estimator (ePE)] supports different ways to acquire the duration of the tasks of an application. These methods vary in accuracy and additional software or hardware requirements. Static code analysis provides basic information without the need for hardware or special software and may thus be seamlessly incorporated into early stages of the Software Development Life Cycle (SDLC). More accurate numbers can be collected with interfaces to simulators or the hardware. Depending on the requirements, the methods can be combined as desired.&lt;br /&gt;
&lt;br /&gt;
Addressing performance issues at the onset of the SDLC is not only proactive but also cost-efficient. Early estimation and intervention, as suggested by the below graph, can drastically reduce the complexities and expenses associated with fixing defects post-deployment. Although early estimates may not be as precise as measurements taken during later stages, they serve as a crucial checkpoint to prevent potential timing issues from escalating. This preemptive approach aligns with the principle that the cost to repair defects increases exponentially as they progress through the SDLC, as shown by the sharp rise in cost depicted in the graph. Hence, initial performance estimations are a strategic investment, minimizing the risk of facing a steep climb in remediation costs and efforts at later stages.&lt;br /&gt;
&lt;br /&gt;
== Interested? ==&lt;br /&gt;
{{CallToAction|text=Interested in applying this coverage workflow to your own projects?}}&lt;br /&gt;
[[Category:emmtrix Tools]]&lt;/div&gt;</summary>
		<author><name>Timo.stripf</name></author>
	</entry>
	<entry>
		<id>https://www.emmtrix.com/w139/index.php?title=emmtrix_Performance_Estimator&amp;diff=2921</id>
		<title>emmtrix Performance Estimator</title>
		<link rel="alternate" type="text/html" href="https://www.emmtrix.com/w139/index.php?title=emmtrix_Performance_Estimator&amp;diff=2921"/>
		<updated>2026-02-04T02:03:52Z</updated>

		<summary type="html">&lt;p&gt;Timo.stripf: /* Problem Statement and Motivation */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;The [https://www.emmtrix.com/tools/emmtrix-performance-estimator &#039;&#039;&#039;emmtrix Performance Estimator (ePE)&#039;&#039;&#039;] is a static analysis tool for estimating the execution time of embedded C code. It determines timing characteristics of a software system without executing the code on the target hardware.&lt;br /&gt;
&lt;br /&gt;
Unlike measurement-based approaches, which rely on instrumented code, simulators, or real hardware, ePE performs a purely static analysis. The estimation is based on the program structure, control flow, and target-specific execution models. As a result, performance characteristics can be assessed early in the development process, even when the final hardware platform is not yet available.&lt;br /&gt;
&lt;br /&gt;
The primary goal of ePE is to provide reproducible and analyzable execution time estimates, including minimum, maximum, and average values. These estimates can be evaluated on different levels of granularity, ranging from individual functions up to complete applications.&lt;br /&gt;
&lt;br /&gt;
ePE is designed for embedded software development where timing behavior is a critical system property. Typical application domains include real-time and safety-critical systems, such as automotive ECUs and industrial control units. In particular, ePE is commonly used in projects targeting Infineon microcontroller families, including the &#039;&#039;&#039;Infineon&#039;s AURIX™ [[Infineon AURIX TC2xx|TC2xx]] / [[Infineon AURIX TC3xx|TC3xx]]/ [[Infineon AURIX TC4x|TC4x]] microcontroller family&#039;&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
The tool supports different estimation techniques with varying levels of accuracy, ranging from source-code-based analysis to assembly-level estimation with microarchitectural modeling. For supported Infineon architectures, this includes processor-specific timing models that reflect relevant architectural features.&lt;br /&gt;
&lt;br /&gt;
In addition, ePE integrates into automated build and verification workflows and can be combined with model-based development environments. This enables continuous performance assessment and early detection of timing regressions during software evolution.&lt;br /&gt;
==Problem Statement and Motivation==&lt;br /&gt;
In embedded and real-time software systems, execution time is a critical non-functional property. Tasks and functions must meet strict timing constraints in order to guarantee system correctness, schedulability, and safety. Violations of these constraints may result in missed deadlines, degraded system behavior, or safety hazards.&lt;br /&gt;
&lt;br /&gt;
[[File:DefectDetectionandRemediation.webp|524x524px|Cost of defect remediation across the software development life cycle|alt=Cost of defect remediation across the software development life cycle|thumb]]&lt;br /&gt;
&lt;br /&gt;
Traditional performance evaluation techniques are predominantly measurement-based. They rely on executing instrumented software on simulators, hardware-in-the-loop (HIL) setups, or real target hardware. While such approaches can provide accurate measurements, they also introduce several practical limitations:&lt;br /&gt;
*Target hardware is often unavailable or not yet representative in early development phases.&lt;br /&gt;
*Measurements are influenced by instrumentation overhead and runtime environment effects.&lt;br /&gt;
* Results may vary across toolchains, compiler versions, or hardware revisions.&lt;br /&gt;
*Performance issues are typically detected late in the software development life cycle.&lt;br /&gt;
Early development phases, however, require performance feedback at a time when software architecture, algorithms, and scheduling concepts are still subject to change. At this stage, late detection of execution time problems can lead to costly redesigns, as architectural modifications become increasingly difficult once system integration has progressed.&lt;br /&gt;
&lt;br /&gt;
This general principle is well known from software quality engineering. Defects are typically introduced early during development, but often detected much later, while the effort required to correct them increases significantly over time. Although originally formulated for functional defects, this observation applies analogously to timing- and performance-related issues in embedded systems. Late detection of timing violations frequently requires invasive changes, such as restructuring software components, modifying scheduling strategies, or adapting hardware configurations. In contrast, early performance estimation enables developers to assess timing risks at a point where corrective actions are comparatively inexpensive.&lt;br /&gt;
&lt;br /&gt;
Static execution time estimation addresses this challenge by analyzing software without executing it. By deriving execution frequencies from the program structure and combining them with target-specific execution models, performance characteristics can be estimated deterministically and reproducibly. This allows systematic performance assessment early in the development process and supports continuous evaluation as the software evolves.&lt;br /&gt;
&lt;br /&gt;
The emmtrix Performance Estimator (ePE) is designed to support this approach. It provides a static, analyzable, and reproducible method for estimating execution time, complementing measurement-based techniques used in later development stages and enabling early detection of timing-related risks.&lt;br /&gt;
=== Estimation Methods ===&lt;br /&gt;
In general, the execution time of a task or block can be modelled as:&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;t&amp;lt;sub&amp;gt;exec&amp;lt;/sub&amp;gt; = execution_frequency&amp;lt;sub&amp;gt;block&amp;lt;/sub&amp;gt;* single_duration&amp;lt;sub&amp;gt;block&amp;lt;/sub&amp;gt;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
The C code based static code analysis derives the execution frequency by analyzing loop boundaries using constant folding. For the duration, each instruction in the code is modeled as the number of cycles of an abstract hardware model of the processor, and then summed.&lt;br /&gt;
&lt;br /&gt;
ePE offers three accuracy levels:&lt;br /&gt;
*analysis of C code&lt;br /&gt;
*generically compiler-optimized code&lt;br /&gt;
*assembly code from the target compiler.&lt;br /&gt;
Method 1 yields results with minimum effort while method 3 takes the timing of the processor pipeline into account. All methods offer excellent reliability when tracking the tendency of changes in software runtimes e.g. when used in a continuous integration environment.&lt;br /&gt;
&lt;br /&gt;
==== Static Code Analysis: ====&lt;br /&gt;
[[File:Static Code Analysis.jpg|frameless|800x800px]]&lt;br /&gt;
&lt;br /&gt;
==== Processor Pipeline: ====&lt;br /&gt;
[[File:ePE Processor Pipeline.png|frameless|950x950px]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
[https://www.emmtrix.com/tools/emmtrix-performance-estimator emmtrix Performance Estimator (ePE)] supports different ways to acquire the duration of the tasks of an application. These methods vary in accuracy and additional software or hardware requirements. Static code analysis provides basic information without the need for hardware or special software and may thus be seamlessly incorporated into early stages of the Software Development Life Cycle (SDLC). More accurate numbers can be collected with interfaces to simulators or the hardware. Depending on the requirements, the methods can be combined as desired.&lt;br /&gt;
&lt;br /&gt;
Addressing performance issues at the onset of the SDLC is not only proactive but also cost-efficient. Early estimation and intervention, as suggested by the below graph, can drastically reduce the complexities and expenses associated with fixing defects post-deployment. Although early estimates may not be as precise as measurements taken during later stages, they serve as a crucial checkpoint to prevent potential timing issues from escalating. This preemptive approach aligns with the principle that the cost to repair defects increases exponentially as they progress through the SDLC, as shown by the sharp rise in cost depicted in the graph. Hence, initial performance estimations are a strategic investment, minimizing the risk of facing a steep climb in remediation costs and efforts at later stages.&lt;br /&gt;
&lt;br /&gt;
== Interested? ==&lt;br /&gt;
{{CallToAction|text=Interested in applying this coverage workflow to your own projects?}}&lt;br /&gt;
[[Category:emmtrix Tools]]&lt;/div&gt;</summary>
		<author><name>Timo.stripf</name></author>
	</entry>
	<entry>
		<id>https://www.emmtrix.com/w139/index.php?title=emmtrix_Performance_Estimator&amp;diff=2920</id>
		<title>emmtrix Performance Estimator</title>
		<link rel="alternate" type="text/html" href="https://www.emmtrix.com/w139/index.php?title=emmtrix_Performance_Estimator&amp;diff=2920"/>
		<updated>2026-02-04T02:00:52Z</updated>

		<summary type="html">&lt;p&gt;Timo.stripf: /* Estimation Methods */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;The [https://www.emmtrix.com/tools/emmtrix-performance-estimator &#039;&#039;&#039;emmtrix Performance Estimator (ePE)&#039;&#039;&#039;] is a static analysis tool for estimating the execution time of embedded C code. It determines timing characteristics of a software system without executing the code on the target hardware.&lt;br /&gt;
&lt;br /&gt;
Unlike measurement-based approaches, which rely on instrumented code, simulators, or real hardware, ePE performs a purely static analysis. The estimation is based on the program structure, control flow, and target-specific execution models. As a result, performance characteristics can be assessed early in the development process, even when the final hardware platform is not yet available.&lt;br /&gt;
&lt;br /&gt;
The primary goal of ePE is to provide reproducible and analyzable execution time estimates, including minimum, maximum, and average values. These estimates can be evaluated on different levels of granularity, ranging from individual functions up to complete applications.&lt;br /&gt;
&lt;br /&gt;
ePE is designed for embedded software development where timing behavior is a critical system property. Typical application domains include real-time and safety-critical systems, such as automotive ECUs and industrial control units. In particular, ePE is commonly used in projects targeting Infineon microcontroller families, including the &#039;&#039;&#039;Infineon&#039;s AURIX™ [[Infineon AURIX TC2xx|TC2xx]] / [[Infineon AURIX TC3xx|TC3xx]]/ [[Infineon AURIX TC4x|TC4x]] microcontroller family&#039;&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
The tool supports different estimation techniques with varying levels of accuracy, ranging from source-code-based analysis to assembly-level estimation with microarchitectural modeling. For supported Infineon architectures, this includes processor-specific timing models that reflect relevant architectural features.&lt;br /&gt;
&lt;br /&gt;
In addition, ePE integrates into automated build and verification workflows and can be combined with model-based development environments. This enables continuous performance assessment and early detection of timing regressions during software evolution.&lt;br /&gt;
==Problem Statement and Motivation==&lt;br /&gt;
In embedded and real-time software systems, execution time is a critical non-functional property. Tasks and functions must meet strict timing constraints in order to guarantee system correctness, schedulability, and safety. Violations of these constraints may result in missed deadlines, degraded system behavior, or safety hazards.&lt;br /&gt;
&lt;br /&gt;
Traditional performance evaluation techniques are predominantly measurement-based. They rely on executing instrumented software on simulators, hardware-in-the-loop (HIL) setups, or real target hardware. While such approaches can provide accurate measurements, they also introduce several practical limitations:&lt;br /&gt;
*Target hardware is often unavailable or not yet representative in early development phases.&lt;br /&gt;
*Measurements are influenced by instrumentation overhead and runtime environment effects.&lt;br /&gt;
* Results may vary across toolchains, compiler versions, or hardware revisions.&lt;br /&gt;
*Performance issues are typically detected late in the software development life cycle.&lt;br /&gt;
Early development phases, however, require performance feedback at a time when software architecture, algorithms, and scheduling concepts are still subject to change. At this stage, late detection of execution time problems can lead to costly redesigns, as architectural modifications become increasingly difficult once system integration has progressed.&lt;br /&gt;
&lt;br /&gt;
This general principle is well known from software quality engineering. Defects are typically introduced early during development, but often detected much later, while the effort required to correct them increases significantly over time. Although originally formulated for functional defects, this observation applies analogously to timing- and performance-related issues in embedded systems.[[File:DefectDetectionandRemediation.webp|524x524px|Cost of defect remediation across the software development life cycle|alt=Cost of defect remediation across the software development life cycle|thumb]]Late detection of timing violations frequently requires invasive changes, such as restructuring software components, modifying scheduling strategies, or adapting hardware configurations. In contrast, early performance estimation enables developers to assess timing risks at a point where corrective actions are comparatively inexpensive.&lt;br /&gt;
&lt;br /&gt;
Static execution time estimation addresses this challenge by analyzing software without executing it. By deriving execution frequencies from the program structure and combining them with target-specific execution models, performance characteristics can be estimated deterministically and reproducibly. This allows systematic performance assessment early in the development process and supports continuous evaluation as the software evolves.&lt;br /&gt;
&lt;br /&gt;
The emmtrix Performance Estimator (ePE) is designed to support this approach. It provides a static, analyzable, and reproducible method for estimating execution time, complementing measurement-based techniques used in later development stages and enabling early detection of timing-related risks.&lt;br /&gt;
=== Estimation Methods ===&lt;br /&gt;
In general, the execution time of a task or block can be modelled as:&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;t&amp;lt;sub&amp;gt;exec&amp;lt;/sub&amp;gt; = execution_frequency&amp;lt;sub&amp;gt;block&amp;lt;/sub&amp;gt;* single_duration&amp;lt;sub&amp;gt;block&amp;lt;/sub&amp;gt;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
The C code based static code analysis derives the execution frequency by analyzing loop boundaries using constant folding. For the duration, each instruction in the code is modeled as the number of cycles of an abstract hardware model of the processor, and then summed.&lt;br /&gt;
&lt;br /&gt;
ePE offers three accuracy levels:&lt;br /&gt;
*analysis of C code&lt;br /&gt;
*generically compiler-optimized code&lt;br /&gt;
*assembly code from the target compiler.&lt;br /&gt;
Method 1 yields results with minimum effort while method 3 takes the timing of the processor pipeline into account. All methods offer excellent reliability when tracking the tendency of changes in software runtimes e.g. when used in a continuous integration environment.&lt;br /&gt;
&lt;br /&gt;
==== Static Code Analysis: ====&lt;br /&gt;
[[File:Static Code Analysis.jpg|frameless|800x800px]]&lt;br /&gt;
&lt;br /&gt;
==== Processor Pipeline: ====&lt;br /&gt;
[[File:ePE Processor Pipeline.png|frameless|950x950px]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
[https://www.emmtrix.com/tools/emmtrix-performance-estimator emmtrix Performance Estimator (ePE)] supports different ways to acquire the duration of the tasks of an application. These methods vary in accuracy and additional software or hardware requirements. Static code analysis provides basic information without the need for hardware or special software and may thus be seamlessly incorporated into early stages of the Software Development Life Cycle (SDLC). More accurate numbers can be collected with interfaces to simulators or the hardware. Depending on the requirements, the methods can be combined as desired.&lt;br /&gt;
&lt;br /&gt;
Addressing performance issues at the onset of the SDLC is not only proactive but also cost-efficient. Early estimation and intervention, as suggested by the below graph, can drastically reduce the complexities and expenses associated with fixing defects post-deployment. Although early estimates may not be as precise as measurements taken during later stages, they serve as a crucial checkpoint to prevent potential timing issues from escalating. This preemptive approach aligns with the principle that the cost to repair defects increases exponentially as they progress through the SDLC, as shown by the sharp rise in cost depicted in the graph. Hence, initial performance estimations are a strategic investment, minimizing the risk of facing a steep climb in remediation costs and efforts at later stages.&lt;br /&gt;
[[File:DefectDetectionandRemediation.webp|center|frameless|800x800px|Cost of Defect Remediation across the Software Development Life Cycle]]&lt;br /&gt;
&lt;br /&gt;
== Interested? ==&lt;br /&gt;
{{CallToAction|text=Interested in applying this coverage workflow to your own projects?}}&lt;br /&gt;
[[Category:emmtrix Tools]]&lt;/div&gt;</summary>
		<author><name>Timo.stripf</name></author>
	</entry>
	<entry>
		<id>https://www.emmtrix.com/w139/index.php?title=emmtrix_Performance_Estimator&amp;diff=2919</id>
		<title>emmtrix Performance Estimator</title>
		<link rel="alternate" type="text/html" href="https://www.emmtrix.com/w139/index.php?title=emmtrix_Performance_Estimator&amp;diff=2919"/>
		<updated>2026-02-04T01:50:34Z</updated>

		<summary type="html">&lt;p&gt;Timo.stripf: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;The [https://www.emmtrix.com/tools/emmtrix-performance-estimator &#039;&#039;&#039;emmtrix Performance Estimator (ePE)&#039;&#039;&#039;] is a static analysis tool for estimating the execution time of embedded C code. It determines timing characteristics of a software system without executing the code on the target hardware.&lt;br /&gt;
&lt;br /&gt;
Unlike measurement-based approaches, which rely on instrumented code, simulators, or real hardware, ePE performs a purely static analysis. The estimation is based on the program structure, control flow, and target-specific execution models. As a result, performance characteristics can be assessed early in the development process, even when the final hardware platform is not yet available.&lt;br /&gt;
&lt;br /&gt;
The primary goal of ePE is to provide reproducible and analyzable execution time estimates, including minimum, maximum, and average values. These estimates can be evaluated on different levels of granularity, ranging from individual functions up to complete applications.&lt;br /&gt;
&lt;br /&gt;
ePE is designed for embedded software development where timing behavior is a critical system property. Typical application domains include real-time and safety-critical systems, such as automotive ECUs and industrial control units. In particular, ePE is commonly used in projects targeting Infineon microcontroller families, including the &#039;&#039;&#039;Infineon&#039;s AURIX™ [[Infineon AURIX TC2xx|TC2xx]] / [[Infineon AURIX TC3xx|TC3xx]]/ [[Infineon AURIX TC4x|TC4x]] microcontroller family&#039;&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
The tool supports different estimation techniques with varying levels of accuracy, ranging from source-code-based analysis to assembly-level estimation with microarchitectural modeling. For supported Infineon architectures, this includes processor-specific timing models that reflect relevant architectural features.&lt;br /&gt;
&lt;br /&gt;
In addition, ePE integrates into automated build and verification workflows and can be combined with model-based development environments. This enables continuous performance assessment and early detection of timing regressions during software evolution.&lt;br /&gt;
=== Estimation Methods ===&lt;br /&gt;
In general, the execution time of a task or block can be modelled as:&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;t&amp;lt;sub&amp;gt;exec&amp;lt;/sub&amp;gt; = execution_frequency&amp;lt;sub&amp;gt;block&amp;lt;/sub&amp;gt;* single_duration&amp;lt;sub&amp;gt;block&amp;lt;/sub&amp;gt;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
The C code based static code analysis derives the execution frequency by analyzing loop boundaries using constant folding. For the duration, each instruction in the code is modeled as the number of cycles of an abstract hardware model of the processor, and then summed.&lt;br /&gt;
&lt;br /&gt;
ePE offers three accuracy levels:&lt;br /&gt;
*analysis of C code&lt;br /&gt;
*generically compiler-optimized code&lt;br /&gt;
*assembly code from the target compiler.&lt;br /&gt;
Method 1 yields results with minimum effort while method 3 takes the timing of the processor pipeline into account. All methods offer excellent reliability when tracking the tendency of changes in software runtimes e.g. when used in a continuous integration environment.&lt;br /&gt;
&lt;br /&gt;
==== Static Code Analysis: ====&lt;br /&gt;
[[File:Static Code Analysis.jpg|frameless|800x800px]]&lt;br /&gt;
&lt;br /&gt;
==== Processor Pipeline: ====&lt;br /&gt;
[[File:ePE Processor Pipeline.png|frameless|950x950px]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
[https://www.emmtrix.com/tools/emmtrix-performance-estimator emmtrix Performance Estimator (ePE)] supports different ways to acquire the duration of the tasks of an application. These methods vary in accuracy and additional software or hardware requirements. Static code analysis provides basic information without the need for hardware or special software and may thus be seamlessly incorporated into early stages of the Software Development Life Cycle (SDLC). More accurate numbers can be collected with interfaces to simulators or the hardware. Depending on the requirements, the methods can be combined as desired.&lt;br /&gt;
&lt;br /&gt;
Addressing performance issues at the onset of the SDLC is not only proactive but also cost-efficient. Early estimation and intervention, as suggested by the below graph, can drastically reduce the complexities and expenses associated with fixing defects post-deployment. Although early estimates may not be as precise as measurements taken during later stages, they serve as a crucial checkpoint to prevent potential timing issues from escalating. This preemptive approach aligns with the principle that the cost to repair defects increases exponentially as they progress through the SDLC, as shown by the sharp rise in cost depicted in the graph. Hence, initial performance estimations are a strategic investment, minimizing the risk of facing a steep climb in remediation costs and efforts at later stages.&lt;br /&gt;
[[File:DefectDetectionandRemediation.webp|center|frameless|800x800px|Cost of Defect Remediation across the Software Development Life Cycle]]&lt;br /&gt;
&lt;br /&gt;
== Interested? ==&lt;br /&gt;
{{CallToAction|text=Interested in applying this coverage workflow to your own projects?}}&lt;br /&gt;
[[Category:emmtrix Tools]]&lt;/div&gt;</summary>
		<author><name>Timo.stripf</name></author>
	</entry>
	<entry>
		<id>https://www.emmtrix.com/w139/index.php?title=emmtrix_ONNX-to-C_Code_Generator&amp;diff=2918</id>
		<title>emmtrix ONNX-to-C Code Generator</title>
		<link rel="alternate" type="text/html" href="https://www.emmtrix.com/w139/index.php?title=emmtrix_ONNX-to-C_Code_Generator&amp;diff=2918"/>
		<updated>2026-02-04T01:19:08Z</updated>

		<summary type="html">&lt;p&gt;Timo.stripf: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&#039;&#039;&#039;emmtrix ONNX-to-C Code Generator&#039;&#039;&#039; (emx-onnx-cgen) is an emmtrix-developed, open-source AI frontend compiler that translates ONNX models into deterministic, analyzable C code specifically designed for auto-vectorization and embedded target optimization.&lt;br /&gt;
&lt;br /&gt;
The primary goal of emx-onnx-cgen is not to perform aggressive hardware-specific optimizations itself, but to generate high-quality C code that serves as an ideal input for the emmtrix Vectorizer and subsequent backend toolchains targeting embedded architectures.&lt;br /&gt;
&lt;br /&gt;
emx-onnx-cgen is particularly suited for safety-critical, resource-constrained, and bare-metal environments where transparency, determinism, and static analyzability are mandatory.&lt;br /&gt;
==Motivation==&lt;br /&gt;
Typical AI deployment solutions rely on runtime engines or architecture-specific code generators that:&lt;br /&gt;
*hide control flow and memory access patterns,&lt;br /&gt;
*rely on dynamic memory allocation,&lt;br /&gt;
*limit static analysis and certification,&lt;br /&gt;
*restrict the effectiveness of auto-vectorization.&lt;br /&gt;
emx-onnx-cgen follows a different approach:&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;ONNX → Clean C → Vectorizer → Target Architecture&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
By lowering AI models into a well-structured and explicit C representation, emx-onnx-cgen enables advanced compiler analyses and vectorization passes that would otherwise be impossible or unreliable.&lt;br /&gt;
==Role in the emmtrix Toolchain==&lt;br /&gt;
emx-onnx-cgen acts as the &#039;&#039;&#039;frontend&#039;&#039;&#039; in the emmtrix AI compilation pipeline.&lt;br /&gt;
;Pipeline Overview&lt;br /&gt;
*&#039;&#039;&#039;ONNX Model&#039;&#039;&#039;Imported neural network description&lt;br /&gt;
&lt;br /&gt;
*&#039;&#039;&#039;emx-onnx-cgen&#039;&#039;&#039;Normalization, lowering, and generation of vectorization-friendly C code&lt;br /&gt;
&lt;br /&gt;
*&#039;&#039;&#039;emmtrix Vectorizer&#039;&#039;&#039;Loop analysis, data-flow analysis, auto-vectorization&lt;br /&gt;
&lt;br /&gt;
*&#039;&#039;&#039;Backend / Toolchain&#039;&#039;&#039;Architecture-specific code generation (DSP, MCU, SoC)&lt;br /&gt;
&#039;&#039;&#039;Diagram suggestion:&#039;&#039;&#039;&amp;lt;pre&amp;gt;&lt;br /&gt;
ONNX Model&lt;br /&gt;
    |&lt;br /&gt;
    v&lt;br /&gt;
emx-onnx-cgen&lt;br /&gt;
    |&lt;br /&gt;
    v&lt;br /&gt;
Vectorization-Friendly C Code&lt;br /&gt;
    |&lt;br /&gt;
    v&lt;br /&gt;
emmtrix Vectorizer&lt;br /&gt;
    |&lt;br /&gt;
    v&lt;br /&gt;
Optimized Embedded Binary&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
==Design Goals==&lt;br /&gt;
emx-onnx-cgen is designed with the following goals:&lt;br /&gt;
*Deterministic code generation&lt;br /&gt;
*Fully static memory layout (no heap usage)&lt;br /&gt;
*Explicit loops and control flow&lt;br /&gt;
*Predictable memory access patterns&lt;br /&gt;
*Readable and auditable C code&lt;br /&gt;
*Maximal compatibility with auto-vectorization tools&lt;br /&gt;
*Suitability for embedded and safety-critical systems&lt;br /&gt;
==Non-Goals==&lt;br /&gt;
emx-onnx-cgen explicitly does &#039;&#039;&#039;not&#039;&#039;&#039; aim to:&lt;br /&gt;
*Act as a runtime inference engine&lt;br /&gt;
*Perform hand-written SIMD or target-specific optimizations&lt;br /&gt;
*Hide model execution behind opaque APIs&lt;br /&gt;
*Support training or backpropagation&lt;br /&gt;
==Code Generation Principles==&lt;br /&gt;
The generated C code follows strict structural rules to support static analysis and vectorization:&lt;br /&gt;
*Simple, canonical loop forms&lt;br /&gt;
*Linear array accesses&lt;br /&gt;
*No hidden pointer aliasing&lt;br /&gt;
*No dynamic dispatch&lt;br /&gt;
*No recursion&lt;br /&gt;
*No dynamic memory allocation&lt;br /&gt;
*Explicit tensor dimensions and strides&lt;br /&gt;
These properties allow the emmtrix Vectorizer to reliably detect vectorization opportunities and apply architecture-specific optimizations.&lt;br /&gt;
==Example==&lt;br /&gt;
===High-Level Operation===&lt;br /&gt;
An ONNX operator such as a vector addition is lowered into a simple C loop:&amp;lt;pre&amp;gt;&lt;br /&gt;
for (i = 0; i &amp;lt; N; ++i) {&lt;br /&gt;
    output[i] = input_a[i] + input_b[i];&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;This structure is intentionally chosen to be:&lt;br /&gt;
*easy to analyze,&lt;br /&gt;
*free of side effects,&lt;br /&gt;
*ideal for SIMD vectorization by downstream tools.&lt;br /&gt;
==Embedded Target Suitability==&lt;br /&gt;
emx-onnx-cgen is suitable for:&lt;br /&gt;
*Bare-metal systems&lt;br /&gt;
*RTOS-based systems&lt;br /&gt;
*Automotive ECUs&lt;br /&gt;
*Industrial controllers&lt;br /&gt;
*DSP-based platforms&lt;br /&gt;
The generated code depends only on standard C headers and can be integrated into existing embedded build systems without requiring a runtime framework.&lt;br /&gt;
==Verification and Determinism==&lt;br /&gt;
To ensure correctness, emx-onnx-cgen supports verification against reference ONNX execution.&lt;br /&gt;
&lt;br /&gt;
Key properties:&lt;br /&gt;
*Bit-stable code generation&lt;br /&gt;
*Reproducible builds&lt;br /&gt;
*Deterministic execution behavior&lt;br /&gt;
These properties are essential for certification, validation, and long-term maintenance of embedded AI software.&lt;br /&gt;
==Related Pages==&lt;br /&gt;
*[[emmtrix Code Vectorizer]]&lt;br /&gt;
*[[Numerical Precision in ONNX and AI Inference]]&lt;br /&gt;
==See Also==&lt;br /&gt;
*emx-onnx-cgen GitHub Repository - https://github.com/emmtrix/emx-onnx-cgen/&lt;br /&gt;
[[Category:emmtrix Tools]]&lt;/div&gt;</summary>
		<author><name>Timo.stripf</name></author>
	</entry>
	<entry>
		<id>https://www.emmtrix.com/w139/index.php?title=emmtrix_ONNX-to-C_Code_Generator&amp;diff=2917</id>
		<title>emmtrix ONNX-to-C Code Generator</title>
		<link rel="alternate" type="text/html" href="https://www.emmtrix.com/w139/index.php?title=emmtrix_ONNX-to-C_Code_Generator&amp;diff=2917"/>
		<updated>2026-02-04T01:17:50Z</updated>

		<summary type="html">&lt;p&gt;Timo.stripf: Created page with &amp;quot;&amp;#039;&amp;#039;&amp;#039;emmtrix ONNX-to-C Code Generator (emx-onnx-cgen)&amp;#039;&amp;#039;&amp;#039; is an emmtrix-developed &amp;#039;&amp;#039;&amp;#039;AI frontend compiler&amp;#039;&amp;#039;&amp;#039; that translates ONNX models into &amp;#039;&amp;#039;&amp;#039;deterministic, analyzable C code&amp;#039;&amp;#039;&amp;#039; specifically designed for &amp;#039;&amp;#039;&amp;#039;auto-vectorization and embedded target optimization&amp;#039;&amp;#039;&amp;#039;.  The primary goal of emx-onnx-cgen is not to perform aggressive hardware-specific optimizations itself, but to generate &amp;#039;&amp;#039;&amp;#039;high-quality C code&amp;#039;&amp;#039;&amp;#039; that serves as an ideal input for the emmtrix &amp;#039;&amp;#039;&amp;#039;Vectorizer&amp;#039;&amp;#039;&amp;#039; and...&amp;quot;&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&#039;&#039;&#039;emmtrix ONNX-to-C Code Generator (emx-onnx-cgen)&#039;&#039;&#039; is an emmtrix-developed &#039;&#039;&#039;AI frontend compiler&#039;&#039;&#039; that translates ONNX models into &#039;&#039;&#039;deterministic, analyzable C code&#039;&#039;&#039; specifically designed for &#039;&#039;&#039;auto-vectorization and embedded target optimization&#039;&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
The primary goal of emx-onnx-cgen is not to perform aggressive hardware-specific optimizations itself, but to generate &#039;&#039;&#039;high-quality C code&#039;&#039;&#039; that serves as an ideal input for the emmtrix &#039;&#039;&#039;Vectorizer&#039;&#039;&#039; and subsequent backend toolchains targeting embedded architectures.&lt;br /&gt;
&lt;br /&gt;
emx-onnx-cgen is particularly suited for safety-critical, resource-constrained, and bare-metal environments where transparency, determinism, and static analyzability are mandatory.&lt;br /&gt;
==Motivation==&lt;br /&gt;
Typical AI deployment solutions rely on runtime engines or architecture-specific code generators that:&lt;br /&gt;
*hide control flow and memory access patterns,&lt;br /&gt;
*rely on dynamic memory allocation,&lt;br /&gt;
*limit static analysis and certification,&lt;br /&gt;
*restrict the effectiveness of auto-vectorization.&lt;br /&gt;
emx-onnx-cgen follows a different approach:&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;ONNX → Clean C → Vectorizer → Target Architecture&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
By lowering AI models into a well-structured and explicit C representation, emx-onnx-cgen enables advanced compiler analyses and vectorization passes that would otherwise be impossible or unreliable.&lt;br /&gt;
==Role in the emmtrix Toolchain==&lt;br /&gt;
emx-onnx-cgen acts as the &#039;&#039;&#039;frontend&#039;&#039;&#039; in the emmtrix AI compilation pipeline.&lt;br /&gt;
;Pipeline Overview&lt;br /&gt;
*&#039;&#039;&#039;ONNX Model&#039;&#039;&#039;Imported neural network description&lt;br /&gt;
&lt;br /&gt;
*&#039;&#039;&#039;emx-onnx-cgen&#039;&#039;&#039;Normalization, lowering, and generation of vectorization-friendly C code&lt;br /&gt;
&lt;br /&gt;
*&#039;&#039;&#039;emmtrix Vectorizer&#039;&#039;&#039;Loop analysis, data-flow analysis, auto-vectorization&lt;br /&gt;
&lt;br /&gt;
*&#039;&#039;&#039;Backend / Toolchain&#039;&#039;&#039;Architecture-specific code generation (DSP, MCU, SoC)&lt;br /&gt;
&#039;&#039;&#039;Diagram suggestion:&#039;&#039;&#039;&amp;lt;pre&amp;gt;&lt;br /&gt;
ONNX Model&lt;br /&gt;
    |&lt;br /&gt;
    v&lt;br /&gt;
emx-onnx-cgen&lt;br /&gt;
    |&lt;br /&gt;
    v&lt;br /&gt;
Vectorization-Friendly C Code&lt;br /&gt;
    |&lt;br /&gt;
    v&lt;br /&gt;
emmtrix Vectorizer&lt;br /&gt;
    |&lt;br /&gt;
    v&lt;br /&gt;
Optimized Embedded Binary&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
==Design Goals==&lt;br /&gt;
emx-onnx-cgen is designed with the following goals:&lt;br /&gt;
*Deterministic code generation&lt;br /&gt;
*Fully static memory layout (no heap usage)&lt;br /&gt;
*Explicit loops and control flow&lt;br /&gt;
*Predictable memory access patterns&lt;br /&gt;
*Readable and auditable C code&lt;br /&gt;
*Maximal compatibility with auto-vectorization tools&lt;br /&gt;
*Suitability for embedded and safety-critical systems&lt;br /&gt;
==Non-Goals==&lt;br /&gt;
emx-onnx-cgen explicitly does &#039;&#039;&#039;not&#039;&#039;&#039; aim to:&lt;br /&gt;
*Act as a runtime inference engine&lt;br /&gt;
*Perform hand-written SIMD or target-specific optimizations&lt;br /&gt;
*Hide model execution behind opaque APIs&lt;br /&gt;
*Support training or backpropagation&lt;br /&gt;
==Code Generation Principles==&lt;br /&gt;
The generated C code follows strict structural rules to support static analysis and vectorization:&lt;br /&gt;
*Simple, canonical loop forms&lt;br /&gt;
*Linear array accesses&lt;br /&gt;
*No hidden pointer aliasing&lt;br /&gt;
*No dynamic dispatch&lt;br /&gt;
*No recursion&lt;br /&gt;
*No dynamic memory allocation&lt;br /&gt;
*Explicit tensor dimensions and strides&lt;br /&gt;
These properties allow the emmtrix Vectorizer to reliably detect vectorization opportunities and apply architecture-specific optimizations.&lt;br /&gt;
==Example==&lt;br /&gt;
===High-Level Operation===&lt;br /&gt;
An ONNX operator such as a vector addition is lowered into a simple C loop:&amp;lt;pre&amp;gt;&lt;br /&gt;
for (i = 0; i &amp;lt; N; ++i) {&lt;br /&gt;
    output[i] = input_a[i] + input_b[i];&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;This structure is intentionally chosen to be:&lt;br /&gt;
*easy to analyze,&lt;br /&gt;
*free of side effects,&lt;br /&gt;
*ideal for SIMD vectorization by downstream tools.&lt;br /&gt;
==Embedded Target Suitability==&lt;br /&gt;
emx-onnx-cgen is suitable for:&lt;br /&gt;
*Bare-metal systems&lt;br /&gt;
*RTOS-based systems&lt;br /&gt;
*Automotive ECUs&lt;br /&gt;
*Industrial controllers&lt;br /&gt;
*DSP-based platforms&lt;br /&gt;
The generated code depends only on standard C headers and can be integrated into existing embedded build systems without requiring a runtime framework.&lt;br /&gt;
==Verification and Determinism==&lt;br /&gt;
To ensure correctness, emx-onnx-cgen supports verification against reference ONNX execution.&lt;br /&gt;
&lt;br /&gt;
Key properties:&lt;br /&gt;
*Bit-stable code generation&lt;br /&gt;
*Reproducible builds&lt;br /&gt;
*Deterministic execution behavior&lt;br /&gt;
These properties are essential for certification, validation, and long-term maintenance of embedded AI software.&lt;br /&gt;
==Related Pages==&lt;br /&gt;
*[[emmtrix Code Vectorizer]]&lt;br /&gt;
*[[Numerical Precision in ONNX and AI Inference]]&lt;br /&gt;
==See Also==&lt;br /&gt;
*emx-onnx-cgen GitHub Repository - https://github.com/emmtrix/emx-onnx-cgen/&lt;br /&gt;
[[Category:emmtrix Tools]]&lt;/div&gt;</summary>
		<author><name>Timo.stripf</name></author>
	</entry>
	<entry>
		<id>https://www.emmtrix.com/w139/index.php?title=Main_Page&amp;diff=2916</id>
		<title>Main Page</title>
		<link rel="alternate" type="text/html" href="https://www.emmtrix.com/w139/index.php?title=Main_Page&amp;diff=2916"/>
		<updated>2026-02-04T01:12:29Z</updated>

		<summary type="html">&lt;p&gt;Timo.stripf: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;__NOTOC__&lt;br /&gt;
&lt;br /&gt;
Welcome to the emmtrix Technologies Wiki. As a company with a deep-rooted passion for compilers, we specialize in source-to-source compilers designed to analyze, optimize and transform your code. This Wiki aims to offer detailed, technical background information that complements the tools and resources available on our official website. Here, you&#039;ll find in-depth explanations, usage guidelines, and insights into the engineering behind our specialized software solutions. Whether you&#039;re a developer or a technically-inclined enthusiast, this space is designed to deepen your understanding of what makes our tools essential for your projects.&lt;br /&gt;
&lt;br /&gt;
{| border=&amp;quot;0&amp;quot; style=&amp;quot;padding: 10px&amp;quot;&lt;br /&gt;
| style=&amp;quot;vertical-align: top; padding: 10px;&amp;quot; |&lt;br /&gt;
=== emmtrix Products ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;!--T:7--&amp;gt;&lt;br /&gt;
* [[emmtrix Dependency Analyzer]]&lt;br /&gt;
* [[emmtrix Performance Estimator]]&lt;br /&gt;
* [[emmtrix Code Vectorizer]]&lt;br /&gt;
** [[emmtrix ONNX-to-C Code Generator]]&lt;br /&gt;
* [[emmtrix C++ to C Compiler]]&lt;br /&gt;
** [https://online-ecpp2c.emmtrix.com emmtrix C++ to C Compiler Online]&lt;br /&gt;
* [[emmtrix C to Rust Compiler]]&lt;br /&gt;
* emmtrix Parallel Studio&lt;br /&gt;
* Generic information&lt;br /&gt;
** [[emmtrix Studio Release Notes]]&lt;br /&gt;
** [[:Category:emmtrix Studio FAQ|FAQ]]&lt;br /&gt;
* Discontinued product&lt;br /&gt;
** [[emmtrix Code Generator]]&lt;br /&gt;
** [[emmtrix Model Code Generator]]&lt;br /&gt;
* Other Solutions&lt;br /&gt;
** [[TC4x PPU Coverage Analysis]]&lt;br /&gt;
** [[emmtrix Link Stubber]]&lt;br /&gt;
| style=&amp;quot;vertical-align: top; padding: 10px;&amp;quot; |&lt;br /&gt;
=== Supported Architectures ===&lt;br /&gt;
* [[TriCore Instruction Set Architecture]]&lt;br /&gt;
** [[Infineon AURIX TC2xx]]&lt;br /&gt;
** [[Infineon AURIX TC3xx]]&lt;br /&gt;
** [[Infineon AURIX TC4x]], including [[Infineon AURIX TC4x Parallel Processing Unit (PPU)|Parallel Processing Unit (PPU)]]&lt;br /&gt;
* ... and many more&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
|&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Compiler Background ===&lt;br /&gt;
&lt;br /&gt;
* [[Loop Transformations]]&lt;br /&gt;
* [[:Category:Code Transformation]]&lt;br /&gt;
*[[Demystifying C++]]&lt;br /&gt;
*[[The alias Attribute|The alias attribute]]&lt;br /&gt;
*[[Clang Diagnostics Overview]]&lt;br /&gt;
*[[:Category:Clang Diagnostics]]&lt;br /&gt;
&lt;br /&gt;
=== Other Topics ===&lt;br /&gt;
* [[C to Z3 Cheat Sheet]]&lt;br /&gt;
* [[Eclipse Xcore Cheat Sheet]]&lt;br /&gt;
* [[Logical Execution Time (LET)]]&lt;br /&gt;
* [[ULP Difference of Float Numbers]]&lt;br /&gt;
* [[Numerical Precision in ONNX and AI Inference]]&lt;br /&gt;
* [[Automatic C to Rust Translation]]&lt;/div&gt;</summary>
		<author><name>Timo.stripf</name></author>
	</entry>
	<entry>
		<id>https://www.emmtrix.com/w139/index.php?title=emmtrix_Code_Vectorizer&amp;diff=2915</id>
		<title>emmtrix Code Vectorizer</title>
		<link rel="alternate" type="text/html" href="https://www.emmtrix.com/w139/index.php?title=emmtrix_Code_Vectorizer&amp;diff=2915"/>
		<updated>2026-02-03T09:51:27Z</updated>

		<summary type="html">&lt;p&gt;Timo.stripf: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;emmtrix Code Vectorizer is a C source-to-source compiler that vectorizes C code for various SIMD architectures like [[Infineon AURIX TC4x|Infineon AURIX TC4x PPU]], x86 AVX, ARM NEON, or RISC-V V extensions. It follows an semi-automatic approach, where the user can guide the vectorization process from an Eclipse-based GUI or by using pragmas in the C code. The vectorized C code is always visible to the user, which makes it easy to follow the transformation and to understand the generated code. The generated code can be compiled with the vendor’s compiler and either run on the target hardware or in a simulator to get performance feedback and verify the correctness of the transformation.&lt;br /&gt;
&lt;br /&gt;
=== Multi-Precision Vector Math Library ===&lt;br /&gt;
The emmtrix Code Vectorizer incorporates a multi-precision vector math library that provides vectorized implementations of common C mathematical functions like sinf, cosf, expf, logf, powf, sqrtf, tanhf and many more. The library provides multiple versions of each function to provide the best trade-off between accuracy and performance. During vectorization, the vector math library is used to replace scalar math functions with vectorized versions. By specifying the accuracy requirements in [[ULP Difference of Float Numbers|ULPs (units in the last place)]], the user can control which version of the function is used.&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Function&lt;br /&gt;
!Info&lt;br /&gt;
! Accuracy&amp;lt;ref&amp;gt;Accuracy of hardware version are given from the data sheet. A ULP 0.0 means that the implementation is exact. Otherwise, the highest know ULP is provided. Math functions that are based on non-perfect hardware implementation use a pessimistic software implementation for ULP evaluation. A more accurate ULP evaluation could be provided on request.&amp;lt;/ref&amp;gt; [ULP]&lt;br /&gt;
! Latency on AURIX™ TC4x [cycles]&lt;br /&gt;
! Throughput on AURIX™ TC4x [cycles]&lt;br /&gt;
|-&lt;br /&gt;
|fabsf&lt;br /&gt;
|&lt;br /&gt;
|0.0&lt;br /&gt;
|&lt;br /&gt;
|&lt;br /&gt;
|-&lt;br /&gt;
|fminf&lt;br /&gt;
|&lt;br /&gt;
|0.0&lt;br /&gt;
|&lt;br /&gt;
|&lt;br /&gt;
|-&lt;br /&gt;
|fmaxf&lt;br /&gt;
|&lt;br /&gt;
|0.0&lt;br /&gt;
|&lt;br /&gt;
|&lt;br /&gt;
|-&lt;br /&gt;
|roundf&lt;br /&gt;
|&lt;br /&gt;
|0.0&lt;br /&gt;
|11&lt;br /&gt;
|5&lt;br /&gt;
|-&lt;br /&gt;
|ceilf&lt;br /&gt;
|&lt;br /&gt;
|0.0&lt;br /&gt;
|6&lt;br /&gt;
|4&lt;br /&gt;
|-&lt;br /&gt;
|floorf&lt;br /&gt;
|&lt;br /&gt;
|0.0&lt;br /&gt;
|6&lt;br /&gt;
|4&lt;br /&gt;
|-&lt;br /&gt;
|truncf&lt;br /&gt;
|&lt;br /&gt;
|0.0&lt;br /&gt;
|6&lt;br /&gt;
|4&lt;br /&gt;
|-&lt;br /&gt;
| sqrtf&lt;br /&gt;
|Hardware version&lt;br /&gt;
| 0.5&lt;br /&gt;
|16&lt;br /&gt;
|7&lt;br /&gt;
|-&lt;br /&gt;
| expf&lt;br /&gt;
|Hardware version&lt;br /&gt;
| 1.0&lt;br /&gt;
|11&lt;br /&gt;
|6&lt;br /&gt;
|-&lt;br /&gt;
|exp2f&lt;br /&gt;
|Hardware version&lt;br /&gt;
|1.0&lt;br /&gt;
|14&lt;br /&gt;
|6&lt;br /&gt;
|-&lt;br /&gt;
| log2f&lt;br /&gt;
|Hardware version&lt;br /&gt;
| 1.0&lt;br /&gt;
|14&lt;br /&gt;
|6&lt;br /&gt;
|-&lt;br /&gt;
|logf&lt;br /&gt;
|Based on log2f ULP 1.0 hardware version&lt;br /&gt;
|1.93&lt;br /&gt;
|&lt;br /&gt;
|&lt;br /&gt;
|-&lt;br /&gt;
|log10f&lt;br /&gt;
|Based on log2f ULP 1.0 hardware version&lt;br /&gt;
|2.50&lt;br /&gt;
|&lt;br /&gt;
|&lt;br /&gt;
|-&lt;br /&gt;
| sinf&lt;br /&gt;
|Hardware version&lt;br /&gt;
| 1.0&lt;br /&gt;
|14&lt;br /&gt;
|6&lt;br /&gt;
|-&lt;br /&gt;
| cosf&lt;br /&gt;
|Hardware version&lt;br /&gt;
| 1.0&lt;br /&gt;
|14&lt;br /&gt;
|6&lt;br /&gt;
|-&lt;br /&gt;
|tanf&lt;br /&gt;
|Based on sinf/cosf ULP 1.0 hardware version&lt;br /&gt;
|3.42&lt;br /&gt;
|&lt;br /&gt;
|&lt;br /&gt;
|-&lt;br /&gt;
| rowspan=&amp;quot;4&amp;quot; | tanhf&lt;br /&gt;
| rowspan=&amp;quot;4&amp;quot; |Based on expf ULP 1.0 hardware version&lt;br /&gt;
|7.08&lt;br /&gt;
|41&lt;br /&gt;
|18&lt;br /&gt;
|-&lt;br /&gt;
|4.23&lt;br /&gt;
|41&lt;br /&gt;
|19&lt;br /&gt;
|-&lt;br /&gt;
|2.74&lt;br /&gt;
|41&lt;br /&gt;
|23&lt;br /&gt;
|-&lt;br /&gt;
|1.91&lt;br /&gt;
|41&lt;br /&gt;
|27&lt;br /&gt;
|-&lt;br /&gt;
|hypotf&lt;br /&gt;
|Based on sqrt ULP 0.5 hardware vesrion&lt;br /&gt;
|1.995&lt;br /&gt;
|&lt;br /&gt;
|&lt;br /&gt;
|-&lt;br /&gt;
|remainderf&lt;br /&gt;
|&lt;br /&gt;
|&lt;br /&gt;
|&lt;br /&gt;
|&lt;br /&gt;
|-&lt;br /&gt;
|fmodf&lt;br /&gt;
|&lt;br /&gt;
|&lt;br /&gt;
|&lt;br /&gt;
|&lt;br /&gt;
|-&lt;br /&gt;
|fdimf&lt;br /&gt;
|&lt;br /&gt;
|&lt;br /&gt;
|&lt;br /&gt;
|&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
[[Category:emmtrix Tools]]&lt;br /&gt;
&amp;lt;references /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Interested? ==&lt;br /&gt;
{{CallToAction|text=Interested in applying this coverage workflow to your own projects?}}&lt;br /&gt;
[[Category:Numerical Precision]]&lt;/div&gt;</summary>
		<author><name>Timo.stripf</name></author>
	</entry>
	<entry>
		<id>https://www.emmtrix.com/w139/index.php?title=tanh_Software_Implementation&amp;diff=2914</id>
		<title>tanh Software Implementation</title>
		<link rel="alternate" type="text/html" href="https://www.emmtrix.com/w139/index.php?title=tanh_Software_Implementation&amp;diff=2914"/>
		<updated>2026-02-03T09:51:12Z</updated>

		<summary type="html">&lt;p&gt;Timo.stripf: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;[[File:Hyperbolic Tangent.svg|thumb]]&lt;br /&gt;
The hyperbolic tangent function, commonly referred to as tanh, is a mathematical function that maps real numbers to the range (−1,1). Defined as:&lt;br /&gt;
&lt;br /&gt;
:&amp;lt;math&amp;gt;\tanh(x) = \frac{e^x - e^{-x}}{e^x + e^{-x}}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
it is a smooth, continuous, and symmetric function centered at the origin. The tanh function is widely used in mathematics, physics, and engineering, particularly in areas involving hyperbolic geometry and signal processing.&lt;br /&gt;
&lt;br /&gt;
In machine learning and deep learning, tanh is a popular activation function because it introduces non-linearity and maps inputs to a centered range around zero, helping to balance gradients during optimization. Its output approaches 1 for large positive inputs and −1 for large negative inputs, with steep transitions around x=0.&lt;br /&gt;
&lt;br /&gt;
== Simple Approximation with Exponential Math Function ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+&lt;br /&gt;
!Id&lt;br /&gt;
!Formula&lt;br /&gt;
!Highest ULP Error&lt;br /&gt;
!ULP &amp;lt;= 2.0&lt;br /&gt;
!NaN&lt;br /&gt;
|-&lt;br /&gt;
|exp_v1&lt;br /&gt;
|&amp;lt;math&amp;gt;\frac{e^x - e^{-x}}{e^x + e^{-x}}&amp;lt;/math&amp;gt;&lt;br /&gt;
|x &amp;lt; 0: 8.55638e+08 at -2.98023e-08 with 0x0p+0 instead of -0x1p-25&lt;br /&gt;
&lt;br /&gt;
x &amp;gt; 0: 8.55638e+08 at 2.98023e-08 with 0x0p+0 instead of 0x1p-25&lt;br /&gt;
| -88.7246 &amp;lt; x &amp;lt; -7.14197&lt;br /&gt;
7.14197 &amp;lt; x &amp;lt; 88.7246&lt;br /&gt;
| -inf &amp;lt; x &amp;lt; -88.7246&lt;br /&gt;
88.7246 &amp;lt; x &amp;lt; inf&lt;br /&gt;
|-&lt;br /&gt;
|exp_v2&lt;br /&gt;
|&amp;lt;math&amp;gt;\frac{e^{2x} - 1}{e^{2x} + 1}&amp;lt;/math&amp;gt;&lt;br /&gt;
|x &amp;lt; 0: 8.47249e+08 at -1.49012e-08 with 0x0p+0 instead of -0x1p-26&lt;br /&gt;
x &amp;gt; 0: 8.55638e+08 at 2.98023e-08 with 0x0p+0 instead of 0x1.fffffep-26&lt;br /&gt;
| -inf &amp;lt; x &amp;lt; -0.547821&lt;br /&gt;
0.254822 &amp;lt; x &amp;lt; 8.31787&lt;br /&gt;
&lt;br /&gt;
8.38379 &amp;lt; x &amp;lt; 44.3623&lt;br /&gt;
|44.3623 &amp;lt; x &amp;lt; inf&lt;br /&gt;
|-&lt;br /&gt;
|exp_v3&lt;br /&gt;
|&amp;lt;math&amp;gt;1 - \frac{2}{e^{2x} + 1}&amp;lt;/math&amp;gt;&lt;br /&gt;
|x &amp;lt; 0: 8.59832e+08 at -4.47035e-08 with 0x0p+0 instead of -0x1.8p-25&lt;br /&gt;
x &amp;gt; 0: 8.68221e+08 at 8.9407e-08 with 0x0p+0 instead of 0x1.7ffffcp-24&lt;br /&gt;
| -inf &amp;lt; x &amp;lt; -6.26233&lt;br /&gt;
0.346382 &amp;lt; x &amp;lt; inf&lt;br /&gt;
| -&lt;br /&gt;
|-&lt;br /&gt;
|expm1&lt;br /&gt;
|&amp;lt;math&amp;gt;\frac{expm1(2x)}{expm1(2x) + 2}&amp;lt;/math&amp;gt; &lt;br /&gt;
|x &amp;lt; 0: 2.49585 at -3.95627 with -0x1.ffa00ap-1 instead of -0x1.ffa00ep-1&lt;br /&gt;
x &amp;gt; 0: 2.41657 at 0.0155837 with 0x1.fe9b66p-7 instead of 0x1.fe9b62p-7&lt;br /&gt;
| -inf &amp;lt; x &amp;lt; -5.57227&lt;br /&gt;
-0.42981 &amp;lt; x &amp;lt; 0.00777948&lt;br /&gt;
&lt;br /&gt;
0.125351 &amp;lt; x &amp;lt; 44.3623&lt;br /&gt;
|44.3623 &amp;lt; x &amp;lt; inf&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
The first equation is the definition of the hyperbolic tangent function. This equation requires two exponentials with different values. Since exponentials are expensive to compute, we can use the second equation to reduce the number of exponentials to one.&lt;br /&gt;
&lt;br /&gt;
The second equation has the drawback that both the numerator and the denominator can become infinity for large values of x which results in NaN. That happens for &amp;lt;math&amp;gt;|x| &amp;gt; \log(FLT\_MAX)/2 \approx 44&amp;lt;/math&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
The third equation solves this problem but has a slightly higher error for &amp;lt;math&amp;gt;x \approx 2^{-25}&amp;lt;/math&amp;gt;. In the following graph you can see the error chart for equation 3. The error charts for the other two equations are very similar. All three equations have an error of up to 16 mio ULPs for &amp;lt;math&amp;gt;x &amp;lt; 2^{-5}&amp;lt;/math&amp;gt; which means that up to 24 bits could be wrong.&lt;br /&gt;
&lt;br /&gt;
=== exp_v2 ===&lt;br /&gt;
&lt;br /&gt;
{{#widget:FloatErrorGraph|id=tanhf_exp_v2&lt;br /&gt;
  |data=[ [&amp;quot;-2^127&amp;quot;,0,1.17549e-38],[&amp;quot;-2^126&amp;quot;,0,1.17549e-38],[&amp;quot;-2^125&amp;quot;,0,1.17549e-38],[&amp;quot;-2^124&amp;quot;,0,1.17549e-38],[&amp;quot;-2^123&amp;quot;,0,1.17549e-38],[&amp;quot;-2^122&amp;quot;,0,1.17549e-38],[&amp;quot;-2^121&amp;quot;,0,1.17549e-38],[&amp;quot;-2^120&amp;quot;,0,1.17549e-38],[&amp;quot;-2^119&amp;quot;,0,1.17549e-38],[&amp;quot;-2^118&amp;quot;,0,1.17549e-38],[&amp;quot;-2^117&amp;quot;,0,1.17549e-38],[&amp;quot;-2^116&amp;quot;,0,1.17549e-38],[&amp;quot;-2^115&amp;quot;,0,1.17549e-38],[&amp;quot;-2^114&amp;quot;,0,1.17549e-38],[&amp;quot;-2^113&amp;quot;,0,1.17549e-38],[&amp;quot;-2^112&amp;quot;,0,1.17549e-38],[&amp;quot;-2^111&amp;quot;,0,1.17549e-38],[&amp;quot;-2^110&amp;quot;,0,1.17549e-38],[&amp;quot;-2^109&amp;quot;,0,1.17549e-38],[&amp;quot;-2^108&amp;quot;,0,1.17549e-38],[&amp;quot;-2^107&amp;quot;,0,1.17549e-38],[&amp;quot;-2^106&amp;quot;,0,1.17549e-38],[&amp;quot;-2^105&amp;quot;,0,1.17549e-38],[&amp;quot;-2^104&amp;quot;,0,1.17549e-38],[&amp;quot;-2^103&amp;quot;,0,1.17549e-38],[&amp;quot;-2^102&amp;quot;,0,1.17549e-38],[&amp;quot;-2^101&amp;quot;,0,1.17549e-38],[&amp;quot;-2^100&amp;quot;,0,1.17549e-38],[&amp;quot;-2^99&amp;quot;,0,1.17549e-38],[&amp;quot;-2^98&amp;quot;,0,1.17549e-38],[&amp;quot;-2^97&amp;quot;,0,1.17549e-38],[&amp;quot;-2^96&amp;quot;,0,1.17549e-38],[&amp;quot;-2^95&amp;quot;,0,1.17549e-38],[&amp;quot;-2^94&amp;quot;,0,1.17549e-38],[&amp;quot;-2^93&amp;quot;,0,1.17549e-38],[&amp;quot;-2^92&amp;quot;,0,1.17549e-38],[&amp;quot;-2^91&amp;quot;,0,1.17549e-38],[&amp;quot;-2^90&amp;quot;,0,1.17549e-38],[&amp;quot;-2^89&amp;quot;,0,1.17549e-38],[&amp;quot;-2^88&amp;quot;,0,1.17549e-38],[&amp;quot;-2^87&amp;quot;,0,1.17549e-38],[&amp;quot;-2^86&amp;quot;,0,1.17549e-38],[&amp;quot;-2^85&amp;quot;,0,1.17549e-38],[&amp;quot;-2^84&amp;quot;,0,1.17549e-38],[&amp;quot;-2^83&amp;quot;,0,1.17549e-38],[&amp;quot;-2^82&amp;quot;,0,1.17549e-38],[&amp;quot;-2^81&amp;quot;,0,1.17549e-38],[&amp;quot;-2^80&amp;quot;,0,1.17549e-38],[&amp;quot;-2^79&amp;quot;,0,1.17549e-38],[&amp;quot;-2^78&amp;quot;,0,1.17549e-38],[&amp;quot;-2^77&amp;quot;,0,1.17549e-38],[&amp;quot;-2^76&amp;quot;,0,1.17549e-38],[&amp;quot;-2^75&amp;quot;,0,1.17549e-38],[&amp;quot;-2^74&amp;quot;,0,1.17549e-38],[&amp;quot;-2^73&amp;quot;,0,1.17549e-38],[&amp;quot;-2^72&amp;quot;,0,1.17549e-38],[&amp;quot;-2^71&amp;quot;,0,1.17549e-38],[&amp;quot;-2^70&amp;quot;,0,1.17549e-38],[&amp;quot;-2^69&amp;quot;,0,1.17549e-38],[&amp;quot;-2^68&amp;quot;,0,1.17549e-38],[&amp;quot;-2^67&amp;quot;,0,1.17549e-38],[&amp;quot;-2^66&amp;quot;,0,1.17549e-38],[&amp;quot;-2^65&amp;quot;,0,1.17549e-38],[&amp;quot;-2^64&amp;quot;,0,1.17549e-38],[&amp;quot;-2^63&amp;quot;,0,1.17549e-38],[&amp;quot;-2^62&amp;quot;,0,1.17549e-38],[&amp;quot;-2^61&amp;quot;,0,1.17549e-38],[&amp;quot;-2^60&amp;quot;,0,1.17549e-38],[&amp;quot;-2^59&amp;quot;,0,1.17549e-38],[&amp;quot;-2^58&amp;quot;,0,1.17549e-38],[&amp;quot;-2^57&amp;quot;,0,1.17549e-38],[&amp;quot;-2^56&amp;quot;,0,1.17549e-38],[&amp;quot;-2^55&amp;quot;,0,1.17549e-38],[&amp;quot;-2^54&amp;quot;,0,1.17549e-38],[&amp;quot;-2^53&amp;quot;,0,1.17549e-38],[&amp;quot;-2^52&amp;quot;,0,1.17549e-38],[&amp;quot;-2^51&amp;quot;,0,1.17549e-38],[&amp;quot;-2^50&amp;quot;,0,1.17549e-38],[&amp;quot;-2^49&amp;quot;,0,1.17549e-38],[&amp;quot;-2^48&amp;quot;,0,1.17549e-38],[&amp;quot;-2^47&amp;quot;,0,1.17549e-38],[&amp;quot;-2^46&amp;quot;,0,1.17549e-38],[&amp;quot;-2^45&amp;quot;,0,1.17549e-38],[&amp;quot;-2^44&amp;quot;,0,1.17549e-38],[&amp;quot;-2^43&amp;quot;,0,1.17549e-38],[&amp;quot;-2^42&amp;quot;,0,1.17549e-38],[&amp;quot;-2^41&amp;quot;,0,1.17549e-38],[&amp;quot;-2^40&amp;quot;,0,1.17549e-38],[&amp;quot;-2^39&amp;quot;,0,1.17549e-38],[&amp;quot;-2^38&amp;quot;,0,1.17549e-38],[&amp;quot;-2^37&amp;quot;,0,1.17549e-38],[&amp;quot;-2^36&amp;quot;,0,1.17549e-38],[&amp;quot;-2^35&amp;quot;,0,1.17549e-38],[&amp;quot;-2^34&amp;quot;,0,1.17549e-38],[&amp;quot;-2^33&amp;quot;,0,1.17549e-38],[&amp;quot;-2^32&amp;quot;,0,1.17549e-38],[&amp;quot;-2^31&amp;quot;,0,1.17549e-38],[&amp;quot;-2^30&amp;quot;,0,1.17549e-38],[&amp;quot;-2^29&amp;quot;,0,1.17549e-38],[&amp;quot;-2^28&amp;quot;,0,1.17549e-38],[&amp;quot;-2^27&amp;quot;,0,1.17549e-38],[&amp;quot;-2^26&amp;quot;,0,1.17549e-38],[&amp;quot;-2^25&amp;quot;,0,1.17549e-38],[&amp;quot;-2^24&amp;quot;,0,1.17549e-38],[&amp;quot;-2^23&amp;quot;,0,1.17549e-38],[&amp;quot;-2^22&amp;quot;,0,1.17549e-38],[&amp;quot;-2^21&amp;quot;,0,1.17549e-38],[&amp;quot;-2^20&amp;quot;,0,1.17549e-38],[&amp;quot;-2^19&amp;quot;,0,1.17549e-38],[&amp;quot;-2^18&amp;quot;,0,1.17549e-38],[&amp;quot;-2^17&amp;quot;,0,1.17549e-38],[&amp;quot;-2^16&amp;quot;,0,1.17549e-38],[&amp;quot;-2^15&amp;quot;,0,1.17549e-38],[&amp;quot;-2^14&amp;quot;,0,1.17549e-38],[&amp;quot;-2^13&amp;quot;,0,1.17549e-38],[&amp;quot;-2^12&amp;quot;,0,1.17549e-38],[&amp;quot;-2^11&amp;quot;,0,1.17549e-38],[&amp;quot;-2^10&amp;quot;,0,1.17549e-38],[&amp;quot;-2^9&amp;quot;,0,1.17549e-38],[&amp;quot;-2^8&amp;quot;,0,1.17549e-38],[&amp;quot;-2^7&amp;quot;,0,1.17549e-38],[&amp;quot;-2^6&amp;quot;,0,1.17549e-38],[&amp;quot;-2^5&amp;quot;,0,1.17549e-38],[&amp;quot;-2^4&amp;quot;,2.53131e-14,4.24683e-07],[&amp;quot;-2^3&amp;quot;,5.95888e-08,0.999734],[&amp;quot;-2^2&amp;quot;,8.86696e-08,1.48763],[&amp;quot;-2^1&amp;quot;,8.91609e-08,1.49587],[&amp;quot;-2^0&amp;quot;,8.73743e-08,1.4659],[&amp;quot;-2^-1&amp;quot;,8.65382e-08,2.13222],[&amp;quot;-2^-2&amp;quot;,6.18481e-08,2.61428],[&amp;quot;-2^-3&amp;quot;,3.88071e-08,3.08594],[&amp;quot;-2^-4&amp;quot;,2.6234e-08,4.23594],[&amp;quot;-2^-5&amp;quot;,2.02799e-08,8.19726],[&amp;quot;-2^-6&amp;quot;,1.75183e-08,12.4495],[&amp;quot;-2^-7&amp;quot;,1.61937e-08,17.3878],[&amp;quot;-2^-8&amp;quot;,1.5566e-08,33.4278],[&amp;quot;-2^-9&amp;quot;,1.51688e-08,65.1495],[&amp;quot;-2^-10&amp;quot;,1.50281e-08,129.09],[&amp;quot;-2^-11&amp;quot;,1.49613e-08,257.033],[&amp;quot;-2^-12&amp;quot;,1.49296e-08,512.977],[&amp;quot;-2^-13&amp;quot;,1.49174e-08,1025.12],[&amp;quot;-2^-14&amp;quot;,1.4909e-08,2049.08],[&amp;quot;-2^-15&amp;quot;,1.49049e-08,4097.02],[&amp;quot;-2^-16&amp;quot;,1.4903e-08,8193],[&amp;quot;-2^-17&amp;quot;,1.49021e-08,16385],[&amp;quot;-2^-18&amp;quot;,1.48862e-08,32735],[&amp;quot;-2^-19&amp;quot;,1.48973e-08,65519],[&amp;quot;-2^-20&amp;quot;,1.49001e-08,131063],[&amp;quot;-2^-21&amp;quot;,1.49009e-08,262139],[&amp;quot;-2^-22&amp;quot;,1.49011e-08,524285],[&amp;quot;-2^-23&amp;quot;,1.49011e-08,1.04857e+06],[&amp;quot;-2^-24&amp;quot;,1.49011e-08,2.09715e+06],[&amp;quot;-2^-25&amp;quot;,1.49012e-08,4.1943e+06],[&amp;quot;-2^-26&amp;quot;,1.49007e-08,8.38835e+06],[&amp;quot;-2^-27&amp;quot;,1.49012e-08,8.47249e+08],[&amp;quot;-2^-28&amp;quot;,7.45058e-09,8.38861e+08],[&amp;quot;-2^-29&amp;quot;,3.72529e-09,8.30472e+08],[&amp;quot;-2^-30&amp;quot;,1.86265e-09,8.22084e+08],[&amp;quot;-2^-31&amp;quot;,9.31323e-10,8.13695e+08],[&amp;quot;-2^-32&amp;quot;,4.65661e-10,8.05306e+08],[&amp;quot;-2^-33&amp;quot;,2.32831e-10,7.96918e+08],[&amp;quot;-2^-34&amp;quot;,1.16415e-10,7.88529e+08],[&amp;quot;-2^-35&amp;quot;,5.82077e-11,7.80141e+08],[&amp;quot;-2^-36&amp;quot;,2.91038e-11,7.71752e+08],[&amp;quot;-2^-37&amp;quot;,1.45519e-11,7.63363e+08],[&amp;quot;-2^-38&amp;quot;,7.27596e-12,7.54975e+08],[&amp;quot;-2^-39&amp;quot;,3.63798e-12,7.46586e+08],[&amp;quot;-2^-40&amp;quot;,1.81899e-12,7.38198e+08],[&amp;quot;-2^-41&amp;quot;,9.09495e-13,7.29809e+08],[&amp;quot;-2^-42&amp;quot;,4.54747e-13,7.2142e+08],[&amp;quot;-2^-43&amp;quot;,2.27374e-13,7.13032e+08],[&amp;quot;-2^-44&amp;quot;,1.13687e-13,7.04643e+08],[&amp;quot;-2^-45&amp;quot;,5.68434e-14,6.96254e+08],[&amp;quot;-2^-46&amp;quot;,2.84217e-14,6.87866e+08],[&amp;quot;-2^-47&amp;quot;,1.42109e-14,6.79477e+08],[&amp;quot;-2^-48&amp;quot;,7.10543e-15,6.71089e+08],[&amp;quot;-2^-49&amp;quot;,3.55271e-15,6.627e+08],[&amp;quot;-2^-50&amp;quot;,1.77636e-15,6.54311e+08],[&amp;quot;-2^-51&amp;quot;,8.88178e-16,6.45923e+08],[&amp;quot;-2^-52&amp;quot;,4.44089e-16,6.37534e+08],[&amp;quot;-2^-53&amp;quot;,2.22045e-16,6.29146e+08],[&amp;quot;-2^-54&amp;quot;,1.11022e-16,6.20757e+08],[&amp;quot;-2^-55&amp;quot;,5.55111e-17,6.12368e+08],[&amp;quot;-2^-56&amp;quot;,2.77556e-17,6.0398e+08],[&amp;quot;-2^-57&amp;quot;,1.38778e-17,5.95591e+08],[&amp;quot;-2^-58&amp;quot;,6.93889e-18,5.87203e+08],[&amp;quot;-2^-59&amp;quot;,3.46945e-18,5.78814e+08],[&amp;quot;-2^-60&amp;quot;,1.73472e-18,5.70425e+08],[&amp;quot;-2^-61&amp;quot;,8.67362e-19,5.62037e+08],[&amp;quot;-2^-62&amp;quot;,4.33681e-19,5.53648e+08],[&amp;quot;-2^-63&amp;quot;,2.1684e-19,5.4526e+08],[&amp;quot;-2^-64&amp;quot;,1.0842e-19,5.36871e+08],[&amp;quot;-2^-65&amp;quot;,5.42101e-20,5.28482e+08],[&amp;quot;-2^-66&amp;quot;,2.71051e-20,5.20094e+08],[&amp;quot;-2^-67&amp;quot;,1.35525e-20,5.11705e+08],[&amp;quot;-2^-68&amp;quot;,6.77626e-21,5.03316e+08],[&amp;quot;-2^-69&amp;quot;,3.38813e-21,4.94928e+08],[&amp;quot;-2^-70&amp;quot;,1.69407e-21,4.86539e+08],[&amp;quot;-2^-71&amp;quot;,8.47033e-22,4.78151e+08],[&amp;quot;-2^-72&amp;quot;,4.23516e-22,4.69762e+08],[&amp;quot;-2^-73&amp;quot;,2.11758e-22,4.61373e+08],[&amp;quot;-2^-74&amp;quot;,1.05879e-22,4.52985e+08],[&amp;quot;-2^-75&amp;quot;,5.29396e-23,4.44596e+08],[&amp;quot;-2^-76&amp;quot;,2.64698e-23,4.36208e+08],[&amp;quot;-2^-77&amp;quot;,1.32349e-23,4.27819e+08],[&amp;quot;-2^-78&amp;quot;,6.61744e-24,4.1943e+08],[&amp;quot;-2^-79&amp;quot;,3.30872e-24,4.11042e+08],[&amp;quot;-2^-80&amp;quot;,1.65436e-24,4.02653e+08],[&amp;quot;-2^-81&amp;quot;,8.27181e-25,3.94265e+08],[&amp;quot;-2^-82&amp;quot;,4.1359e-25,3.85876e+08],[&amp;quot;-2^-83&amp;quot;,2.06795e-25,3.77487e+08],[&amp;quot;-2^-84&amp;quot;,1.03398e-25,3.69099e+08],[&amp;quot;-2^-85&amp;quot;,5.16988e-26,3.6071e+08],[&amp;quot;-2^-86&amp;quot;,2.58494e-26,3.52322e+08],[&amp;quot;-2^-87&amp;quot;,1.29247e-26,3.43933e+08],[&amp;quot;-2^-88&amp;quot;,6.46235e-27,3.35544e+08],[&amp;quot;-2^-89&amp;quot;,3.23117e-27,3.27156e+08],[&amp;quot;-2^-90&amp;quot;,1.61559e-27,3.18767e+08],[&amp;quot;-2^-91&amp;quot;,8.07794e-28,3.10378e+08],[&amp;quot;-2^-92&amp;quot;,4.03897e-28,3.0199e+08],[&amp;quot;-2^-93&amp;quot;,2.01948e-28,2.93601e+08],[&amp;quot;-2^-94&amp;quot;,1.00974e-28,2.85213e+08],[&amp;quot;-2^-95&amp;quot;,5.04871e-29,2.76824e+08],[&amp;quot;-2^-96&amp;quot;,2.52435e-29,2.68435e+08],[&amp;quot;-2^-97&amp;quot;,1.26218e-29,2.60047e+08],[&amp;quot;-2^-98&amp;quot;,6.31089e-30,2.51658e+08],[&amp;quot;-2^-99&amp;quot;,3.15544e-30,2.4327e+08],[&amp;quot;-2^-100&amp;quot;,1.57772e-30,2.34881e+08],[&amp;quot;-2^-101&amp;quot;,7.88861e-31,2.26492e+08],[&amp;quot;-2^-102&amp;quot;,3.9443e-31,2.18104e+08],[&amp;quot;-2^-103&amp;quot;,1.97215e-31,2.09715e+08],[&amp;quot;-2^-104&amp;quot;,9.86076e-32,2.01327e+08],[&amp;quot;-2^-105&amp;quot;,4.93038e-32,1.92938e+08],[&amp;quot;-2^-106&amp;quot;,2.46519e-32,1.84549e+08],[&amp;quot;-2^-107&amp;quot;,1.2326e-32,1.76161e+08],[&amp;quot;-2^-108&amp;quot;,6.16298e-33,1.67772e+08],[&amp;quot;-2^-109&amp;quot;,3.08149e-33,1.59384e+08],[&amp;quot;-2^-110&amp;quot;,1.54074e-33,1.50995e+08],[&amp;quot;-2^-111&amp;quot;,7.70372e-34,1.42606e+08],[&amp;quot;-2^-112&amp;quot;,3.85186e-34,1.34218e+08],[&amp;quot;-2^-113&amp;quot;,1.92593e-34,1.25829e+08],[&amp;quot;-2^-114&amp;quot;,9.62965e-35,1.17441e+08],[&amp;quot;-2^-115&amp;quot;,4.81482e-35,1.09052e+08],[&amp;quot;-2^-116&amp;quot;,2.40741e-35,1.00663e+08],[&amp;quot;-2^-117&amp;quot;,1.20371e-35,9.22747e+07],[&amp;quot;-2^-118&amp;quot;,6.01853e-36,8.38861e+07],[&amp;quot;-2^-119&amp;quot;,3.00927e-36,7.54975e+07],[&amp;quot;-2^-120&amp;quot;,1.50463e-36,6.71089e+07],[&amp;quot;-2^-121&amp;quot;,7.52316e-37,5.87203e+07],[&amp;quot;-2^-122&amp;quot;,3.76158e-37,5.03316e+07],[&amp;quot;-2^-123&amp;quot;,1.88079e-37,4.1943e+07],[&amp;quot;-2^-124&amp;quot;,9.40395e-38,3.35544e+07],[&amp;quot;-2^-125&amp;quot;,4.70198e-38,2.51658e+07],[&amp;quot;-2^-126&amp;quot;,2.35099e-38,1.67772e+07],[&amp;quot;-SN&amp;quot;,1.17546e-38,8.38835e+06],[&amp;quot;0&amp;quot;,0,1.17549e-38],[&amp;quot;+SN&amp;quot;,1.17546e-38,8.38835e+06],[&amp;quot;+2^-126&amp;quot;,2.35095e-38,1.6777e+07],[&amp;quot;+2^-125&amp;quot;,4.70191e-38,2.51656e+07],[&amp;quot;+2^-124&amp;quot;,9.40381e-38,3.35542e+07],[&amp;quot;+2^-123&amp;quot;,1.88076e-37,4.19428e+07],[&amp;quot;+2^-122&amp;quot;,3.76152e-37,5.03314e+07],[&amp;quot;+2^-121&amp;quot;,7.52305e-37,5.872e+07],[&amp;quot;+2^-120&amp;quot;,1.50461e-36,6.71086e+07],[&amp;quot;+2^-119&amp;quot;,3.00922e-36,7.54972e+07],[&amp;quot;+2^-118&amp;quot;,6.01844e-36,8.38858e+07],[&amp;quot;+2^-117&amp;quot;,1.20369e-35,9.22744e+07],[&amp;quot;+2^-116&amp;quot;,2.40738e-35,1.00663e+08],[&amp;quot;+2^-115&amp;quot;,4.81475e-35,1.09052e+08],[&amp;quot;+2^-114&amp;quot;,9.6295e-35,1.1744e+08],[&amp;quot;+2^-113&amp;quot;,1.9259e-34,1.25829e+08],[&amp;quot;+2^-112&amp;quot;,3.8518e-34,1.34217e+08],[&amp;quot;+2^-111&amp;quot;,7.7036e-34,1.42606e+08],[&amp;quot;+2^-110&amp;quot;,1.54072e-33,1.50995e+08],[&amp;quot;+2^-109&amp;quot;,3.08144e-33,1.59383e+08],[&amp;quot;+2^-108&amp;quot;,6.16288e-33,1.67772e+08],[&amp;quot;+2^-107&amp;quot;,1.23258e-32,1.76161e+08],[&amp;quot;+2^-106&amp;quot;,2.46515e-32,1.84549e+08],[&amp;quot;+2^-105&amp;quot;,4.93031e-32,1.92938e+08],[&amp;quot;+2^-104&amp;quot;,9.86061e-32,2.01326e+08],[&amp;quot;+2^-103&amp;quot;,1.97212e-31,2.09715e+08],[&amp;quot;+2^-102&amp;quot;,3.94424e-31,2.18104e+08],[&amp;quot;+2^-101&amp;quot;,7.88849e-31,2.26492e+08],[&amp;quot;+2^-100&amp;quot;,1.5777e-30,2.34881e+08],[&amp;quot;+2^-99&amp;quot;,3.1554e-30,2.43269e+08],[&amp;quot;+2^-98&amp;quot;,6.31079e-30,2.51658e+08],[&amp;quot;+2^-97&amp;quot;,1.26216e-29,2.60047e+08],[&amp;quot;+2^-96&amp;quot;,2.52432e-29,2.68435e+08],[&amp;quot;+2^-95&amp;quot;,5.04863e-29,2.76824e+08],[&amp;quot;+2^-94&amp;quot;,1.00973e-28,2.85212e+08],[&amp;quot;+2^-93&amp;quot;,2.01945e-28,2.93601e+08],[&amp;quot;+2^-92&amp;quot;,4.03891e-28,3.0199e+08],[&amp;quot;+2^-91&amp;quot;,8.07781e-28,3.10378e+08],[&amp;quot;+2^-90&amp;quot;,1.61556e-27,3.18767e+08],[&amp;quot;+2^-89&amp;quot;,3.23112e-27,3.27155e+08],[&amp;quot;+2^-88&amp;quot;,6.46225e-27,3.35544e+08],[&amp;quot;+2^-87&amp;quot;,1.29245e-26,3.43933e+08],[&amp;quot;+2^-86&amp;quot;,2.5849e-26,3.52321e+08],[&amp;quot;+2^-85&amp;quot;,5.1698e-26,3.6071e+08],[&amp;quot;+2^-84&amp;quot;,1.03396e-25,3.69098e+08],[&amp;quot;+2^-83&amp;quot;,2.06792e-25,3.77487e+08],[&amp;quot;+2^-82&amp;quot;,4.13584e-25,3.85876e+08],[&amp;quot;+2^-81&amp;quot;,8.27168e-25,3.94264e+08],[&amp;quot;+2^-80&amp;quot;,1.65434e-24,4.02653e+08],[&amp;quot;+2^-79&amp;quot;,3.30867e-24,4.11042e+08],[&amp;quot;+2^-78&amp;quot;,6.61734e-24,4.1943e+08],[&amp;quot;+2^-77&amp;quot;,1.32347e-23,4.27819e+08],[&amp;quot;+2^-76&amp;quot;,2.64694e-23,4.36207e+08],[&amp;quot;+2^-75&amp;quot;,5.29388e-23,4.44596e+08],[&amp;quot;+2^-74&amp;quot;,1.05878e-22,4.52985e+08],[&amp;quot;+2^-73&amp;quot;,2.11755e-22,4.61373e+08],[&amp;quot;+2^-72&amp;quot;,4.2351e-22,4.69762e+08],[&amp;quot;+2^-71&amp;quot;,8.4702e-22,4.7815e+08],[&amp;quot;+2^-70&amp;quot;,1.69404e-21,4.86539e+08],[&amp;quot;+2^-69&amp;quot;,3.38808e-21,4.94928e+08],[&amp;quot;+2^-68&amp;quot;,6.77616e-21,5.03316e+08],[&amp;quot;+2^-67&amp;quot;,1.35523e-20,5.11705e+08],[&amp;quot;+2^-66&amp;quot;,2.71046e-20,5.20093e+08],[&amp;quot;+2^-65&amp;quot;,5.42093e-20,5.28482e+08],[&amp;quot;+2^-64&amp;quot;,1.08419e-19,5.36871e+08],[&amp;quot;+2^-63&amp;quot;,2.16837e-19,5.45259e+08],[&amp;quot;+2^-62&amp;quot;,4.33674e-19,5.53648e+08],[&amp;quot;+2^-61&amp;quot;,8.67349e-19,5.62036e+08],[&amp;quot;+2^-60&amp;quot;,1.7347e-18,5.70425e+08],[&amp;quot;+2^-59&amp;quot;,3.46939e-18,5.78814e+08],[&amp;quot;+2^-58&amp;quot;,6.93879e-18,5.87202e+08],[&amp;quot;+2^-57&amp;quot;,1.38776e-17,5.95591e+08],[&amp;quot;+2^-56&amp;quot;,2.77552e-17,6.0398e+08],[&amp;quot;+2^-55&amp;quot;,5.55103e-17,6.12368e+08],[&amp;quot;+2^-54&amp;quot;,1.11021e-16,6.20757e+08],[&amp;quot;+2^-53&amp;quot;,2.22041e-16,6.29145e+08],[&amp;quot;+2^-52&amp;quot;,4.44082e-16,6.37534e+08],[&amp;quot;+2^-51&amp;quot;,8.88165e-16,6.45923e+08],[&amp;quot;+2^-50&amp;quot;,1.77633e-15,6.54311e+08],[&amp;quot;+2^-49&amp;quot;,3.55266e-15,6.627e+08],[&amp;quot;+2^-48&amp;quot;,7.10532e-15,6.71088e+08],[&amp;quot;+2^-47&amp;quot;,1.42106e-14,6.79477e+08],[&amp;quot;+2^-46&amp;quot;,2.84213e-14,6.87866e+08],[&amp;quot;+2^-45&amp;quot;,5.68426e-14,6.96254e+08],[&amp;quot;+2^-44&amp;quot;,1.13685e-13,7.04643e+08],[&amp;quot;+2^-43&amp;quot;,2.2737e-13,7.13031e+08],[&amp;quot;+2^-42&amp;quot;,4.5474e-13,7.2142e+08],[&amp;quot;+2^-41&amp;quot;,9.09481e-13,7.29809e+08],[&amp;quot;+2^-40&amp;quot;,1.81896e-12,7.38197e+08],[&amp;quot;+2^-39&amp;quot;,3.63792e-12,7.46586e+08],[&amp;quot;+2^-38&amp;quot;,7.27585e-12,7.54974e+08],[&amp;quot;+2^-37&amp;quot;,1.45517e-11,7.63363e+08],[&amp;quot;+2^-36&amp;quot;,2.91034e-11,7.71752e+08],[&amp;quot;+2^-35&amp;quot;,5.82068e-11,7.8014e+08],[&amp;quot;+2^-34&amp;quot;,1.16414e-10,7.88529e+08],[&amp;quot;+2^-33&amp;quot;,2.32827e-10,7.96918e+08],[&amp;quot;+2^-32&amp;quot;,4.65654e-10,8.05306e+08],[&amp;quot;+2^-31&amp;quot;,9.31308e-10,8.13695e+08],[&amp;quot;+2^-30&amp;quot;,1.86262e-09,8.22083e+08],[&amp;quot;+2^-29&amp;quot;,3.72523e-09,8.30472e+08],[&amp;quot;+2^-28&amp;quot;,7.45047e-09,8.38861e+08],[&amp;quot;+2^-27&amp;quot;,1.49009e-08,8.47249e+08],[&amp;quot;+2^-26&amp;quot;,2.98019e-08,8.55638e+08],[&amp;quot;+2^-25&amp;quot;,2.98023e-08,8.38861e+06],[&amp;quot;+2^-24&amp;quot;,2.98023e-08,4.1943e+06],[&amp;quot;+2^-23&amp;quot;,2.98023e-08,2.09715e+06],[&amp;quot;+2^-22&amp;quot;,2.98023e-08,1.04857e+06],[&amp;quot;+2^-21&amp;quot;,2.98021e-08,524284],[&amp;quot;+2^-20&amp;quot;,2.98014e-08,262136],[&amp;quot;+2^-19&amp;quot;,2.97987e-08,131056],[&amp;quot;+2^-18&amp;quot;,2.97878e-08,65504],[&amp;quot;+2^-17&amp;quot;,2.98014e-08,32767],[&amp;quot;+2^-16&amp;quot;,2.98005e-08,16384],[&amp;quot;+2^-15&amp;quot;,2.98023e-08,8192],[&amp;quot;+2^-14&amp;quot;,2.98025e-08,4351.99],[&amp;quot;+2^-13&amp;quot;,2.98044e-08,2815.96],[&amp;quot;+2^-12&amp;quot;,2.98091e-08,1024.23],[&amp;quot;+2^-11&amp;quot;,2.9812e-08,512.167],[&amp;quot;+2^-10&amp;quot;,2.98154e-08,256.113],[&amp;quot;+2^-9&amp;quot;,2.98591e-08,128.244],[&amp;quot;+2^-8&amp;quot;,3.00204e-08,85.3328],[&amp;quot;+2^-7&amp;quot;,3.01947e-08,43.675],[&amp;quot;+2^-6&amp;quot;,3.05713e-08,30.45],[&amp;quot;+2^-5&amp;quot;,3.14319e-08,8.43743],[&amp;quot;+2^-4&amp;quot;,3.31291e-08,7.77058],[&amp;quot;+2^-3&amp;quot;,3.58597e-08,3.99659],[&amp;quot;+2^-2&amp;quot;,4.14639e-08,2.29246],[&amp;quot;+2^-1&amp;quot;,8.02409e-08,1.34622],[&amp;quot;+2^0&amp;quot;,8.66201e-08,1.45324],[&amp;quot;+2^1&amp;quot;,8.8654e-08,1.48737],[&amp;quot;+2^2&amp;quot;,6.7976e-08,1.14045],[&amp;quot;+2^3&amp;quot;,1.33955e-07,2.2474],[&amp;quot;+2^4&amp;quot;,2.53131e-14,4.24683e-07],[&amp;quot;+2^5&amp;quot;,0,null],[&amp;quot;+2^6&amp;quot;,0,null],[&amp;quot;+2^7&amp;quot;,0,null],[&amp;quot;+2^8&amp;quot;,0,null],[&amp;quot;+2^9&amp;quot;,0,null],[&amp;quot;+2^10&amp;quot;,0,null],[&amp;quot;+2^11&amp;quot;,0,null],[&amp;quot;+2^12&amp;quot;,0,null],[&amp;quot;+2^13&amp;quot;,0,null],[&amp;quot;+2^14&amp;quot;,0,null],[&amp;quot;+2^15&amp;quot;,0,null],[&amp;quot;+2^16&amp;quot;,0,null],[&amp;quot;+2^17&amp;quot;,0,null],[&amp;quot;+2^18&amp;quot;,0,null],[&amp;quot;+2^19&amp;quot;,0,null],[&amp;quot;+2^20&amp;quot;,0,null],[&amp;quot;+2^21&amp;quot;,0,null],[&amp;quot;+2^22&amp;quot;,0,null],[&amp;quot;+2^23&amp;quot;,0,null],[&amp;quot;+2^24&amp;quot;,0,null],[&amp;quot;+2^25&amp;quot;,0,null],[&amp;quot;+2^26&amp;quot;,0,null],[&amp;quot;+2^27&amp;quot;,0,null],[&amp;quot;+2^28&amp;quot;,0,null],[&amp;quot;+2^29&amp;quot;,0,null],[&amp;quot;+2^30&amp;quot;,0,null],[&amp;quot;+2^31&amp;quot;,0,null],[&amp;quot;+2^32&amp;quot;,0,null],[&amp;quot;+2^33&amp;quot;,0,null],[&amp;quot;+2^34&amp;quot;,0,null],[&amp;quot;+2^35&amp;quot;,0,null],[&amp;quot;+2^36&amp;quot;,0,null],[&amp;quot;+2^37&amp;quot;,0,null],[&amp;quot;+2^38&amp;quot;,0,null],[&amp;quot;+2^39&amp;quot;,0,null],[&amp;quot;+2^40&amp;quot;,0,null],[&amp;quot;+2^41&amp;quot;,0,null],[&amp;quot;+2^42&amp;quot;,0,null],[&amp;quot;+2^43&amp;quot;,0,null],[&amp;quot;+2^44&amp;quot;,0,null],[&amp;quot;+2^45&amp;quot;,0,null],[&amp;quot;+2^46&amp;quot;,0,null],[&amp;quot;+2^47&amp;quot;,0,null],[&amp;quot;+2^48&amp;quot;,0,null],[&amp;quot;+2^49&amp;quot;,0,null],[&amp;quot;+2^50&amp;quot;,0,null],[&amp;quot;+2^51&amp;quot;,0,null],[&amp;quot;+2^52&amp;quot;,0,null],[&amp;quot;+2^53&amp;quot;,0,null],[&amp;quot;+2^54&amp;quot;,0,null],[&amp;quot;+2^55&amp;quot;,0,null],[&amp;quot;+2^56&amp;quot;,0,null],[&amp;quot;+2^57&amp;quot;,0,null],[&amp;quot;+2^58&amp;quot;,0,null],[&amp;quot;+2^59&amp;quot;,0,null],[&amp;quot;+2^60&amp;quot;,0,null],[&amp;quot;+2^61&amp;quot;,0,null],[&amp;quot;+2^62&amp;quot;,0,null],[&amp;quot;+2^63&amp;quot;,0,null],[&amp;quot;+2^64&amp;quot;,0,null],[&amp;quot;+2^65&amp;quot;,0,null],[&amp;quot;+2^66&amp;quot;,0,null],[&amp;quot;+2^67&amp;quot;,0,null],[&amp;quot;+2^68&amp;quot;,0,null],[&amp;quot;+2^69&amp;quot;,0,null],[&amp;quot;+2^70&amp;quot;,0,null],[&amp;quot;+2^71&amp;quot;,0,null],[&amp;quot;+2^72&amp;quot;,0,null],[&amp;quot;+2^73&amp;quot;,0,null],[&amp;quot;+2^74&amp;quot;,0,null],[&amp;quot;+2^75&amp;quot;,0,null],[&amp;quot;+2^76&amp;quot;,0,null],[&amp;quot;+2^77&amp;quot;,0,null],[&amp;quot;+2^78&amp;quot;,0,null],[&amp;quot;+2^79&amp;quot;,0,null],[&amp;quot;+2^80&amp;quot;,0,null],[&amp;quot;+2^81&amp;quot;,0,null],[&amp;quot;+2^82&amp;quot;,0,null],[&amp;quot;+2^83&amp;quot;,0,null],[&amp;quot;+2^84&amp;quot;,0,null],[&amp;quot;+2^85&amp;quot;,0,null],[&amp;quot;+2^86&amp;quot;,0,null],[&amp;quot;+2^87&amp;quot;,0,null],[&amp;quot;+2^88&amp;quot;,0,null],[&amp;quot;+2^89&amp;quot;,0,null],[&amp;quot;+2^90&amp;quot;,0,null],[&amp;quot;+2^91&amp;quot;,0,null],[&amp;quot;+2^92&amp;quot;,0,null],[&amp;quot;+2^93&amp;quot;,0,null],[&amp;quot;+2^94&amp;quot;,0,null],[&amp;quot;+2^95&amp;quot;,0,null],[&amp;quot;+2^96&amp;quot;,0,null],[&amp;quot;+2^97&amp;quot;,0,null],[&amp;quot;+2^98&amp;quot;,0,null],[&amp;quot;+2^99&amp;quot;,0,null],[&amp;quot;+2^100&amp;quot;,0,null],[&amp;quot;+2^101&amp;quot;,0,null],[&amp;quot;+2^102&amp;quot;,0,null],[&amp;quot;+2^103&amp;quot;,0,null],[&amp;quot;+2^104&amp;quot;,0,null],[&amp;quot;+2^105&amp;quot;,0,null],[&amp;quot;+2^106&amp;quot;,0,null],[&amp;quot;+2^107&amp;quot;,0,null],[&amp;quot;+2^108&amp;quot;,0,null],[&amp;quot;+2^109&amp;quot;,0,null],[&amp;quot;+2^110&amp;quot;,0,null],[&amp;quot;+2^111&amp;quot;,0,null],[&amp;quot;+2^112&amp;quot;,0,null],[&amp;quot;+2^113&amp;quot;,0,null],[&amp;quot;+2^114&amp;quot;,0,null],[&amp;quot;+2^115&amp;quot;,0,null],[&amp;quot;+2^116&amp;quot;,0,null],[&amp;quot;+2^117&amp;quot;,0,null],[&amp;quot;+2^118&amp;quot;,0,null],[&amp;quot;+2^119&amp;quot;,0,null],[&amp;quot;+2^120&amp;quot;,0,null],[&amp;quot;+2^121&amp;quot;,0,null],[&amp;quot;+2^122&amp;quot;,0,null],[&amp;quot;+2^123&amp;quot;,0,null],[&amp;quot;+2^124&amp;quot;,0,null],[&amp;quot;+2^125&amp;quot;,0,null],[&amp;quot;+2^126&amp;quot;,0,null],[&amp;quot;+2^127&amp;quot;,0,null] ]&lt;br /&gt;
 }}&lt;br /&gt;
=== expm1 ===&lt;br /&gt;
&lt;br /&gt;
{{#widget:FloatErrorGraph|id=tanhf_expm1&lt;br /&gt;
  |data=[ [&amp;quot;-2^127&amp;quot;,0,1.17549e-38],[&amp;quot;-2^126&amp;quot;,0,1.17549e-38],[&amp;quot;-2^125&amp;quot;,0,1.17549e-38],[&amp;quot;-2^124&amp;quot;,0,1.17549e-38],[&amp;quot;-2^123&amp;quot;,0,1.17549e-38],[&amp;quot;-2^122&amp;quot;,0,1.17549e-38],[&amp;quot;-2^121&amp;quot;,0,1.17549e-38],[&amp;quot;-2^120&amp;quot;,0,1.17549e-38],[&amp;quot;-2^119&amp;quot;,0,1.17549e-38],[&amp;quot;-2^118&amp;quot;,0,1.17549e-38],[&amp;quot;-2^117&amp;quot;,0,1.17549e-38],[&amp;quot;-2^116&amp;quot;,0,1.17549e-38],[&amp;quot;-2^115&amp;quot;,0,1.17549e-38],[&amp;quot;-2^114&amp;quot;,0,1.17549e-38],[&amp;quot;-2^113&amp;quot;,0,1.17549e-38],[&amp;quot;-2^112&amp;quot;,0,1.17549e-38],[&amp;quot;-2^111&amp;quot;,0,1.17549e-38],[&amp;quot;-2^110&amp;quot;,0,1.17549e-38],[&amp;quot;-2^109&amp;quot;,0,1.17549e-38],[&amp;quot;-2^108&amp;quot;,0,1.17549e-38],[&amp;quot;-2^107&amp;quot;,0,1.17549e-38],[&amp;quot;-2^106&amp;quot;,0,1.17549e-38],[&amp;quot;-2^105&amp;quot;,0,1.17549e-38],[&amp;quot;-2^104&amp;quot;,0,1.17549e-38],[&amp;quot;-2^103&amp;quot;,0,1.17549e-38],[&amp;quot;-2^102&amp;quot;,0,1.17549e-38],[&amp;quot;-2^101&amp;quot;,0,1.17549e-38],[&amp;quot;-2^100&amp;quot;,0,1.17549e-38],[&amp;quot;-2^99&amp;quot;,0,1.17549e-38],[&amp;quot;-2^98&amp;quot;,0,1.17549e-38],[&amp;quot;-2^97&amp;quot;,0,1.17549e-38],[&amp;quot;-2^96&amp;quot;,0,1.17549e-38],[&amp;quot;-2^95&amp;quot;,0,1.17549e-38],[&amp;quot;-2^94&amp;quot;,0,1.17549e-38],[&amp;quot;-2^93&amp;quot;,0,1.17549e-38],[&amp;quot;-2^92&amp;quot;,0,1.17549e-38],[&amp;quot;-2^91&amp;quot;,0,1.17549e-38],[&amp;quot;-2^90&amp;quot;,0,1.17549e-38],[&amp;quot;-2^89&amp;quot;,0,1.17549e-38],[&amp;quot;-2^88&amp;quot;,0,1.17549e-38],[&amp;quot;-2^87&amp;quot;,0,1.17549e-38],[&amp;quot;-2^86&amp;quot;,0,1.17549e-38],[&amp;quot;-2^85&amp;quot;,0,1.17549e-38],[&amp;quot;-2^84&amp;quot;,0,1.17549e-38],[&amp;quot;-2^83&amp;quot;,0,1.17549e-38],[&amp;quot;-2^82&amp;quot;,0,1.17549e-38],[&amp;quot;-2^81&amp;quot;,0,1.17549e-38],[&amp;quot;-2^80&amp;quot;,0,1.17549e-38],[&amp;quot;-2^79&amp;quot;,0,1.17549e-38],[&amp;quot;-2^78&amp;quot;,0,1.17549e-38],[&amp;quot;-2^77&amp;quot;,0,1.17549e-38],[&amp;quot;-2^76&amp;quot;,0,1.17549e-38],[&amp;quot;-2^75&amp;quot;,0,1.17549e-38],[&amp;quot;-2^74&amp;quot;,0,1.17549e-38],[&amp;quot;-2^73&amp;quot;,0,1.17549e-38],[&amp;quot;-2^72&amp;quot;,0,1.17549e-38],[&amp;quot;-2^71&amp;quot;,0,1.17549e-38],[&amp;quot;-2^70&amp;quot;,0,1.17549e-38],[&amp;quot;-2^69&amp;quot;,0,1.17549e-38],[&amp;quot;-2^68&amp;quot;,0,1.17549e-38],[&amp;quot;-2^67&amp;quot;,0,1.17549e-38],[&amp;quot;-2^66&amp;quot;,0,1.17549e-38],[&amp;quot;-2^65&amp;quot;,0,1.17549e-38],[&amp;quot;-2^64&amp;quot;,0,1.17549e-38],[&amp;quot;-2^63&amp;quot;,0,1.17549e-38],[&amp;quot;-2^62&amp;quot;,0,1.17549e-38],[&amp;quot;-2^61&amp;quot;,0,1.17549e-38],[&amp;quot;-2^60&amp;quot;,0,1.17549e-38],[&amp;quot;-2^59&amp;quot;,0,1.17549e-38],[&amp;quot;-2^58&amp;quot;,0,1.17549e-38],[&amp;quot;-2^57&amp;quot;,0,1.17549e-38],[&amp;quot;-2^56&amp;quot;,0,1.17549e-38],[&amp;quot;-2^55&amp;quot;,0,1.17549e-38],[&amp;quot;-2^54&amp;quot;,0,1.17549e-38],[&amp;quot;-2^53&amp;quot;,0,1.17549e-38],[&amp;quot;-2^52&amp;quot;,0,1.17549e-38],[&amp;quot;-2^51&amp;quot;,0,1.17549e-38],[&amp;quot;-2^50&amp;quot;,0,1.17549e-38],[&amp;quot;-2^49&amp;quot;,0,1.17549e-38],[&amp;quot;-2^48&amp;quot;,0,1.17549e-38],[&amp;quot;-2^47&amp;quot;,0,1.17549e-38],[&amp;quot;-2^46&amp;quot;,0,1.17549e-38],[&amp;quot;-2^45&amp;quot;,0,1.17549e-38],[&amp;quot;-2^44&amp;quot;,0,1.17549e-38],[&amp;quot;-2^43&amp;quot;,0,1.17549e-38],[&amp;quot;-2^42&amp;quot;,0,1.17549e-38],[&amp;quot;-2^41&amp;quot;,0,1.17549e-38],[&amp;quot;-2^40&amp;quot;,0,1.17549e-38],[&amp;quot;-2^39&amp;quot;,0,1.17549e-38],[&amp;quot;-2^38&amp;quot;,0,1.17549e-38],[&amp;quot;-2^37&amp;quot;,0,1.17549e-38],[&amp;quot;-2^36&amp;quot;,0,1.17549e-38],[&amp;quot;-2^35&amp;quot;,0,1.17549e-38],[&amp;quot;-2^34&amp;quot;,0,1.17549e-38],[&amp;quot;-2^33&amp;quot;,0,1.17549e-38],[&amp;quot;-2^32&amp;quot;,0,1.17549e-38],[&amp;quot;-2^31&amp;quot;,0,1.17549e-38],[&amp;quot;-2^30&amp;quot;,0,1.17549e-38],[&amp;quot;-2^29&amp;quot;,0,1.17549e-38],[&amp;quot;-2^28&amp;quot;,0,1.17549e-38],[&amp;quot;-2^27&amp;quot;,0,1.17549e-38],[&amp;quot;-2^26&amp;quot;,0,1.17549e-38],[&amp;quot;-2^25&amp;quot;,0,1.17549e-38],[&amp;quot;-2^24&amp;quot;,0,1.17549e-38],[&amp;quot;-2^23&amp;quot;,0,1.17549e-38],[&amp;quot;-2^22&amp;quot;,0,1.17549e-38],[&amp;quot;-2^21&amp;quot;,0,1.17549e-38],[&amp;quot;-2^20&amp;quot;,0,1.17549e-38],[&amp;quot;-2^19&amp;quot;,0,1.17549e-38],[&amp;quot;-2^18&amp;quot;,0,1.17549e-38],[&amp;quot;-2^17&amp;quot;,0,1.17549e-38],[&amp;quot;-2^16&amp;quot;,0,1.17549e-38],[&amp;quot;-2^15&amp;quot;,0,1.17549e-38],[&amp;quot;-2^14&amp;quot;,0,1.17549e-38],[&amp;quot;-2^13&amp;quot;,0,1.17549e-38],[&amp;quot;-2^12&amp;quot;,0,1.17549e-38],[&amp;quot;-2^11&amp;quot;,0,1.17549e-38],[&amp;quot;-2^10&amp;quot;,0,1.17549e-38],[&amp;quot;-2^9&amp;quot;,0,1.17549e-38],[&amp;quot;-2^8&amp;quot;,0,1.17549e-38],[&amp;quot;-2^7&amp;quot;,0,1.17549e-38],[&amp;quot;-2^6&amp;quot;,0,1.17549e-38],[&amp;quot;-2^5&amp;quot;,0,1.17549e-38],[&amp;quot;-2^4&amp;quot;,2.53131e-14,4.24683e-07],[&amp;quot;-2^3&amp;quot;,1.19138e-07,1.9988],[&amp;quot;-2^2&amp;quot;,1.46791e-07,2.46274],[&amp;quot;-2^1&amp;quot;,1.47723e-07,2.47839],[&amp;quot;-2^0&amp;quot;,1.40758e-07,2.36153],[&amp;quot;-2^-1&amp;quot;,1.13422e-07,2.23445],[&amp;quot;-2^-2&amp;quot;,6.38012e-08,2.14081],[&amp;quot;-2^-3&amp;quot;,2.69538e-08,1.80884],[&amp;quot;-2^-4&amp;quot;,1.21308e-08,1.62817],[&amp;quot;-2^-5&amp;quot;,5.59535e-09,1.50199],[&amp;quot;-2^-6&amp;quot;,2.69177e-09,1.44513],[&amp;quot;-2^-7&amp;quot;,1.31289e-09,1.4097],[&amp;quot;-2^-8&amp;quot;,6.49688e-10,1.3952],[&amp;quot;-2^-9&amp;quot;,3.32043e-10,1.42611],[&amp;quot;-2^-10&amp;quot;,1.66275e-10,1.4283],[&amp;quot;-2^-11&amp;quot;,8.08595e-11,1.38916],[&amp;quot;-2^-12&amp;quot;,3.8939e-11,1.33794],[&amp;quot;-2^-13&amp;quot;,1.9401e-11,1.33323],[&amp;quot;-2^-14&amp;quot;,7.88212e-12,1.08331],[&amp;quot;-2^-15&amp;quot;,3.71372e-12,1.02082],[&amp;quot;-2^-16&amp;quot;,1.82845e-12,1.0052],[&amp;quot;-2^-17&amp;quot;,9.10676e-13,1.0013],[&amp;quot;-2^-18&amp;quot;,4.54895e-13,1.00032],[&amp;quot;-2^-19&amp;quot;,2.27392e-13,1.00008],[&amp;quot;-2^-20&amp;quot;,1.13689e-13,1.00002],[&amp;quot;-2^-21&amp;quot;,5.68437e-14,1],[&amp;quot;-2^-22&amp;quot;,2.84217e-14,1],[&amp;quot;-2^-23&amp;quot;,1.42109e-14,1],[&amp;quot;-2^-24&amp;quot;,7.10543e-15,1],[&amp;quot;-2^-25&amp;quot;,3.55271e-15,1],[&amp;quot;-2^-26&amp;quot;,9.92617e-24,5.58794e-09],[&amp;quot;-2^-27&amp;quot;,1.65436e-24,1.86265e-09],[&amp;quot;-2^-28&amp;quot;,8.27181e-25,1.86265e-09],[&amp;quot;-2^-29&amp;quot;,4.1359e-25,1.86265e-09],[&amp;quot;-2^-30&amp;quot;,2.06795e-25,1.86265e-09],[&amp;quot;-2^-31&amp;quot;,1.03398e-25,1.86265e-09],[&amp;quot;-2^-32&amp;quot;,5.16988e-26,1.86265e-09],[&amp;quot;-2^-33&amp;quot;,2.58494e-26,1.86265e-09],[&amp;quot;-2^-34&amp;quot;,1.29247e-26,1.86265e-09],[&amp;quot;-2^-35&amp;quot;,6.46235e-27,1.86265e-09],[&amp;quot;-2^-36&amp;quot;,3.23117e-27,1.86265e-09],[&amp;quot;-2^-37&amp;quot;,1.61559e-27,1.86265e-09],[&amp;quot;-2^-38&amp;quot;,8.07794e-28,1.86265e-09],[&amp;quot;-2^-39&amp;quot;,4.03897e-28,1.86265e-09],[&amp;quot;-2^-40&amp;quot;,2.01948e-28,1.86265e-09],[&amp;quot;-2^-41&amp;quot;,1.00974e-28,1.86265e-09],[&amp;quot;-2^-42&amp;quot;,5.04871e-29,1.86265e-09],[&amp;quot;-2^-43&amp;quot;,2.52435e-29,1.86265e-09],[&amp;quot;-2^-44&amp;quot;,1.26218e-29,1.86265e-09],[&amp;quot;-2^-45&amp;quot;,6.31089e-30,1.86265e-09],[&amp;quot;-2^-46&amp;quot;,3.15544e-30,1.86265e-09],[&amp;quot;-2^-47&amp;quot;,1.57772e-30,1.86265e-09],[&amp;quot;-2^-48&amp;quot;,7.88861e-31,1.86265e-09],[&amp;quot;-2^-49&amp;quot;,3.9443e-31,1.86265e-09],[&amp;quot;-2^-50&amp;quot;,1.97215e-31,1.86265e-09],[&amp;quot;-2^-51&amp;quot;,9.86076e-32,1.86265e-09],[&amp;quot;-2^-52&amp;quot;,4.93038e-32,1.86265e-09],[&amp;quot;-2^-53&amp;quot;,2.46519e-32,1.86265e-09],[&amp;quot;-2^-54&amp;quot;,1.2326e-32,1.86265e-09],[&amp;quot;-2^-55&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-56&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-57&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-58&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-59&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-60&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-61&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-62&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-63&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-64&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-65&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-66&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-67&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-68&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-69&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-70&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-71&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-72&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-73&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-74&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-75&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-76&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-77&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-78&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-79&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-80&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-81&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-82&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-83&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-84&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-85&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-86&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-87&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-88&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-89&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-90&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-91&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-92&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-93&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-94&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-95&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-96&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-97&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-98&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-99&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-100&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-101&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-102&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-103&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-104&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-105&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-106&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-107&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-108&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-109&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-110&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-111&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-112&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-113&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-114&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-115&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-116&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-117&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-118&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-119&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-120&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-121&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-122&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-123&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-124&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-125&amp;quot;,0,1.17549e-38],[&amp;quot;-2^-126&amp;quot;,0,1.17549e-38],[&amp;quot;-SN&amp;quot;,0,1.17549e-38],[&amp;quot;0&amp;quot;,0,1.17549e-38],[&amp;quot;+SN&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-126&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-125&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-124&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-123&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-122&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-121&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-120&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-119&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-118&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-117&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-116&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-115&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-114&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-113&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-112&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-111&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-110&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-109&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-108&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-107&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-106&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-105&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-104&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-103&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-102&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-101&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-100&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-99&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-98&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-97&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-96&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-95&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-94&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-93&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-92&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-91&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-90&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-89&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-88&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-87&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-86&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-85&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-84&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-83&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-82&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-81&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-80&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-79&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-78&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-77&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-76&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-75&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-74&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-73&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-72&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-71&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-70&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-69&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-68&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-67&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-66&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-65&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-64&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-63&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-62&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-61&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-60&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-59&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-58&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-57&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-56&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-55&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-54&amp;quot;,1.2326e-32,1.86265e-09],[&amp;quot;+2^-53&amp;quot;,2.46519e-32,1.86265e-09],[&amp;quot;+2^-52&amp;quot;,4.93038e-32,1.86265e-09],[&amp;quot;+2^-51&amp;quot;,9.86076e-32,1.86265e-09],[&amp;quot;+2^-50&amp;quot;,1.97215e-31,1.86265e-09],[&amp;quot;+2^-49&amp;quot;,3.9443e-31,1.86265e-09],[&amp;quot;+2^-48&amp;quot;,7.88861e-31,1.86265e-09],[&amp;quot;+2^-47&amp;quot;,1.57772e-30,1.86265e-09],[&amp;quot;+2^-46&amp;quot;,3.15544e-30,1.86265e-09],[&amp;quot;+2^-45&amp;quot;,6.31089e-30,1.86265e-09],[&amp;quot;+2^-44&amp;quot;,1.26218e-29,1.86265e-09],[&amp;quot;+2^-43&amp;quot;,2.52435e-29,1.86265e-09],[&amp;quot;+2^-42&amp;quot;,5.04871e-29,1.86265e-09],[&amp;quot;+2^-41&amp;quot;,1.00974e-28,1.86265e-09],[&amp;quot;+2^-40&amp;quot;,2.01948e-28,1.86265e-09],[&amp;quot;+2^-39&amp;quot;,4.03897e-28,1.86265e-09],[&amp;quot;+2^-38&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-37&amp;quot;,1.61559e-27,1.86265e-09],[&amp;quot;+2^-36&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-35&amp;quot;,6.46235e-27,1.86265e-09],[&amp;quot;+2^-34&amp;quot;,0,1.17549e-38],[&amp;quot;+2^-33&amp;quot;,2.58494e-26,1.86265e-09],[&amp;quot;+2^-32&amp;quot;,5.16988e-26,1.86265e-09],[&amp;quot;+2^-31&amp;quot;,1.03398e-25,1.86265e-09],[&amp;quot;+2^-30&amp;quot;,2.06795e-25,1.86265e-09],[&amp;quot;+2^-29&amp;quot;,4.1359e-25,1.86265e-09],[&amp;quot;+2^-28&amp;quot;,8.27181e-25,1.86265e-09],[&amp;quot;+2^-27&amp;quot;,1.65436e-24,1.86265e-09],[&amp;quot;+2^-26&amp;quot;,9.92617e-24,5.58794e-09],[&amp;quot;+2^-25&amp;quot;,3.55271e-15,1],[&amp;quot;+2^-24&amp;quot;,7.10543e-15,1],[&amp;quot;+2^-23&amp;quot;,1.42109e-14,1],[&amp;quot;+2^-22&amp;quot;,2.84217e-14,1],[&amp;quot;+2^-21&amp;quot;,5.68437e-14,1],[&amp;quot;+2^-20&amp;quot;,1.13689e-13,1.00002],[&amp;quot;+2^-19&amp;quot;,2.27391e-13,1.00008],[&amp;quot;+2^-18&amp;quot;,4.54892e-13,1.00032],[&amp;quot;+2^-17&amp;quot;,9.10665e-13,1.00129],[&amp;quot;+2^-16&amp;quot;,1.82841e-12,1.00518],[&amp;quot;+2^-15&amp;quot;,3.71353e-12,1.02077],[&amp;quot;+2^-14&amp;quot;,1.39798e-11,1.92136],[&amp;quot;+2^-13&amp;quot;,2.63239e-11,1.80896],[&amp;quot;+2^-12&amp;quot;,5.37712e-11,1.84757],[&amp;quot;+2^-11&amp;quot;,1.09932e-10,1.88861],[&amp;quot;+2^-10&amp;quot;,2.24787e-10,1.93091],[&amp;quot;+2^-9&amp;quot;,4.3452e-10,1.86625],[&amp;quot;+2^-8&amp;quot;,1.01783e-09,2.18576],[&amp;quot;+2^-7&amp;quot;,2.10562e-09,2.26089],[&amp;quot;+2^-6&amp;quot;,4.03954e-09,2.16871],[&amp;quot;+2^-5&amp;quot;,8.15746e-09,2.18975],[&amp;quot;+2^-4&amp;quot;,1.5545e-08,2.08641],[&amp;quot;+2^-3&amp;quot;,2.62153e-08,2.05864],[&amp;quot;+2^-2&amp;quot;,4.09295e-08,1.73804],[&amp;quot;+2^-1&amp;quot;,8.02409e-08,1.34622],[&amp;quot;+2^0&amp;quot;,8.66201e-08,1.45324],[&amp;quot;+2^1&amp;quot;,8.8654e-08,1.48737],[&amp;quot;+2^2&amp;quot;,6.7976e-08,1.14045],[&amp;quot;+2^3&amp;quot;,7.44117e-08,1.24842],[&amp;quot;+2^4&amp;quot;,2.53131e-14,4.24683e-07],[&amp;quot;+2^5&amp;quot;,0,null],[&amp;quot;+2^6&amp;quot;,0,null],[&amp;quot;+2^7&amp;quot;,0,null],[&amp;quot;+2^8&amp;quot;,0,null],[&amp;quot;+2^9&amp;quot;,0,null],[&amp;quot;+2^10&amp;quot;,0,null],[&amp;quot;+2^11&amp;quot;,0,null],[&amp;quot;+2^12&amp;quot;,0,null],[&amp;quot;+2^13&amp;quot;,0,null],[&amp;quot;+2^14&amp;quot;,0,null],[&amp;quot;+2^15&amp;quot;,0,null],[&amp;quot;+2^16&amp;quot;,0,null],[&amp;quot;+2^17&amp;quot;,0,null],[&amp;quot;+2^18&amp;quot;,0,null],[&amp;quot;+2^19&amp;quot;,0,null],[&amp;quot;+2^20&amp;quot;,0,null],[&amp;quot;+2^21&amp;quot;,0,null],[&amp;quot;+2^22&amp;quot;,0,null],[&amp;quot;+2^23&amp;quot;,0,null],[&amp;quot;+2^24&amp;quot;,0,null],[&amp;quot;+2^25&amp;quot;,0,null],[&amp;quot;+2^26&amp;quot;,0,null],[&amp;quot;+2^27&amp;quot;,0,null],[&amp;quot;+2^28&amp;quot;,0,null],[&amp;quot;+2^29&amp;quot;,0,null],[&amp;quot;+2^30&amp;quot;,0,null],[&amp;quot;+2^31&amp;quot;,0,null],[&amp;quot;+2^32&amp;quot;,0,null],[&amp;quot;+2^33&amp;quot;,0,null],[&amp;quot;+2^34&amp;quot;,0,null],[&amp;quot;+2^35&amp;quot;,0,null],[&amp;quot;+2^36&amp;quot;,0,null],[&amp;quot;+2^37&amp;quot;,0,null],[&amp;quot;+2^38&amp;quot;,0,null],[&amp;quot;+2^39&amp;quot;,0,null],[&amp;quot;+2^40&amp;quot;,0,null],[&amp;quot;+2^41&amp;quot;,0,null],[&amp;quot;+2^42&amp;quot;,0,null],[&amp;quot;+2^43&amp;quot;,0,null],[&amp;quot;+2^44&amp;quot;,0,null],[&amp;quot;+2^45&amp;quot;,0,null],[&amp;quot;+2^46&amp;quot;,0,null],[&amp;quot;+2^47&amp;quot;,0,null],[&amp;quot;+2^48&amp;quot;,0,null],[&amp;quot;+2^49&amp;quot;,0,null],[&amp;quot;+2^50&amp;quot;,0,null],[&amp;quot;+2^51&amp;quot;,0,null],[&amp;quot;+2^52&amp;quot;,0,null],[&amp;quot;+2^53&amp;quot;,0,null],[&amp;quot;+2^54&amp;quot;,0,null],[&amp;quot;+2^55&amp;quot;,0,null],[&amp;quot;+2^56&amp;quot;,0,null],[&amp;quot;+2^57&amp;quot;,0,null],[&amp;quot;+2^58&amp;quot;,0,null],[&amp;quot;+2^59&amp;quot;,0,null],[&amp;quot;+2^60&amp;quot;,0,null],[&amp;quot;+2^61&amp;quot;,0,null],[&amp;quot;+2^62&amp;quot;,0,null],[&amp;quot;+2^63&amp;quot;,0,null],[&amp;quot;+2^64&amp;quot;,0,null],[&amp;quot;+2^65&amp;quot;,0,null],[&amp;quot;+2^66&amp;quot;,0,null],[&amp;quot;+2^67&amp;quot;,0,null],[&amp;quot;+2^68&amp;quot;,0,null],[&amp;quot;+2^69&amp;quot;,0,null],[&amp;quot;+2^70&amp;quot;,0,null],[&amp;quot;+2^71&amp;quot;,0,null],[&amp;quot;+2^72&amp;quot;,0,null],[&amp;quot;+2^73&amp;quot;,0,null],[&amp;quot;+2^74&amp;quot;,0,null],[&amp;quot;+2^75&amp;quot;,0,null],[&amp;quot;+2^76&amp;quot;,0,null],[&amp;quot;+2^77&amp;quot;,0,null],[&amp;quot;+2^78&amp;quot;,0,null],[&amp;quot;+2^79&amp;quot;,0,null],[&amp;quot;+2^80&amp;quot;,0,null],[&amp;quot;+2^81&amp;quot;,0,null],[&amp;quot;+2^82&amp;quot;,0,null],[&amp;quot;+2^83&amp;quot;,0,null],[&amp;quot;+2^84&amp;quot;,0,null],[&amp;quot;+2^85&amp;quot;,0,null],[&amp;quot;+2^86&amp;quot;,0,null],[&amp;quot;+2^87&amp;quot;,0,null],[&amp;quot;+2^88&amp;quot;,0,null],[&amp;quot;+2^89&amp;quot;,0,null],[&amp;quot;+2^90&amp;quot;,0,null],[&amp;quot;+2^91&amp;quot;,0,null],[&amp;quot;+2^92&amp;quot;,0,null],[&amp;quot;+2^93&amp;quot;,0,null],[&amp;quot;+2^94&amp;quot;,0,null],[&amp;quot;+2^95&amp;quot;,0,null],[&amp;quot;+2^96&amp;quot;,0,null],[&amp;quot;+2^97&amp;quot;,0,null],[&amp;quot;+2^98&amp;quot;,0,null],[&amp;quot;+2^99&amp;quot;,0,null],[&amp;quot;+2^100&amp;quot;,0,null],[&amp;quot;+2^101&amp;quot;,0,null],[&amp;quot;+2^102&amp;quot;,0,null],[&amp;quot;+2^103&amp;quot;,0,null],[&amp;quot;+2^104&amp;quot;,0,null],[&amp;quot;+2^105&amp;quot;,0,null],[&amp;quot;+2^106&amp;quot;,0,null],[&amp;quot;+2^107&amp;quot;,0,null],[&amp;quot;+2^108&amp;quot;,0,null],[&amp;quot;+2^109&amp;quot;,0,null],[&amp;quot;+2^110&amp;quot;,0,null],[&amp;quot;+2^111&amp;quot;,0,null],[&amp;quot;+2^112&amp;quot;,0,null],[&amp;quot;+2^113&amp;quot;,0,null],[&amp;quot;+2^114&amp;quot;,0,null],[&amp;quot;+2^115&amp;quot;,0,null],[&amp;quot;+2^116&amp;quot;,0,null],[&amp;quot;+2^117&amp;quot;,0,null],[&amp;quot;+2^118&amp;quot;,0,null],[&amp;quot;+2^119&amp;quot;,0,null],[&amp;quot;+2^120&amp;quot;,0,null],[&amp;quot;+2^121&amp;quot;,0,null],[&amp;quot;+2^122&amp;quot;,0,null],[&amp;quot;+2^123&amp;quot;,0,null],[&amp;quot;+2^124&amp;quot;,0,null],[&amp;quot;+2^125&amp;quot;,0,null],[&amp;quot;+2^126&amp;quot;,0,null],[&amp;quot;+2^127&amp;quot;,0,null] ]&lt;br /&gt;
 }}&lt;br /&gt;
&lt;br /&gt;
==Taylor==&lt;br /&gt;
&lt;br /&gt;
{{#widget:FloatErrorGraph|id=taylor&lt;br /&gt;
  |data=[ [&amp;quot;-2^127&amp;quot;,0,0],[&amp;quot;-2^126&amp;quot;,0,0],[&amp;quot;-2^125&amp;quot;,0,0],[&amp;quot;-2^124&amp;quot;,0,0],[&amp;quot;-2^123&amp;quot;,0,0],[&amp;quot;-2^122&amp;quot;,0,0],[&amp;quot;-2^121&amp;quot;,0,0],[&amp;quot;-2^120&amp;quot;,0,0],[&amp;quot;-2^119&amp;quot;,0,0],[&amp;quot;-2^118&amp;quot;,0,0],[&amp;quot;-2^117&amp;quot;,0,0],[&amp;quot;-2^116&amp;quot;,0,0],[&amp;quot;-2^115&amp;quot;,0,0],[&amp;quot;-2^114&amp;quot;,0,0],[&amp;quot;-2^113&amp;quot;,0,0],[&amp;quot;-2^112&amp;quot;,0,0],[&amp;quot;-2^111&amp;quot;,0,0],[&amp;quot;-2^110&amp;quot;,0,0],[&amp;quot;-2^109&amp;quot;,0,0],[&amp;quot;-2^108&amp;quot;,0,0],[&amp;quot;-2^107&amp;quot;,0,0],[&amp;quot;-2^106&amp;quot;,0,0],[&amp;quot;-2^105&amp;quot;,0,0],[&amp;quot;-2^104&amp;quot;,0,0],[&amp;quot;-2^103&amp;quot;,0,0],[&amp;quot;-2^102&amp;quot;,0,0],[&amp;quot;-2^101&amp;quot;,0,0],[&amp;quot;-2^100&amp;quot;,0,0],[&amp;quot;-2^99&amp;quot;,0,0],[&amp;quot;-2^98&amp;quot;,0,0],[&amp;quot;-2^97&amp;quot;,0,0],[&amp;quot;-2^96&amp;quot;,0,0],[&amp;quot;-2^95&amp;quot;,0,0],[&amp;quot;-2^94&amp;quot;,0,0],[&amp;quot;-2^93&amp;quot;,0,0],[&amp;quot;-2^92&amp;quot;,0,0],[&amp;quot;-2^91&amp;quot;,0,0],[&amp;quot;-2^90&amp;quot;,0,0],[&amp;quot;-2^89&amp;quot;,0,0],[&amp;quot;-2^88&amp;quot;,0,0],[&amp;quot;-2^87&amp;quot;,0,0],[&amp;quot;-2^86&amp;quot;,0,0],[&amp;quot;-2^85&amp;quot;,0,0],[&amp;quot;-2^84&amp;quot;,0,0],[&amp;quot;-2^83&amp;quot;,0,0],[&amp;quot;-2^82&amp;quot;,0,0],[&amp;quot;-2^81&amp;quot;,0,0],[&amp;quot;-2^80&amp;quot;,0,0],[&amp;quot;-2^79&amp;quot;,0,0],[&amp;quot;-2^78&amp;quot;,0,0],[&amp;quot;-2^77&amp;quot;,0,0],[&amp;quot;-2^76&amp;quot;,0,0],[&amp;quot;-2^75&amp;quot;,0,0],[&amp;quot;-2^74&amp;quot;,0,0],[&amp;quot;-2^73&amp;quot;,0,0],[&amp;quot;-2^72&amp;quot;,0,0],[&amp;quot;-2^71&amp;quot;,0,0],[&amp;quot;-2^70&amp;quot;,0,0],[&amp;quot;-2^69&amp;quot;,0,0],[&amp;quot;-2^68&amp;quot;,0,0],[&amp;quot;-2^67&amp;quot;,0,0],[&amp;quot;-2^66&amp;quot;,0,0],[&amp;quot;-2^65&amp;quot;,0,0],[&amp;quot;-2^64&amp;quot;,0,0],[&amp;quot;-2^63&amp;quot;,1.84467e+19,3.09485e+26],[&amp;quot;-2^62&amp;quot;,9.22336e+18,1.54742e+26],[&amp;quot;-2^61&amp;quot;,4.61168e+18,7.73712e+25],[&amp;quot;-2^60&amp;quot;,2.30584e+18,3.86856e+25],[&amp;quot;-2^59&amp;quot;,1.15292e+18,1.93428e+25],[&amp;quot;-2^58&amp;quot;,5.7646e+17,9.6714e+24],[&amp;quot;-2^57&amp;quot;,2.8823e+17,4.8357e+24],[&amp;quot;-2^56&amp;quot;,1.44115e+17,2.41785e+24],[&amp;quot;-2^55&amp;quot;,7.20575e+16,1.20892e+24],[&amp;quot;-2^54&amp;quot;,3.60288e+16,6.04462e+23],[&amp;quot;-2^53&amp;quot;,1.80144e+16,3.02231e+23],[&amp;quot;-2^52&amp;quot;,9.00719e+15,1.51116e+23],[&amp;quot;-2^51&amp;quot;,4.5036e+15,7.55578e+22],[&amp;quot;-2^50&amp;quot;,2.2518e+15,3.77789e+22],[&amp;quot;-2^49&amp;quot;,1.1259e+15,1.88894e+22],[&amp;quot;-2^48&amp;quot;,5.62949e+14,9.44472e+21],[&amp;quot;-2^47&amp;quot;,2.81475e+14,4.72236e+21],[&amp;quot;-2^46&amp;quot;,1.40737e+14,2.36118e+21],[&amp;quot;-2^45&amp;quot;,7.03687e+13,1.18059e+21],[&amp;quot;-2^44&amp;quot;,3.51843e+13,5.90295e+20],[&amp;quot;-2^43&amp;quot;,1.75922e+13,2.95148e+20],[&amp;quot;-2^42&amp;quot;,8.79608e+12,1.47574e+20],[&amp;quot;-2^41&amp;quot;,4.39804e+12,7.37869e+19],[&amp;quot;-2^40&amp;quot;,2.19902e+12,3.68935e+19],[&amp;quot;-2^39&amp;quot;,1.09951e+12,1.84467e+19],[&amp;quot;-2^38&amp;quot;,5.49755e+11,9.22336e+18],[&amp;quot;-2^37&amp;quot;,2.74878e+11,4.61168e+18],[&amp;quot;-2^36&amp;quot;,1.37439e+11,2.30584e+18],[&amp;quot;-2^35&amp;quot;,6.87194e+10,1.15292e+18],[&amp;quot;-2^34&amp;quot;,3.43597e+10,5.7646e+17],[&amp;quot;-2^33&amp;quot;,1.71799e+10,2.8823e+17],[&amp;quot;-2^32&amp;quot;,8.58993e+09,1.44115e+17],[&amp;quot;-2^31&amp;quot;,4.29085e+09,7.19885e+16],[&amp;quot;-2^30&amp;quot;,2.14542e+09,3.59943e+16],[&amp;quot;-2^29&amp;quot;,1.07271e+09,1.79971e+16],[&amp;quot;-2^28&amp;quot;,5.36356e+08,8.99856e+15],[&amp;quot;-2^27&amp;quot;,2.68178e+08,4.49928e+15],[&amp;quot;-2^26&amp;quot;,1.34089e+08,2.24964e+15],[&amp;quot;-2^25&amp;quot;,6.70445e+07,1.12482e+15],[&amp;quot;-2^24&amp;quot;,3.35223e+07,5.6241e+14],[&amp;quot;-2^23&amp;quot;,1.67611e+07,2.81205e+14],[&amp;quot;-2^22&amp;quot;,8.38056e+06,1.40603e+14],[&amp;quot;-2^21&amp;quot;,4.19028e+06,7.03013e+13],[&amp;quot;-2^20&amp;quot;,2.09514e+06,3.51506e+13],[&amp;quot;-2^19&amp;quot;,1.04757e+06,1.75753e+13],[&amp;quot;-2^18&amp;quot;,523784,8.78764e+12],[&amp;quot;-2^17&amp;quot;,261892,4.39381e+12],[&amp;quot;-2^16&amp;quot;,130945,2.1969e+12],[&amp;quot;-2^15&amp;quot;,65472.2,1.09844e+12],[&amp;quot;-2^14&amp;quot;,32735.6,5.49212e+11],[&amp;quot;-2^13&amp;quot;,16367.3,2.74598e+11],[&amp;quot;-2^12&amp;quot;,8183.15,1.3729e+11],[&amp;quot;-2^11&amp;quot;,4091.07,6.86368e+10],[&amp;quot;-2^10&amp;quot;,2045.04,3.431e+10],[&amp;quot;-2^9&amp;quot;,1022.02,1.71466e+10],[&amp;quot;-2^8&amp;quot;,510.509,8.56492e+09],[&amp;quot;-2^7&amp;quot;,254.754,4.27406e+09],[&amp;quot;-2^6&amp;quot;,126.876,2.12862e+09],[&amp;quot;-2^5&amp;quot;,62.9354,1.05588e+09],[&amp;quot;-2^4&amp;quot;,30.9629,5.19471e+08],[&amp;quot;-2^3&amp;quot;,14.9718,2.51185e+08],[&amp;quot;-2^2&amp;quot;,6.96672,1.16882e+08],[&amp;quot;-2^1&amp;quot;,2.94633,4.94312e+07],[&amp;quot;-2^0&amp;quot;,0.940715,1.57826e+07],[&amp;quot;-2^-1&amp;quot;,0.127635,2.14135e+06],[&amp;quot;-2^-2&amp;quot;,0.00318735,106950],[&amp;quot;-2^-3&amp;quot;,3.11475e-05,2090.28],[&amp;quot;-2^-4&amp;quot;,2.57737e-07,34.5929],[&amp;quot;-2^-5&amp;quot;,3.89213e-09,1.04478],[&amp;quot;-2^-6&amp;quot;,9.51472e-10,0.510818],[&amp;quot;-2^-7&amp;quot;,4.66402e-10,0.500795],[&amp;quot;-2^-8&amp;quot;,2.32917e-10,0.500185],[&amp;quot;-2^-9&amp;quot;,1.16423e-10,0.500033],[&amp;quot;-2^-10&amp;quot;,5.82082e-11,0.500004],[&amp;quot;-2^-11&amp;quot;,2.91037e-11,0.499998],[&amp;quot;-2^-12&amp;quot;,1.45519e-11,0.5],[&amp;quot;-2^-13&amp;quot;,4.85062e-12,0.333332],[&amp;quot;-2^-14&amp;quot;,6.06328e-13,0.0833331],[&amp;quot;-2^-15&amp;quot;,7.5791e-14,0.0208333],[&amp;quot;-2^-16&amp;quot;,9.47387e-15,0.00520832],[&amp;quot;-2^-17&amp;quot;,1.18424e-15,0.00130208],[&amp;quot;-2^-18&amp;quot;,1.48029e-16,0.00032552],[&amp;quot;-2^-19&amp;quot;,1.85039e-17,8.13808e-05],[&amp;quot;-2^-20&amp;quot;,2.31304e-18,2.03457e-05],[&amp;quot;-2^-21&amp;quot;,2.89156e-19,5.08688e-06],[&amp;quot;-2^-22&amp;quot;,3.61577e-20,1.27219e-06],[&amp;quot;-2^-23&amp;quot;,4.52633e-21,3.18512e-07],[&amp;quot;-2^-24&amp;quot;,5.691e-22,8.00937e-08],[&amp;quot;-2^-25&amp;quot;,7.27919e-23,2.04891e-08],[&amp;quot;-2^-26&amp;quot;,9.92617e-24,5.58794e-09],[&amp;quot;-2^-27&amp;quot;,1.65436e-24,1.86265e-09],[&amp;quot;-2^-28&amp;quot;,8.27181e-25,1.86265e-09],[&amp;quot;-2^-29&amp;quot;,4.1359e-25,1.86265e-09],[&amp;quot;-2^-30&amp;quot;,2.06795e-25,1.86265e-09],[&amp;quot;-2^-31&amp;quot;,1.03398e-25,1.86265e-09],[&amp;quot;-2^-32&amp;quot;,5.16988e-26,1.86265e-09],[&amp;quot;-2^-33&amp;quot;,2.58494e-26,1.86265e-09],[&amp;quot;-2^-34&amp;quot;,1.29247e-26,1.86265e-09],[&amp;quot;-2^-35&amp;quot;,6.46235e-27,1.86265e-09],[&amp;quot;-2^-36&amp;quot;,3.23117e-27,1.86265e-09],[&amp;quot;-2^-37&amp;quot;,1.61559e-27,1.86265e-09],[&amp;quot;-2^-38&amp;quot;,8.07794e-28,1.86265e-09],[&amp;quot;-2^-39&amp;quot;,4.03897e-28,1.86265e-09],[&amp;quot;-2^-40&amp;quot;,2.01948e-28,1.86265e-09],[&amp;quot;-2^-41&amp;quot;,1.00974e-28,1.86265e-09],[&amp;quot;-2^-42&amp;quot;,5.04871e-29,1.86265e-09],[&amp;quot;-2^-43&amp;quot;,2.52435e-29,1.86265e-09],[&amp;quot;-2^-44&amp;quot;,1.26218e-29,1.86265e-09],[&amp;quot;-2^-45&amp;quot;,6.31089e-30,1.86265e-09],[&amp;quot;-2^-46&amp;quot;,3.15544e-30,1.86265e-09],[&amp;quot;-2^-47&amp;quot;,1.57772e-30,1.86265e-09],[&amp;quot;-2^-48&amp;quot;,7.88861e-31,1.86265e-09],[&amp;quot;-2^-49&amp;quot;,3.9443e-31,1.86265e-09],[&amp;quot;-2^-50&amp;quot;,1.97215e-31,1.86265e-09],[&amp;quot;-2^-51&amp;quot;,9.86076e-32,1.86265e-09],[&amp;quot;-2^-52&amp;quot;,4.93038e-32,1.86265e-09],[&amp;quot;-2^-53&amp;quot;,2.46519e-32,1.86265e-09],[&amp;quot;-2^-54&amp;quot;,1.2326e-32,1.86265e-09],[&amp;quot;-2^-55&amp;quot;,0,0],[&amp;quot;-2^-56&amp;quot;,0,0],[&amp;quot;-2^-57&amp;quot;,0,0],[&amp;quot;-2^-58&amp;quot;,0,0],[&amp;quot;-2^-59&amp;quot;,0,0],[&amp;quot;-2^-60&amp;quot;,0,0],[&amp;quot;-2^-61&amp;quot;,0,0],[&amp;quot;-2^-62&amp;quot;,0,0],[&amp;quot;-2^-63&amp;quot;,0,0],[&amp;quot;-2^-64&amp;quot;,0,0],[&amp;quot;-2^-65&amp;quot;,0,0],[&amp;quot;-2^-66&amp;quot;,0,0],[&amp;quot;-2^-67&amp;quot;,0,0],[&amp;quot;-2^-68&amp;quot;,0,0],[&amp;quot;-2^-69&amp;quot;,0,0],[&amp;quot;-2^-70&amp;quot;,0,0],[&amp;quot;-2^-71&amp;quot;,0,0],[&amp;quot;-2^-72&amp;quot;,0,0],[&amp;quot;-2^-73&amp;quot;,0,0],[&amp;quot;-2^-74&amp;quot;,0,0],[&amp;quot;-2^-75&amp;quot;,0,0],[&amp;quot;-2^-76&amp;quot;,0,0],[&amp;quot;-2^-77&amp;quot;,0,0],[&amp;quot;-2^-78&amp;quot;,0,0],[&amp;quot;-2^-79&amp;quot;,0,0],[&amp;quot;-2^-80&amp;quot;,0,0],[&amp;quot;-2^-81&amp;quot;,0,0],[&amp;quot;-2^-82&amp;quot;,0,0],[&amp;quot;-2^-83&amp;quot;,0,0],[&amp;quot;-2^-84&amp;quot;,0,0],[&amp;quot;-2^-85&amp;quot;,0,0],[&amp;quot;-2^-86&amp;quot;,0,0],[&amp;quot;-2^-87&amp;quot;,0,0],[&amp;quot;-2^-88&amp;quot;,0,0],[&amp;quot;-2^-89&amp;quot;,0,0],[&amp;quot;-2^-90&amp;quot;,0,0],[&amp;quot;-2^-91&amp;quot;,0,0],[&amp;quot;-2^-92&amp;quot;,0,0],[&amp;quot;-2^-93&amp;quot;,0,0],[&amp;quot;-2^-94&amp;quot;,0,0],[&amp;quot;-2^-95&amp;quot;,0,0],[&amp;quot;-2^-96&amp;quot;,0,0],[&amp;quot;-2^-97&amp;quot;,0,0],[&amp;quot;-2^-98&amp;quot;,0,0],[&amp;quot;-2^-99&amp;quot;,0,0],[&amp;quot;-2^-100&amp;quot;,0,0],[&amp;quot;-2^-101&amp;quot;,0,0],[&amp;quot;-2^-102&amp;quot;,0,0],[&amp;quot;-2^-103&amp;quot;,0,0],[&amp;quot;-2^-104&amp;quot;,0,0],[&amp;quot;-2^-105&amp;quot;,0,0],[&amp;quot;-2^-106&amp;quot;,0,0],[&amp;quot;-2^-107&amp;quot;,0,0],[&amp;quot;-2^-108&amp;quot;,0,0],[&amp;quot;-2^-109&amp;quot;,0,0],[&amp;quot;-2^-110&amp;quot;,0,0],[&amp;quot;-2^-111&amp;quot;,0,0],[&amp;quot;-2^-112&amp;quot;,0,0],[&amp;quot;-2^-113&amp;quot;,0,0],[&amp;quot;-2^-114&amp;quot;,0,0],[&amp;quot;-2^-115&amp;quot;,0,0],[&amp;quot;-2^-116&amp;quot;,0,0],[&amp;quot;-2^-117&amp;quot;,0,0],[&amp;quot;-2^-118&amp;quot;,0,0],[&amp;quot;-2^-119&amp;quot;,0,0],[&amp;quot;-2^-120&amp;quot;,0,0],[&amp;quot;-2^-121&amp;quot;,0,0],[&amp;quot;-2^-122&amp;quot;,0,0],[&amp;quot;-2^-123&amp;quot;,0,0],[&amp;quot;-2^-124&amp;quot;,0,0],[&amp;quot;-2^-125&amp;quot;,0,0],[&amp;quot;-2^-126&amp;quot;,0,0],[&amp;quot;-SN&amp;quot;,0,0],[&amp;quot;0&amp;quot;,0,0],[&amp;quot;+SN&amp;quot;,0,0],[&amp;quot;+2^-126&amp;quot;,0,0],[&amp;quot;+2^-125&amp;quot;,0,0],[&amp;quot;+2^-124&amp;quot;,0,0],[&amp;quot;+2^-123&amp;quot;,0,0],[&amp;quot;+2^-122&amp;quot;,0,0],[&amp;quot;+2^-121&amp;quot;,0,0],[&amp;quot;+2^-120&amp;quot;,0,0],[&amp;quot;+2^-119&amp;quot;,0,0],[&amp;quot;+2^-118&amp;quot;,0,0],[&amp;quot;+2^-117&amp;quot;,0,0],[&amp;quot;+2^-116&amp;quot;,0,0],[&amp;quot;+2^-115&amp;quot;,0,0],[&amp;quot;+2^-114&amp;quot;,0,0],[&amp;quot;+2^-113&amp;quot;,0,0],[&amp;quot;+2^-112&amp;quot;,0,0],[&amp;quot;+2^-111&amp;quot;,0,0],[&amp;quot;+2^-110&amp;quot;,0,0],[&amp;quot;+2^-109&amp;quot;,0,0],[&amp;quot;+2^-108&amp;quot;,0,0],[&amp;quot;+2^-107&amp;quot;,0,0],[&amp;quot;+2^-106&amp;quot;,0,0],[&amp;quot;+2^-105&amp;quot;,0,0],[&amp;quot;+2^-104&amp;quot;,0,0],[&amp;quot;+2^-103&amp;quot;,0,0],[&amp;quot;+2^-102&amp;quot;,0,0],[&amp;quot;+2^-101&amp;quot;,0,0],[&amp;quot;+2^-100&amp;quot;,0,0],[&amp;quot;+2^-99&amp;quot;,0,0],[&amp;quot;+2^-98&amp;quot;,0,0],[&amp;quot;+2^-97&amp;quot;,0,0],[&amp;quot;+2^-96&amp;quot;,0,0],[&amp;quot;+2^-95&amp;quot;,0,0],[&amp;quot;+2^-94&amp;quot;,0,0],[&amp;quot;+2^-93&amp;quot;,0,0],[&amp;quot;+2^-92&amp;quot;,0,0],[&amp;quot;+2^-91&amp;quot;,0,0],[&amp;quot;+2^-90&amp;quot;,0,0],[&amp;quot;+2^-89&amp;quot;,0,0],[&amp;quot;+2^-88&amp;quot;,0,0],[&amp;quot;+2^-87&amp;quot;,0,0],[&amp;quot;+2^-86&amp;quot;,0,0],[&amp;quot;+2^-85&amp;quot;,0,0],[&amp;quot;+2^-84&amp;quot;,0,0],[&amp;quot;+2^-83&amp;quot;,0,0],[&amp;quot;+2^-82&amp;quot;,0,0],[&amp;quot;+2^-81&amp;quot;,0,0],[&amp;quot;+2^-80&amp;quot;,0,0],[&amp;quot;+2^-79&amp;quot;,0,0],[&amp;quot;+2^-78&amp;quot;,0,0],[&amp;quot;+2^-77&amp;quot;,0,0],[&amp;quot;+2^-76&amp;quot;,0,0],[&amp;quot;+2^-75&amp;quot;,0,0],[&amp;quot;+2^-74&amp;quot;,0,0],[&amp;quot;+2^-73&amp;quot;,0,0],[&amp;quot;+2^-72&amp;quot;,0,0],[&amp;quot;+2^-71&amp;quot;,0,0],[&amp;quot;+2^-70&amp;quot;,0,0],[&amp;quot;+2^-69&amp;quot;,0,0],[&amp;quot;+2^-68&amp;quot;,0,0],[&amp;quot;+2^-67&amp;quot;,0,0],[&amp;quot;+2^-66&amp;quot;,0,0],[&amp;quot;+2^-65&amp;quot;,0,0],[&amp;quot;+2^-64&amp;quot;,0,0],[&amp;quot;+2^-63&amp;quot;,0,0],[&amp;quot;+2^-62&amp;quot;,0,0],[&amp;quot;+2^-61&amp;quot;,0,0],[&amp;quot;+2^-60&amp;quot;,0,0],[&amp;quot;+2^-59&amp;quot;,0,0],[&amp;quot;+2^-58&amp;quot;,0,0],[&amp;quot;+2^-57&amp;quot;,0,0],[&amp;quot;+2^-56&amp;quot;,0,0],[&amp;quot;+2^-55&amp;quot;,0,0],[&amp;quot;+2^-54&amp;quot;,1.2326e-32,1.86265e-09],[&amp;quot;+2^-53&amp;quot;,2.46519e-32,1.86265e-09],[&amp;quot;+2^-52&amp;quot;,4.93038e-32,1.86265e-09],[&amp;quot;+2^-51&amp;quot;,9.86076e-32,1.86265e-09],[&amp;quot;+2^-50&amp;quot;,1.97215e-31,1.86265e-09],[&amp;quot;+2^-49&amp;quot;,3.9443e-31,1.86265e-09],[&amp;quot;+2^-48&amp;quot;,7.88861e-31,1.86265e-09],[&amp;quot;+2^-47&amp;quot;,1.57772e-30,1.86265e-09],[&amp;quot;+2^-46&amp;quot;,3.15544e-30,1.86265e-09],[&amp;quot;+2^-45&amp;quot;,6.31089e-30,1.86265e-09],[&amp;quot;+2^-44&amp;quot;,1.26218e-29,1.86265e-09],[&amp;quot;+2^-43&amp;quot;,2.52435e-29,1.86265e-09],[&amp;quot;+2^-42&amp;quot;,5.04871e-29,1.86265e-09],[&amp;quot;+2^-41&amp;quot;,1.00974e-28,1.86265e-09],[&amp;quot;+2^-40&amp;quot;,2.01948e-28,1.86265e-09],[&amp;quot;+2^-39&amp;quot;,4.03897e-28,1.86265e-09],[&amp;quot;+2^-38&amp;quot;,8.07794e-28,1.86265e-09],[&amp;quot;+2^-37&amp;quot;,1.61559e-27,1.86265e-09],[&amp;quot;+2^-36&amp;quot;,3.23117e-27,1.86265e-09],[&amp;quot;+2^-35&amp;quot;,6.46235e-27,1.86265e-09],[&amp;quot;+2^-34&amp;quot;,1.29247e-26,1.86265e-09],[&amp;quot;+2^-33&amp;quot;,2.58494e-26,1.86265e-09],[&amp;quot;+2^-32&amp;quot;,5.16988e-26,1.86265e-09],[&amp;quot;+2^-31&amp;quot;,1.03398e-25,1.86265e-09],[&amp;quot;+2^-30&amp;quot;,2.06795e-25,1.86265e-09],[&amp;quot;+2^-29&amp;quot;,4.1359e-25,1.86265e-09],[&amp;quot;+2^-28&amp;quot;,8.27181e-25,1.86265e-09],[&amp;quot;+2^-27&amp;quot;,1.65436e-24,1.86265e-09],[&amp;quot;+2^-26&amp;quot;,9.92617e-24,5.58794e-09],[&amp;quot;+2^-25&amp;quot;,7.27919e-23,2.04891e-08],[&amp;quot;+2^-24&amp;quot;,5.691e-22,8.00937e-08],[&amp;quot;+2^-23&amp;quot;,4.52633e-21,3.18512e-07],[&amp;quot;+2^-22&amp;quot;,3.61577e-20,1.27219e-06],[&amp;quot;+2^-21&amp;quot;,2.89156e-19,5.08688e-06],[&amp;quot;+2^-20&amp;quot;,2.31304e-18,2.03457e-05],[&amp;quot;+2^-19&amp;quot;,1.85039e-17,8.13808e-05],[&amp;quot;+2^-18&amp;quot;,1.48029e-16,0.00032552],[&amp;quot;+2^-17&amp;quot;,1.18424e-15,0.00130208],[&amp;quot;+2^-16&amp;quot;,9.47387e-15,0.00520832],[&amp;quot;+2^-15&amp;quot;,7.5791e-14,0.0208333],[&amp;quot;+2^-14&amp;quot;,6.06328e-13,0.0833331],[&amp;quot;+2^-13&amp;quot;,4.85062e-12,0.333332],[&amp;quot;+2^-12&amp;quot;,1.45519e-11,0.5],[&amp;quot;+2^-11&amp;quot;,2.91037e-11,0.499998],[&amp;quot;+2^-10&amp;quot;,5.82082e-11,0.500004],[&amp;quot;+2^-9&amp;quot;,1.16423e-10,0.500033],[&amp;quot;+2^-8&amp;quot;,2.32917e-10,0.500185],[&amp;quot;+2^-7&amp;quot;,4.66402e-10,0.500795],[&amp;quot;+2^-6&amp;quot;,9.51472e-10,0.510818],[&amp;quot;+2^-5&amp;quot;,3.89213e-09,1.04478],[&amp;quot;+2^-4&amp;quot;,2.57737e-07,34.5929],[&amp;quot;+2^-3&amp;quot;,3.11475e-05,2090.28],[&amp;quot;+2^-2&amp;quot;,0.00318735,106950],[&amp;quot;+2^-1&amp;quot;,0.127635,2.14135e+06],[&amp;quot;+2^0&amp;quot;,0.940715,1.57826e+07],[&amp;quot;+2^1&amp;quot;,2.94633,4.94312e+07],[&amp;quot;+2^2&amp;quot;,6.96672,1.16882e+08],[&amp;quot;+2^3&amp;quot;,14.9718,1.33874e+08],[&amp;quot;+2^4&amp;quot;,30.9629,2.59736e+08],[&amp;quot;+2^5&amp;quot;,62.9354,5.27941e+08],[&amp;quot;+2^6&amp;quot;,126.876,1.06431e+09],[&amp;quot;+2^7&amp;quot;,254.754,2.13703e+09],[&amp;quot;+2^8&amp;quot;,510.509,4.28246e+09],[&amp;quot;+2^9&amp;quot;,1022.02,8.57331e+09],[&amp;quot;+2^10&amp;quot;,2045.04,1.7155e+10],[&amp;quot;+2^11&amp;quot;,4091.07,3.43184e+10],[&amp;quot;+2^12&amp;quot;,8183.15,6.86452e+10],[&amp;quot;+2^13&amp;quot;,16367.3,1.37299e+11],[&amp;quot;+2^14&amp;quot;,32735.6,2.74606e+11],[&amp;quot;+2^15&amp;quot;,65472.2,5.4922e+11],[&amp;quot;+2^16&amp;quot;,130945,1.09845e+12],[&amp;quot;+2^17&amp;quot;,261892,2.19691e+12],[&amp;quot;+2^18&amp;quot;,523784,4.39382e+12],[&amp;quot;+2^19&amp;quot;,1.04757e+06,8.78765e+12],[&amp;quot;+2^20&amp;quot;,2.09514e+06,1.75753e+13],[&amp;quot;+2^21&amp;quot;,4.19028e+06,3.51506e+13],[&amp;quot;+2^22&amp;quot;,8.38056e+06,7.03013e+13],[&amp;quot;+2^23&amp;quot;,1.67611e+07,1.40603e+14],[&amp;quot;+2^24&amp;quot;,3.35223e+07,2.81205e+14],[&amp;quot;+2^25&amp;quot;,6.70445e+07,5.6241e+14],[&amp;quot;+2^26&amp;quot;,1.34089e+08,1.12482e+15],[&amp;quot;+2^27&amp;quot;,2.68178e+08,2.24964e+15],[&amp;quot;+2^28&amp;quot;,5.36356e+08,4.49928e+15],[&amp;quot;+2^29&amp;quot;,1.07271e+09,8.99856e+15],[&amp;quot;+2^30&amp;quot;,2.14542e+09,1.79971e+16],[&amp;quot;+2^31&amp;quot;,4.29085e+09,3.59943e+16],[&amp;quot;+2^32&amp;quot;,8.58993e+09,7.20575e+16],[&amp;quot;+2^33&amp;quot;,1.71799e+10,1.44115e+17],[&amp;quot;+2^34&amp;quot;,3.43597e+10,2.8823e+17],[&amp;quot;+2^35&amp;quot;,6.87194e+10,5.7646e+17],[&amp;quot;+2^36&amp;quot;,1.37439e+11,1.15292e+18],[&amp;quot;+2^37&amp;quot;,2.74878e+11,2.30584e+18],[&amp;quot;+2^38&amp;quot;,5.49755e+11,4.61168e+18],[&amp;quot;+2^39&amp;quot;,1.09951e+12,9.22336e+18],[&amp;quot;+2^40&amp;quot;,2.19902e+12,1.84467e+19],[&amp;quot;+2^41&amp;quot;,4.39804e+12,3.68935e+19],[&amp;quot;+2^42&amp;quot;,8.79608e+12,7.37869e+19],[&amp;quot;+2^43&amp;quot;,1.75922e+13,1.47574e+20],[&amp;quot;+2^44&amp;quot;,3.51843e+13,2.95148e+20],[&amp;quot;+2^45&amp;quot;,7.03687e+13,5.90295e+20],[&amp;quot;+2^46&amp;quot;,1.40737e+14,1.18059e+21],[&amp;quot;+2^47&amp;quot;,2.81475e+14,2.36118e+21],[&amp;quot;+2^48&amp;quot;,5.62949e+14,4.72236e+21],[&amp;quot;+2^49&amp;quot;,1.1259e+15,9.44472e+21],[&amp;quot;+2^50&amp;quot;,2.2518e+15,1.88894e+22],[&amp;quot;+2^51&amp;quot;,4.5036e+15,3.77789e+22],[&amp;quot;+2^52&amp;quot;,9.00719e+15,7.55578e+22],[&amp;quot;+2^53&amp;quot;,1.80144e+16,1.51116e+23],[&amp;quot;+2^54&amp;quot;,3.60288e+16,3.02231e+23],[&amp;quot;+2^55&amp;quot;,7.20575e+16,6.04462e+23],[&amp;quot;+2^56&amp;quot;,1.44115e+17,1.20892e+24],[&amp;quot;+2^57&amp;quot;,2.8823e+17,2.41785e+24],[&amp;quot;+2^58&amp;quot;,5.7646e+17,4.8357e+24],[&amp;quot;+2^59&amp;quot;,1.15292e+18,9.6714e+24],[&amp;quot;+2^60&amp;quot;,2.30584e+18,1.93428e+25],[&amp;quot;+2^61&amp;quot;,4.61168e+18,3.86856e+25],[&amp;quot;+2^62&amp;quot;,9.22336e+18,7.73712e+25],[&amp;quot;+2^63&amp;quot;,1.84467e+19,1.54742e+26],[&amp;quot;+2^64&amp;quot;,0,0],[&amp;quot;+2^65&amp;quot;,0,0],[&amp;quot;+2^66&amp;quot;,0,0],[&amp;quot;+2^67&amp;quot;,0,0],[&amp;quot;+2^68&amp;quot;,0,0],[&amp;quot;+2^69&amp;quot;,0,0],[&amp;quot;+2^70&amp;quot;,0,0],[&amp;quot;+2^71&amp;quot;,0,0],[&amp;quot;+2^72&amp;quot;,0,0],[&amp;quot;+2^73&amp;quot;,0,0],[&amp;quot;+2^74&amp;quot;,0,0],[&amp;quot;+2^75&amp;quot;,0,0],[&amp;quot;+2^76&amp;quot;,0,0],[&amp;quot;+2^77&amp;quot;,0,0],[&amp;quot;+2^78&amp;quot;,0,0],[&amp;quot;+2^79&amp;quot;,0,0],[&amp;quot;+2^80&amp;quot;,0,0],[&amp;quot;+2^81&amp;quot;,0,0],[&amp;quot;+2^82&amp;quot;,0,0],[&amp;quot;+2^83&amp;quot;,0,0],[&amp;quot;+2^84&amp;quot;,0,0],[&amp;quot;+2^85&amp;quot;,0,0],[&amp;quot;+2^86&amp;quot;,0,0],[&amp;quot;+2^87&amp;quot;,0,0],[&amp;quot;+2^88&amp;quot;,0,0],[&amp;quot;+2^89&amp;quot;,0,0],[&amp;quot;+2^90&amp;quot;,0,0],[&amp;quot;+2^91&amp;quot;,0,0],[&amp;quot;+2^92&amp;quot;,0,0],[&amp;quot;+2^93&amp;quot;,0,0],[&amp;quot;+2^94&amp;quot;,0,0],[&amp;quot;+2^95&amp;quot;,0,0],[&amp;quot;+2^96&amp;quot;,0,0],[&amp;quot;+2^97&amp;quot;,0,0],[&amp;quot;+2^98&amp;quot;,0,0],[&amp;quot;+2^99&amp;quot;,0,0],[&amp;quot;+2^100&amp;quot;,0,0],[&amp;quot;+2^101&amp;quot;,0,0],[&amp;quot;+2^102&amp;quot;,0,0],[&amp;quot;+2^103&amp;quot;,0,0],[&amp;quot;+2^104&amp;quot;,0,0],[&amp;quot;+2^105&amp;quot;,0,0],[&amp;quot;+2^106&amp;quot;,0,0],[&amp;quot;+2^107&amp;quot;,0,0],[&amp;quot;+2^108&amp;quot;,0,0],[&amp;quot;+2^109&amp;quot;,0,0],[&amp;quot;+2^110&amp;quot;,0,0],[&amp;quot;+2^111&amp;quot;,0,0],[&amp;quot;+2^112&amp;quot;,0,0],[&amp;quot;+2^113&amp;quot;,0,0],[&amp;quot;+2^114&amp;quot;,0,0],[&amp;quot;+2^115&amp;quot;,0,0],[&amp;quot;+2^116&amp;quot;,0,0],[&amp;quot;+2^117&amp;quot;,0,0],[&amp;quot;+2^118&amp;quot;,0,0],[&amp;quot;+2^119&amp;quot;,0,0],[&amp;quot;+2^120&amp;quot;,0,0],[&amp;quot;+2^121&amp;quot;,0,0],[&amp;quot;+2^122&amp;quot;,0,0],[&amp;quot;+2^123&amp;quot;,0,0],[&amp;quot;+2^124&amp;quot;,0,0],[&amp;quot;+2^125&amp;quot;,0,0],[&amp;quot;+2^126&amp;quot;,0,0],[&amp;quot;+2^127&amp;quot;,0,0] ]&lt;br /&gt;
 }}&lt;br /&gt;
&lt;br /&gt;
== Localized Approximation Functions ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
!Id&lt;br /&gt;
!Formula&lt;br /&gt;
!ULP &amp;lt;= 2.0&lt;br /&gt;
!NAN&lt;br /&gt;
|-&lt;br /&gt;
|signum&lt;br /&gt;
|&amp;lt;math&amp;gt;\sgn{x}&amp;lt;/math&amp;gt;&lt;br /&gt;
| -inf &amp;lt; x &amp;lt; -8.31763&lt;br /&gt;
8.31763 &amp;lt; x &amp;lt; inf&lt;br /&gt;
| -&lt;br /&gt;
|-&lt;br /&gt;
|poly1&lt;br /&gt;
|&amp;lt;math&amp;gt;x&amp;lt;/math&amp;gt;&lt;br /&gt;
| -0.000704229 &amp;lt; x &amp;lt; 0.000704229&lt;br /&gt;
| -&lt;br /&gt;
|-&lt;br /&gt;
|poly3&lt;br /&gt;
|&amp;lt;math&amp;gt;x - \frac{1}{3} x^3&amp;lt;/math&amp;gt;&lt;br /&gt;
| -0.0291781 &amp;lt; x &amp;lt; 0.0291781 &lt;br /&gt;
| -&lt;br /&gt;
|-&lt;br /&gt;
|lampert7&lt;br /&gt;
|&amp;lt;math&amp;gt;\frac{x \cdot (135135 + x^2 \cdot (17325 + x^2 \cdot (378 + x^2)))}{135135 + x^2 \cdot (62370 + x^2 \cdot (3150 + 28 \cdot x^2))}&lt;br /&gt;
&amp;lt;/math&amp;gt;&lt;br /&gt;
| -0.000947416 &amp;lt; x &amp;lt; 0.000947207&lt;br /&gt;
| -inf &amp;lt; x &amp;lt; -1.51629e+06&lt;br /&gt;
1.51629e+06 &amp;lt; x &amp;lt; inf&lt;br /&gt;
|-&lt;br /&gt;
|pade&lt;br /&gt;
|&amp;lt;math&amp;gt;x + x \cdot \frac{x^2 \cdot \left( p_1 \cdot x^2 + p_0 \right)}{\left( x^2 + q_1 \right) \cdot x^2 + q_0} &lt;br /&gt;
&lt;br /&gt;
&amp;lt;/math&amp;gt; &amp;lt;math&amp;gt;\begin{align}&lt;br /&gt;
p_0 &amp;amp;= -0.2059432032 \\&lt;br /&gt;
p_1 &amp;amp;= -0.0009577527 \\&lt;br /&gt;
q_0 &amp;amp;= 0.6178299136 \\ &lt;br /&gt;
q_1 &amp;amp;= 0.25&lt;br /&gt;
\end{align}&lt;br /&gt;
&amp;lt;/math&amp;gt;&lt;br /&gt;
| -0.0016973 &amp;lt; x &amp;lt; 0.0016973 &lt;br /&gt;
| -inf &amp;lt; x &amp;lt; -6.14896e+18&lt;br /&gt;
6.14896e+18 &amp;lt; x &amp;lt; inf&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== External Links ==&lt;br /&gt;
&lt;br /&gt;
* https://yaikhom.com/2020-04-28-localised-approximation-of-hyperbolic-tangents.html#mjx-eqn-eqn7-th%20degree%20lambert%20approximant&lt;br /&gt;
*https://github.com/llvm/llvm-project/blob/main/libc/src/math/generic/tanhf.cpp&lt;br /&gt;
*https://forums.developer.nvidia.com/t/hardware-accelerated-tanh-on-turing/173291&lt;br /&gt;
[[Category:Numerical Precision]]&lt;/div&gt;</summary>
		<author><name>Timo.stripf</name></author>
	</entry>
	<entry>
		<id>https://www.emmtrix.com/w139/index.php?title=Category:Numerical_Precision&amp;diff=2913</id>
		<title>Category:Numerical Precision</title>
		<link rel="alternate" type="text/html" href="https://www.emmtrix.com/w139/index.php?title=Category:Numerical_Precision&amp;diff=2913"/>
		<updated>2026-02-03T09:50:52Z</updated>

		<summary type="html">&lt;p&gt;Timo.stripf: Timo.stripf moved page Category:Math Function Accuracy to Category:Numerical Precision without leaving a redirect&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;[[Category:Categories]]&lt;/div&gt;</summary>
		<author><name>Timo.stripf</name></author>
	</entry>
	<entry>
		<id>https://www.emmtrix.com/w139/index.php?title=Finding_Highest_ULP_of_Math_Functions&amp;diff=2912</id>
		<title>Finding Highest ULP of Math Functions</title>
		<link rel="alternate" type="text/html" href="https://www.emmtrix.com/w139/index.php?title=Finding_Highest_ULP_of_Math_Functions&amp;diff=2912"/>
		<updated>2026-02-03T09:50:27Z</updated>

		<summary type="html">&lt;p&gt;Timo.stripf: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;br /&gt;
This article discusses methods for measuring and finding the maximum Unit in the Last Place (ULP) error in floating-point math functions. It covers both single-precision (binary32) and double-precision (binary64) IEEE 754 formats, highlighting the impracticality of exhaustive search for large input spaces and presenting a universal search algorithm that focuses on identifying inputs likely to produce large errors.&lt;br /&gt;
&lt;br /&gt;
== Background and Motivation ==&lt;br /&gt;
&lt;br /&gt;
In the IEEE 754 single-precision format (binary32), there are &amp;lt;math&amp;gt;2^{32} - 2^{24}&amp;lt;/math&amp;gt; finite representable values (excluding &amp;lt;math&amp;gt;\pm\infty&amp;lt;/math&amp;gt; and NaN). For a function with one input parameter, it is still feasible to exhaustively test all possible values. However, difficulties arise when:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Single-precision bivariate functions&#039;&#039;&#039;: The input space could be as large as &amp;lt;math&amp;gt;2^{64}&amp;lt;/math&amp;gt; pairs of 32-bit floats, making an exhaustive search prohibitively expensive with academic resources.&lt;br /&gt;
* &#039;&#039;&#039;Double-precision (binary64) functions&#039;&#039;&#039;: The input space grows even larger (&amp;lt;math&amp;gt;2^{64}&amp;lt;/math&amp;gt; possible 64-bit patterns for a univariate function, and &amp;lt;math&amp;gt;2^{128}&amp;lt;/math&amp;gt; for a bivariate function), making a full enumeration infeasible.&lt;br /&gt;
&lt;br /&gt;
Although a full enumeration for some single-precision univariate functions can be practical, bivariate or double-precision functions present exponentially larger input spaces. Exhaustive testing quickly becomes impossible with standard computational resources. Hence, a “black-box” approach is beneficial:&lt;br /&gt;
&lt;br /&gt;
* It avoids manual analysis of library code.&lt;br /&gt;
* It does not require highly specialized tests for each function or library.&lt;br /&gt;
* It can detect errors such as incorrect argument reduction for large inputs (e.g., near 2^{1024} for sin or cos) without manually coding edge-case checks.&lt;br /&gt;
&lt;br /&gt;
== Motivation for a Universal Algorithm ==&lt;br /&gt;
&lt;br /&gt;
Exhaustive search for single-precision univariate functions is sometimes practical. However, for higher-dimensional or double-precision functions, the sheer number of inputs makes exhaustive testing nearly impossible with standard computational resources. &lt;br /&gt;
&lt;br /&gt;
A “black-box” approach to testing:&lt;br /&gt;
* Avoids manual analysis of library code.&lt;br /&gt;
* Eliminates the need for highly specialized tests for each function or library.&lt;br /&gt;
* Allows the detection of errors such as incorrect argument reduction for large inputs (e.g., near &amp;lt;math&amp;gt;2^{1024}&amp;lt;/math&amp;gt; for &amp;lt;math&amp;gt;\sin&amp;lt;/math&amp;gt; or &amp;lt;math&amp;gt;\cos&amp;lt;/math&amp;gt;) without manually coding those edge-case checks.&lt;br /&gt;
&lt;br /&gt;
== Universal Search Algorithm ==&lt;br /&gt;
&lt;br /&gt;
This section outlines the subdivision-based search algorithm for univariate double-precision functions, though the same approach can be adapted to:&lt;br /&gt;
* Any IEEE 754 format, provided there is a corresponding integer type of the same bit width.&lt;br /&gt;
* Bivariate or higher-dimensional functions (with appropriate modifications).&lt;br /&gt;
&lt;br /&gt;
=== Algorithm Overview ===&lt;br /&gt;
&lt;br /&gt;
Suppose we have a univariate function &amp;lt;math&amp;gt; f(x) &amp;lt;/math&amp;gt; in double precision. Each possible input &amp;lt;math&amp;gt; x &amp;lt;/math&amp;gt; can be associated with a 64-bit integer, thanks to the binary64 format. Let:&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;to_double(uint64_t)&amp;lt;/code&amp;gt; be a function that converts a 64-bit unsigned integer to its corresponding double-precision floating-point number.&lt;br /&gt;
* &amp;lt;code&amp;gt;check(double x)&amp;lt;/code&amp;gt; be a function that checks f at position x and returns the ULP error&lt;br /&gt;
* [a, b] be an inclusive 64-bit integer range representing a subrange of possible inputs.&lt;br /&gt;
* t be a threshold indicating when to switch from sampling to exhaustive checking.&lt;br /&gt;
&lt;br /&gt;
The algorithm proceeds as follows:&lt;br /&gt;
&lt;br /&gt;
# &#039;&#039;&#039;Base Case (Exhaustive Check)&#039;&#039;&#039;: If the interval size &amp;lt;math&amp;gt;(b - a) &amp;lt; t&amp;lt;/math&amp;gt;, then test &#039;&#039;&#039;all&#039;&#039;&#039; integers i in [a, b].   &lt;br /&gt;
#*For each i in [a, b]&lt;br /&gt;
#**convert i to double: &amp;lt;code&amp;gt;x = to_double(i)&amp;lt;/code&amp;gt;&lt;br /&gt;
#**check f at x: &amp;lt;code&amp;gt;err = check(x)&amp;lt;/code&amp;gt;&lt;br /&gt;
#**and keep track of the largest observed error.&lt;br /&gt;
#&#039;&#039;&#039;Recursive Subdivision&#039;&#039;&#039;: If the interval size &amp;lt;math&amp;gt;(b - a) \geq t&amp;lt;/math&amp;gt;&lt;br /&gt;
#*Subdivide [a, b] into two (almost equal) sub-intervals.&lt;br /&gt;
#*In each subinterval, randomly sample t values.&lt;br /&gt;
#*Convert each sampled integer to double and compute the ULP error as above.&lt;br /&gt;
#*Identify the subrange producing the largest observed error.&lt;br /&gt;
#*Recurse on that subrange.&lt;br /&gt;
This subdivision continues until the interval width becomes less than the total number of function evaluations already performed. At that point, the algorithm can afford an exhaustive search on the remaining values without excessively prolonging the total runtime.&lt;br /&gt;
&lt;br /&gt;
=== Practical Optimization: “Top-K” Subintervals===&lt;br /&gt;
&lt;br /&gt;
A more refined strategy, suggested by Eric Schneider, modifies the subdivision step to keep multiple promising subintervals at each level instead of focusing on just one:&lt;br /&gt;
&lt;br /&gt;
*Maintain a list of, for example, 20 subintervals (instead of one) with the largest observed errors.&lt;br /&gt;
*Subdivide each selected subinterval, yielding 40 (or 80 for bivariate) new subintervals in total.&lt;br /&gt;
* Retain the 20 most “promising” intervals out of these newly generated ones.&lt;br /&gt;
*Continue until the intervals are small enough to be exhaustively checked or until resources are exhausted.&lt;br /&gt;
&lt;br /&gt;
===Additional Strategies for Selecting Subintervals===&lt;br /&gt;
&lt;br /&gt;
Several heuristics can be used to choose the “best” subintervals at each recursion step:&lt;br /&gt;
&lt;br /&gt;
# &#039;&#039;&#039;Maximal ULP error&#039;&#039;&#039;: Keep the subinterval with the highest single-sample error.&lt;br /&gt;
# &#039;&#039;&#039;Maximal average ULP error&#039;&#039;&#039;: Keep the subinterval whose sampled points give the highest average ULP error (ignoring cases that produce NaN or &amp;lt;math&amp;gt;\pm\infty&amp;lt;/math&amp;gt;).&lt;br /&gt;
# &#039;&#039;&#039;Statistical estimate&#039;&#039;&#039;: Model the error distribution on each subinterval and estimate the maximum ULP error for the number of points in that interval.&lt;br /&gt;
&lt;br /&gt;
In practice, the first strategy often finds large errors most quickly, while the second and third strategies can occasionally discover extreme corner cases. A useful approach on multi-core systems is to run strategy 2 and 3 each on one core and strategy 1 on the remaining cores.&lt;br /&gt;
===Extension to Bivariate Functions===&lt;br /&gt;
For bivariate functions, there are two approaches to adapt the algorithm. Suppose we have a bivariate function &amp;lt;math&amp;gt; f(x, y) &amp;lt;/math&amp;gt; in single precision and the &amp;lt;code&amp;gt;to_single(uint32_t&amp;gt;)&amp;lt;/code&amp;gt; function that converts a 32-bit unsigned integer to its corresponding single-precision float.&lt;br /&gt;
&lt;br /&gt;
* We can use two 32-bit integers i1 and i2 to represent each input. The integers are then converted to single-precision floats using &amp;lt;code&amp;gt;to_single&amp;lt;/code&amp;gt;. The intervals become rectangular intervals and subdivide of the rectangular intervals create four sub-intervals.&lt;br /&gt;
* Alternatively, we can use a single 64-bit integer to represent the input. The intervals are still one-dimensional. The 64-bit integer is split into two 32-bit using odd and even bits before converting them to single-precision floats. The algorithm then proceeds as before.&lt;br /&gt;
&lt;br /&gt;
==Conclusions==&lt;br /&gt;
&lt;br /&gt;
Finding the highest ULP error for floating-point math functions is an essential part of ensuring numerical accuracy. While exhaustive methods are still possible for some single-precision use cases, more complex scenarios require search-based or sampling-based algorithms. The general-purpose algorithm outlined here:&lt;br /&gt;
&lt;br /&gt;
*Does not rely on inspecting the internal implementations of math libraries.&lt;br /&gt;
*Does not require function-specific logic.&lt;br /&gt;
*Subdivides large input spaces, refining searches where large errors are found.&lt;br /&gt;
&lt;br /&gt;
The approach yields a practical, largely automated tool for pinpointing problematic inputs—even in extreme domains—offering valuable insights into the accuracy and robustness of floating-point math libraries.&lt;br /&gt;
[[Category:Numerical Precision]]&lt;/div&gt;</summary>
		<author><name>Timo.stripf</name></author>
	</entry>
	<entry>
		<id>https://www.emmtrix.com/w139/index.php?title=Numerical_Precision_in_ONNX_and_AI_Inference&amp;diff=2911</id>
		<title>Numerical Precision in ONNX and AI Inference</title>
		<link rel="alternate" type="text/html" href="https://www.emmtrix.com/w139/index.php?title=Numerical_Precision_in_ONNX_and_AI_Inference&amp;diff=2911"/>
		<updated>2026-02-03T09:46:20Z</updated>

		<summary type="html">&lt;p&gt;Timo.stripf: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&#039;&#039;&#039;Open Neural Network Exchange (ONNX)&#039;&#039;&#039; is an open standard format for representing machine learning models and neural network computations across different frameworks and hardware. As models are exported and deployed via ONNX, the &#039;&#039;&#039;numerical precision&#039;&#039;&#039; of computations becomes critical. Deep learning inference involves a variety of floating-point operations, and small numerical discrepancies can accumulate and affect model accuracy or reproducibility. This article provides an in-depth overview of floating-point precision issues in numerical computing, with a focus on how they manifest in AI inference and the ONNX ecosystem. We cover floating-point formats and rounding behavior, accumulation strategies for summations, the use of low-precision formats (FP16, BFloat16, FP8) in modern AI, and how ONNX defines (or leaves unspecified) the precision of accumulators in operations. Strategies for maintaining numerical stability in deep neural networks are discussed, along with the trade-offs between computational performance and arithmetic accuracy. The goal is to give engineers and researchers a comprehensive, encyclopedic reference on numerical precision in ONNX-based AI inference systems. &amp;lt;ref name=&amp;quot;ref1&amp;quot;/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Floating-Point Precision in Numerical Computing ==&lt;br /&gt;
&lt;br /&gt;
=== 2.1. IEEE 754 Floating-Point Formats ===&lt;br /&gt;
&lt;br /&gt;
Modern computers represent real numbers using the IEEE 754 floating-point standard, which defines formats like 32-bit &#039;&#039;single precision&#039;&#039; (FP32) and 64-bit &#039;&#039;double precision&#039;&#039; (FP64). In IEEE 754 binary formats, a floating-point number is composed of three fields: 1 sign bit, several exponent bits (with a bias), and several fraction (mantissa) bits. For example, FP32 consists of 1 sign bit, 8 exponent bits, and 23 fraction bits (24 bits of significand precision including the implicit leading 1). This provides roughly 7 decimal digits of precision and an exponent range allowing values on the order of &#039;&#039;10&amp;lt;sup&amp;gt;±38&amp;lt;/sup&amp;gt;&#039;&#039;. FP64 allocates 1 sign bit, 11 exponent bits, and 52 fraction bits (53-bit precision), yielding about 15–16 decimal digits of precision and a vastly larger range (~&#039;&#039;10&amp;lt;sup&amp;gt;±308&amp;lt;/sup&amp;gt;&#039;&#039;). These larger formats (especially FP64) are the &amp;quot;gold standard&amp;quot; for numerical accuracy in scientific computing, as they reduce round-off error in complex calculations. However, the increased precision comes at the cost of doubled storage and typically reduced computational throughput (e.g. many GPUs execute FP64 instructions at a much lower rate than FP32). &amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
In addition to single and double precision, the IEEE 754-2008 standard introduced a 16-bit &#039;&#039;half precision&#039;&#039; format (FP16, or binary16). FP16 has 1 sign bit, 5 exponent bits, and 10 fraction bits (11-bit significand including the implicit 1). This format covers a much smaller dynamic range (approximately &#039;&#039;6.1 × 10&amp;lt;sup&amp;gt;-5&amp;lt;/sup&amp;gt;&#039;&#039; to &#039;&#039;6.5 × 10&amp;lt;sup&amp;gt;4&amp;lt;/sup&amp;gt;&#039;&#039; for normalized values) and offers only around 3 decimal digits of precision. Half precision was originally used in graphics, but it has gained popularity in machine learning for its speed and memory advantages (discussed later in Section 4). Another format relevant to AI is &#039;&#039;&#039;BFloat16&#039;&#039;&#039; (Brain Float 16), a 16-bit float with &#039;&#039;&#039;8 exponent bits and 7 fraction bits&#039;&#039;&#039;. BFloat16 sacrifices precision (only ~7 fraction bits ≈ 2–3 decimal digits) in order to have the &#039;&#039;&#039;same exponent range as FP32&#039;&#039;&#039;. This means BFloat16 can represent very large or very small numbers (∼&#039;&#039;10&amp;lt;sup&amp;gt;±38&amp;lt;/sup&amp;gt;&#039;&#039;, similar range as FP32) but with much less precision between representable values. BFloat16 was introduced for deep learning by Google (for TPUs) to allow reduced precision &#039;&#039;training&#039;&#039; without frequent overflow/underflow, relying on the wide range and accepting the coarse precision. &amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Each floating-point format has a &#039;&#039;&#039;unit roundoff&#039;&#039;&#039; or machine precision, denoted &#039;&#039;ε&#039;&#039;. This is effectively the spacing between 1.0 and the next representable number. For FP32, &#039;&#039;ε ≈ 2&amp;lt;sup&amp;gt;-23&amp;lt;/sup&amp;gt; ≈ 1.19 × 10&amp;lt;sup&amp;gt;-7&amp;lt;/sup&amp;gt;&#039;&#039;, corresponding to about 7–8 decimal digits of precision. In FP64, &#039;&#039;ε = 2&amp;lt;sup&amp;gt;-52&amp;lt;/sup&amp;gt; ≈ 2.22 × 10&amp;lt;sup&amp;gt;-16&amp;lt;/sup&amp;gt;&#039;&#039; (15–16 decimal digits). Half precision is much less precise: FP16 has &#039;&#039;ε = 2&amp;lt;sup&amp;gt;-10&amp;lt;/sup&amp;gt; ≈ 9.76 × 10&amp;lt;sup&amp;gt;-4&amp;lt;/sup&amp;gt;&#039;&#039;, and BFloat16 has &#039;&#039;ε = 2&amp;lt;sup&amp;gt;-7&amp;lt;/sup&amp;gt; = 7.8125 × 10&amp;lt;sup&amp;gt;-3&amp;lt;/sup&amp;gt;&#039;&#039;. These values quantify how finely the continuum of real numbers is quantized in each format. &amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 2.2. Machine Epsilon and Rounding Error ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Machine epsilon (&#039;&#039;ε&#039;&#039;)&#039;&#039;&#039; is formally defined as the smallest positive number such that &#039;&#039;1.0 + ε &amp;gt; 1.0&#039;&#039; in the floating-point format. It effectively measures the &#039;&#039;&#039;precision&#039;&#039;&#039; of the representation. In FP32, &#039;&#039;ε = 2&amp;lt;sup&amp;gt;-23&amp;lt;/sup&amp;gt; ≈ 1.19 × 10&amp;lt;sup&amp;gt;-7&amp;lt;/sup&amp;gt;&#039;&#039;, meaning any result is rounded to about 7 decimal digits. FP16’s &#039;&#039;ε = 2&amp;lt;sup&amp;gt;-10&amp;lt;/sup&amp;gt; ≈ 9.77 × 10&amp;lt;sup&amp;gt;-4&amp;lt;/sup&amp;gt;&#039;&#039;, over 1000× larger (so only ~3 decimal digits of precision), and BFloat16’s &#039;&#039;ε = 2&amp;lt;sup&amp;gt;-7&amp;lt;/sup&amp;gt; ≈ 7.8 × 10&amp;lt;sup&amp;gt;-3&amp;lt;/sup&amp;gt;&#039;&#039;, reflecting its mere ~8-bit precision. Rounding in IEEE 754 by default uses &#039;&#039;&#039;round-to-nearest (ties to even)&#039;&#039;&#039;, which ensures the rounding error for any single operation is at most half a unit in the last place (0.5 ULP). We can model a floating-point addition as: &amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;\text{fl}(a+b) = (a+b)\,(1 + \delta),&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
where &#039;&#039;|δ| ≤ ε&#039;&#039; for that format. Here &#039;&#039;ε/2&#039;&#039; is the maximum &#039;&#039;&#039;relative error&#039;&#039;&#039; in one rounding (also called unit roundoff). For example, when adding two FP32 numbers, the result is accurate to within about &#039;&#039;10&amp;lt;sup&amp;gt;-7&amp;lt;/sup&amp;gt;&#039;&#039; of the true sum (relative), whereas in BF16, the result can deviate by up to &#039;&#039;∼7.8 × 10&amp;lt;sup&amp;gt;-3&amp;lt;/sup&amp;gt;&#039;&#039; (nearly 0.78% relative error per operation). Thus, &#039;&#039;&#039;each arithmetic operation incurs a small rounding error&#039;&#039;&#039; (on the order of machine epsilon) that can propagate or accumulate through a sequence of calculations. &amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Because floating-point representations have finite exponent range, we must also consider &#039;&#039;&#039;overflow&#039;&#039;&#039; and &#039;&#039;&#039;underflow&#039;&#039;&#039;. If a result’s magnitude exceeds the maximum representable value, it overflows to infinity; if it falls below the minimum normal (or minimum subnormal) value, it underflows to zero (or a denormalized tiny value). FP32 and BF16 share an exponent size (8 bits), so their range for normal numbers is roughly &#039;&#039;[10&amp;lt;sup&amp;gt;-38&amp;lt;/sup&amp;gt;, 10&amp;lt;sup&amp;gt;38&amp;lt;/sup&amp;gt;]&#039;&#039; (approx &amp;lt;math&amp;gt;~\!3.4\times10^{38}&amp;lt;/math&amp;gt; max). FP16’s 5-bit exponent allows max &#039;&#039;∼ 6.55 × 10&amp;lt;sup&amp;gt;4&amp;lt;/sup&amp;gt;&#039;&#039; and min normal &#039;&#039;∼ 6.10 × 10&amp;lt;sup&amp;gt;-5&amp;lt;/sup&amp;gt;&#039;&#039;. In practice, &#039;&#039;&#039;underflow/overflow&#039;&#039;&#039; can occur in low-precision arithmetic if values are not appropriately scaled. For instance, summing a large number of moderately sized terms in FP16 could overflow where FP32 would not. As an example, summing on the order of &#039;&#039;10&amp;lt;sup&amp;gt;4&amp;lt;/sup&amp;gt;&#039;&#039; values of magnitude ~&#039;&#039;10&amp;lt;sup&amp;gt;1&amp;lt;/sup&amp;gt;&#039;&#039; (say, adding 10,000 terms around 10 each) would produce a total &#039;&#039;∼10&amp;lt;sup&amp;gt;5&amp;lt;/sup&amp;gt;&#039;&#039;, which exceeds FP16’s max finite value ~&#039;&#039;6.5 × 10&amp;lt;sup&amp;gt;4&amp;lt;/sup&amp;gt;&#039;&#039; and would overflow to &#039;&#039;+∞&#039;&#039; in half precision. Underflow is usually a concern when subtracting nearly equal numbers (catastrophic cancellation yielding a tiny difference that might flush to zero in low precision). &amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 2.3. Accumulation Error in Reductions ===&lt;br /&gt;
&lt;br /&gt;
Floating-point addition is &#039;&#039;&#039;not associative&#039;&#039;&#039;: &#039;&#039;(a+b)+c&#039;&#039; can differ from &#039;&#039;a+(b+c)&#039;&#039; due to rounding, and thus the order in which we sum a list of numbers can change the result slightly. When summing a large sequence of &#039;&#039;N&#039;&#039; numbers &#039;&#039;x&amp;lt;sub&amp;gt;1&amp;lt;/sub&amp;gt;, x&amp;lt;sub&amp;gt;2&amp;lt;/sub&amp;gt;,..., x&amp;lt;sub&amp;gt;N&amp;lt;/sub&amp;gt;&#039;&#039;, the &#039;&#039;&#039;accumulated rounding error&#039;&#039;&#039; can be significant. In the &#039;&#039;worst case&#039;&#039; (e.g., adding many positive and negative terms that cause maximal cancellation or always rounding one way), the error can grow on the order of &#039;&#039;O(N ε)&#039;&#039; relative to the exact sum. Simply summing &#039;&#039;N&#039;&#039; numbers in sequence (the &#039;&#039;&#039;naive linear summation&#039;&#039;&#039;) has a worst-case error bound proportional to &amp;lt;math&amp;gt;N\,\varepsilon&amp;lt;/math&amp;gt;. This worst-case occurs for adversarial arrangements of values; in more typical scenarios with rounding errors of random signs, one can model the error as a random walk, yielding an expected root-mean-square (RMS) error growth on the order of &amp;lt;math&amp;gt;O(\sqrt{N}\,\varepsilon)&amp;lt;/math&amp;gt;. Still, with very large &#039;&#039;N&#039;&#039;, even &amp;lt;math&amp;gt;\sqrt{N}&amp;lt;/math&amp;gt; times &#039;&#039;ε&#039;&#039; can become large enough to matter. &amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Cancellation&#039;&#039;&#039; is a particular problem: if we add two nearly equal numbers of opposite sign, most leading digits cancel and the result loses significant bits of precision. For example, in single precision, subtracting two numbers that agree in the first 6 decimal digits will yield a result with only ~1 digit of accuracy left. Catastrophic cancellation can dramatically increase relative error. Summation is especially sensitive to cancellation if large positive and negative terms are summed naively. The &#039;&#039;&#039;condition number&#039;&#039;&#039; of a summation (the ratio of the sum of absolute values &amp;lt;math&amp;gt;\sum|x_i|&amp;lt;/math&amp;gt; to the absolute value of the true sum &amp;lt;math&amp;gt;|\sum x_i|&amp;lt;/math&amp;gt;) indicates how sensitive the result is to perturbations. If the numbers have mixed signs and nearly cancel out (making the condition number large), even an optimal summation order will have a large relative error. For instance, summing [1.0, &#039;&#039;10&amp;lt;sup&amp;gt;100&amp;lt;/sup&amp;gt;&#039;&#039;, 1.0, &#039;&#039;-10&amp;lt;sup&amp;gt;100&amp;lt;/sup&amp;gt;&#039;&#039;] in double precision has an exact sum of 2.0, but naive summation may yield 0.0 due to cancellation; an improved summation algorithm yields the correct 2.0. &amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
In practical terms, when summing a long list of floating-point numbers (such as accumulating a dot product or reducing a tensor), the &#039;&#039;&#039;accumulation error&#039;&#039;&#039; can become noticeable if no precautions are taken. Techniques to mitigate this error include carefully choosing the summation order or using higher precision for intermediate sums. We will explore these strategies in the next section.&lt;br /&gt;
&lt;br /&gt;
== Accumulators and Reduction Strategies ==&lt;br /&gt;
&lt;br /&gt;
When performing reductions (sums, dot products, etc.), the method of accumulation can greatly affect the accuracy of the result. Below, we discuss various strategies:&lt;br /&gt;
&lt;br /&gt;
=== 3.1. Naive Linear Accumulation ===&lt;br /&gt;
&lt;br /&gt;
The simplest accumulation strategy is to use a single accumulator and add each element sequentially. In C-like pseudocode:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;c&amp;quot;&amp;gt;&lt;br /&gt;
float sum = 0.0f;&lt;br /&gt;
for (int i = 0; i &amp;lt; N; ++i) {&lt;br /&gt;
    sum += array[i];&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This &#039;&#039;&#039;naive linear summation&#039;&#039;&#039; adds values one by one to the running total. While straightforward and fast, it is prone to the rounding error accumulation described earlier. The partial sum &amp;lt;code&amp;gt;sum&amp;lt;/code&amp;gt; continually grows (in absolute value) as terms are added, so new small terms may fall outside its precision. For example, if &amp;lt;code&amp;gt;sum&amp;lt;/code&amp;gt; is large and positive, adding a comparatively tiny positive number may produce no change because the small term&#039;s significant digits vanish in the presence of the large sum (the increment is below the rounding threshold of &amp;lt;code&amp;gt;sum&amp;lt;/code&amp;gt;). Conversely, adding a small number of opposite sign can suffer from cancellation. The order of addition matters: summing from smallest magnitude to largest (ascending order) tends to yield a more accurate result than summing in descending order, because adding small terms first preserves their contributions before the sum becomes very large. The naive loop above implicitly sums in the given index order; if the data is unsorted, this order is essentially arbitrary.&lt;br /&gt;
&lt;br /&gt;
The performance of linear accumulation is excellent (minimal overhead), and for many applications the resulting error is within acceptable limits. However, for &#039;&#039;&#039;very large N&#039;&#039;&#039; or ill-conditioned sums, the inaccuracy can become a problem. In a reduction involving millions of elements, a single-precision sum could lose several digits of accuracy. In deep learning, this could translate to small discrepancies in layer outputs or aggregated gradients if done naively in low precision.&lt;br /&gt;
&lt;br /&gt;
=== 3.2. Pairwise and Tree-Based Reductions ===&lt;br /&gt;
&lt;br /&gt;
A better approach to summation is &#039;&#039;&#039;pairwise summation&#039;&#039;&#039;, which sums numbers in a binary tree structure rather than linearly. The idea is to recursively split the array into halves, sum each half, then add the two partial sums. This can be implemented as:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;text&amp;quot;&amp;gt;&lt;br /&gt;
function pairwise_sum(x[1..n]):&lt;br /&gt;
    if n == 0:&lt;br /&gt;
        return 0&lt;br /&gt;
    if n == 1:&lt;br /&gt;
        return x[1]&lt;br /&gt;
    m = floor(n/2)&lt;br /&gt;
    left_sum  = pairwise_sum(x[1..m])&lt;br /&gt;
    right_sum = pairwise_sum(x[m+1..n])&lt;br /&gt;
    return left_sum + right_sum&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
By always adding numbers of similar magnitudes first (small subsets are reduced to subtotals, then those are added, etc.), pairwise summation avoids the situation of a very large running total absorbing small additions until the final steps. The error of pairwise summation grows logarithmically with &#039;&#039;N&#039;&#039; in the worst case (roughly &#039;&#039;O(ε log&amp;lt;sub&amp;gt;2&amp;lt;/sub&amp;gt; N)&#039;&#039;), instead of linearly. Intuitively, at each level of the tree, rounding error is constrained, and there are about &#039;&#039;log&amp;lt;sub&amp;gt;2&amp;lt;/sub&amp;gt; N&#039;&#039; levels. In fact, the worst-case relative error bound for pairwise summation (base case of 1 element) is: &amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;|E_N| \le \frac{\varepsilon \log_2 N}{1 - \varepsilon \log_2 N} \sum_{i=1}^N |x_i|,&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
which for practical &#039;&#039;N&#039;&#039; simplifies to on the order of &#039;&#039;ε log&amp;lt;sub&amp;gt;2&amp;lt;/sub&amp;gt; N&#039;&#039; times the condition number of the sum. In typical random-error scenarios, the error behaves even better (on the order of &amp;lt;math&amp;gt;\sqrt{\log N}&amp;lt;/math&amp;gt;). The key is that &#039;&#039;&#039;pairwise (tree) reduction balances the accumulations&#039;&#039;&#039;, preventing one sum from overwhelming the others too early. &amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
From a performance standpoint, pairwise summation has the same &#039;&#039;O(N)&#039;&#039; total additions as linear summation, just a different order. It is also highly amenable to parallelization: independent partial sums can be computed in parallel at each level of the tree. Many numerical libraries and BLAS implementations use a form of blocked or pairwise summation in dot products for improved accuracy at negligible performance cost. For example, NumPy and Julia perform partial pairwise summation by default for better numerical behavior. &amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 3.3. Compensated Summation (Kahan, Neumaier) ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Compensated summation&#039;&#039;&#039; is an alternative technique to improve summation accuracy by tracking the small “lost” quantities. The most famous algorithm is the &#039;&#039;&#039;Kahan summation algorithm&#039;&#039;&#039; (also called Kahan–Babuška). Kahan summation uses an extra variable to accumulate the round-off error. In pseudocode: &amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;c&amp;quot;&amp;gt;&lt;br /&gt;
double sum = 0.0;&lt;br /&gt;
double c = 0.0;    // compensation for lost low-order bits&lt;br /&gt;
for (i = 1; i &amp;lt;= N; ++i) {&lt;br /&gt;
    double y = x[i] - c;    // recover low-order bits by subtracting compensation&lt;br /&gt;
    double t = sum + y;     // perform the addition&lt;br /&gt;
    c = (t - sum) - y;      // compute new compensation (the error in t)&lt;br /&gt;
    sum = t;&lt;br /&gt;
}&lt;br /&gt;
return sum;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Here &amp;lt;code&amp;gt;c&amp;lt;/code&amp;gt; accumulates the tiny leftovers that would otherwise be dropped. Each iteration essentially subtracts the last error from the new addend (&amp;lt;code&amp;gt;y = x[i] - c&amp;lt;/code&amp;gt;) so that &amp;lt;code&amp;gt;t = sum + y&amp;lt;/code&amp;gt; includes the small bits from the previous operation. The new error is then stored in &amp;lt;code&amp;gt;c&amp;lt;/code&amp;gt; for the next iteration. The result is that the final &amp;lt;code&amp;gt;sum&amp;lt;/code&amp;gt; is as if computed with higher precision than the machine’s: Kahan’s algorithm can &#039;&#039;&#039;greatly reduce the error&#039;&#039;&#039; in summing a sequence of floats. In fact, with exact arithmetic for the compensation, Kahan summation yields a result accurate to within 1 or 2 ULP of the true sum, regardless of N (assuming the sequence of partial sums doesn’t overflow). More precisely, Kahan summation achieves a &#039;&#039;&#039;worst-case error independent of N&#039;&#039;&#039; (error bound depends only on machine precision, not the number of terms). In practice, using a double accumulator for summing single-precision numbers via Kahan can give near double-precision accuracy. &amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
An important subtlety is that the compensation variable &amp;lt;code&amp;gt;c&amp;lt;/code&amp;gt; should be kept in &#039;&#039;full precision&#039;&#039;. In the implementation above, we used &amp;lt;code&amp;gt;double&amp;lt;/code&amp;gt; for both sum and c. If summing FP32 values, one might use &amp;lt;code&amp;gt;float sum&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;float c&amp;lt;/code&amp;gt; (both 32-bit), which still improves accuracy, but using a higher precision for the accumulator and compensation (i.e. double) yields even better results. Many implementations of Kahan summation use the same type for sum and c, but it&#039;s assumed to be higher precision than the inputs when possible.&lt;br /&gt;
&lt;br /&gt;
Kahan’s algorithm carries a performance cost: each iteration does five arithmetic operations (two adds, one subtract, etc.) and has a data dependency, which can hinder parallelism or pipelining. Thus, it runs slower than naive summation (roughly 2–3× slower or more, depending on CPU pipeline). In loops where summation is a bottleneck, this may be significant. Nonetheless, for modest N or where maximum accuracy is required, compensated summation is extremely useful. In fact, Python’s &amp;lt;code&amp;gt;math.fsum&amp;lt;/code&amp;gt; function uses a variant of Kahan’s algorithm (actually a more advanced multi-part compensation) to sum iterables with high precision. &amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
There are variations and improvements on basic Kahan summation. &#039;&#039;&#039;Neumaier’s algorithm&#039;&#039;&#039; (sometimes called &#039;&#039;Kahan-Babuška-Neumaier&#039;&#039;) improves Kahan’s method by also handling cases where the next input is larger in magnitude than the running sum. In such cases, the original Kahan formula could suffer error; Neumaier’s version effectively swaps roles when a huge value comes in. Its pseudocode is: &amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;text&amp;quot;&amp;gt;&lt;br /&gt;
function NeumaierSum(array):&lt;br /&gt;
    sum = 0.0&lt;br /&gt;
    compensation = 0.0&lt;br /&gt;
    for each x in array:&lt;br /&gt;
        t = sum + x&lt;br /&gt;
        if (fabs(sum) &amp;gt;= fabs(x))&lt;br /&gt;
            compensation += (sum - t) + x;&lt;br /&gt;
        else&lt;br /&gt;
            compensation += (x - t) + sum;&lt;br /&gt;
        sum = t;&lt;br /&gt;
    return sum + compensation;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This algorithm ensures the compensation covers both scenarios: when the sum is the bigger addend or when the new term is bigger. In the end, it adds the final &amp;lt;code&amp;gt;compensation&amp;lt;/code&amp;gt; once to the result. Neumaier’s method can produce the correct result in cases where basic Kahan fails. For instance, summing [1.0, &#039;&#039;+10&amp;lt;sup&amp;gt;100&amp;lt;/sup&amp;gt;&#039;&#039;, 1.0, &#039;&#039;-10&amp;lt;sup&amp;gt;100&amp;lt;/sup&amp;gt;&#039;&#039;] in double precision: regular Kahan summation might give 0.0 (losing the small terms), whereas Neumaier’s algorithm yields 2.0 exactly. There are also higher-order compensation methods (carrying multiple compensation terms, e.g., the &#039;&#039;&#039;Klein summation&#039;&#039;&#039; variant or using arbitrary precision for intermediate sums) that further reduce error at increased computational cost. &amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
In summary, compensated summation algorithms trade extra computation for greatly improved numerical accuracy. They are especially valuable when summing large arrays of numbers with varying magnitudes, where naive summation could lose most of the low-order bits. Many scientific computing environments provide compensated sum routines for this reason. In deep learning, however, a different approach to improved precision is often taken: &#039;&#039;&#039;using a wider accumulator type&#039;&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
=== 3.4. Mixed-Precision Accumulation ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Mixed-precision accumulation&#039;&#039;&#039; refers to performing arithmetic in a higher precision than the input data. A common scenario is accumulating sums in 32-bit floats when the operands are 16-bit floats. Rather than implementing a compensation algorithm, one can simply use a wider accumulator so that intermediate rounding error is smaller. This is effectively what modern GPUs do for tensor operations: for example, NVIDIA &#039;&#039;&#039;Tensor Cores operate on FP16 input data with an FP32 accumulator&#039;&#039;&#039; (the 16-bit products are accumulated in 32-bit). In this way, summing 2048 products in a dot product has far less precision loss than it would with a 16-bit accumulator. Hardware that supports mixed precision can give the best of both worlds: fast low-precision multiplies and high-precision accumulation. &amp;lt;ref name=&amp;quot;ref6&amp;quot;/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Software can also leverage mixed precision. For instance, one can sum an array of &amp;lt;code&amp;gt;float&amp;lt;/code&amp;gt; values into a &amp;lt;code&amp;gt;double&amp;lt;/code&amp;gt; variable, accumulating the result in double precision and then converting back to float at the end:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;c&amp;quot;&amp;gt;&lt;br /&gt;
double sum = 0.0;&lt;br /&gt;
for (int i=0; i&amp;lt;N; ++i) {&lt;br /&gt;
    sum += (double) arr_float[i];&lt;br /&gt;
}&lt;br /&gt;
float result = (float) sum;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This approach often yields a more accurate &amp;lt;code&amp;gt;result&amp;lt;/code&amp;gt; than summing in single precision directly, because the intermediate sum carries 52-bit precision. The final rounding to float happens only once at the end (minimizing overall rounding error). In fact, the above is one way to implement &amp;lt;code&amp;gt;math.fsum&amp;lt;/code&amp;gt; in Python (it internally does something similar with multiple partial sums in double precision).&lt;br /&gt;
&lt;br /&gt;
In machine learning training and inference, &#039;&#039;&#039;mixed precision&#039;&#039;&#039; is widely used. When using FP16 for speed, it&#039;s common to accumulate with FP32. For example, &#039;&#039;&#039;Google&#039;s TPUs use BFloat16 for multiplication but accumulate in FP32&#039;&#039;&#039; for matrix ops. AWS’s Inferentia (Neuron) hardware similarly supports FP16/BF16 matrix multiplication with FP32 accumulation. The motivation is that summing hundreds or thousands of half-precision values would otherwise introduce intolerable error or risk overflow. With an FP32 accumulator, the reduction error is drastically reduced (by about &#039;&#039;2&amp;lt;sup&amp;gt;13&amp;lt;/sup&amp;gt;&#039;&#039; times, since FP32 has 13 more precision bits than FP16). Mixed-precision accumulation thus provides a simple and effective form of compensation: &#039;&#039;&#039;use a larger bucket to collect many small contributions&#039;&#039;&#039;. &amp;lt;ref name=&amp;quot;ref6&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref7&amp;quot;/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
There is a spectrum of options: summing int8 or int16 integers might use 32-bit integers as accumulators for the same reason (to avoid overflow). Some BLAS libraries even offer &#039;&#039;extended precision accumulators&#039;&#039; for single precision dot products (accumulating in double). The downside of mixed precision is the extra memory and maybe throughput cost of the wider type. But on many platforms, the add latency is dominated by hardware design rather than bit-width, so using a 32-bit adder vs 16-bit has negligible speed impact if supported natively. The key is whether data movement or register pressure increases.&lt;br /&gt;
&lt;br /&gt;
In summary, &#039;&#039;&#039;using a higher precision accumulator is often the easiest way to improve numerical accuracy&#039;&#039;&#039; for reductions. It is a primary strategy in deep learning hardware to maintain stability while exploiting low precision for storage and initial computations.&lt;br /&gt;
&lt;br /&gt;
== Precision Formats in AI and Machine Learning ==&lt;br /&gt;
&lt;br /&gt;
Modern AI systems use a variety of numeric formats to balance speed, memory, and accuracy. We discuss common precisions and their roles:&lt;br /&gt;
&lt;br /&gt;
=== 4.1. FP32 and FP64 in Training and Inference ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;FP32 (single precision)&#039;&#039;&#039; has been the workhorse of deep learning for years. Most training scripts in frameworks like TensorFlow and PyTorch used FP32 by default, as it offers sufficient precision to propagate gradients and accumulate weight updates over millions of iterations without excessive error. With ~7 decimal digits of precision and a wide exponent range, FP32 can handle the dynamic range of activations and gradients in typical neural networks. In inference, FP32 is often more precision than strictly necessary for final predictions, but it has remained common because of its ease and the fact that many GPU operations were optimized for FP32.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;FP64 (double precision)&#039;&#039;&#039; is rarely used in deep learning model training or inference, except in specific scenarios requiring extreme precision. FP64 is common in scientific computing and certain types of numerical simulations. In neural networks, however, the extra precision is usually not needed for model accuracy—using doubles tends to yield negligible improvements in model metrics but at a large performance cost (since on GPUs FP64 throughput can be 1/2, 1/8, or even 1/32 of FP32 throughput depending on hardware). One exception might be when integrating neural nets into larger physics simulations or when solving ill-conditioned problems as part of the model. Some researchers use FP64 to diagnose numerical issues or ensure reproducibility. Generally though, deep learning has favored lower precision for efficiency, and FP64 is considered overkill for most networks. &amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
That said, certain classical machine learning algorithms benefit from double precision. For example, computing the inverse of a matrix (as in a Gaussian Process regression or Kalman filter embedded in a model) can incur large rounding errors in single precision, so using FP64 can significantly improve the result. The ONNX standard supports a float64 tensor type for such needs. In ONNX Runtime, however, emphasis has been on FP32 performance, and some pipelines default to float even if the original model was double. If a model’s prediction computation involves operations that are numerically sensitive (like matrix inversion, Cholesky decomposition, or summing extremely large counts), then FP64 may be required to avoid noticeable discrepancies. A good practice is to test a model in both float and double if you suspect precision issues; significant differences indicate an ill-conditioned calculation that might need higher precision. &amp;lt;ref name=&amp;quot;ref1&amp;quot;/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
In summary, FP32 remains the &#039;&#039;&#039;default&#039;&#039;&#039; format for most training and inference due to its balance of accuracy and efficiency, while FP64 is reserved for niche cases or precision-critical subroutines. FP32 typically achieves the same accuracy as FP64 for end-to-end deep learning tasks, because network training can often compensate for small noise, and other sources of error (like generalization error) dominate. Therefore, the community trend has been moving to &#039;&#039;&#039;lower&#039;&#039;&#039; precisions, not higher, to gain speed — provided the models still converge and perform well. &amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 4.2. FP16 and BFloat16 in Deep Learning ===&lt;br /&gt;
&lt;br /&gt;
To further speed up neural network training and inference, &#039;&#039;&#039;half-precision (FP16)&#039;&#039;&#039; was adopted, especially with the advent of GPUs supporting FP16 math at twice the rate of FP32. Using FP16 reduces memory storage and bandwidth by 50% compared to FP32, which is significant for large models. However, FP16’s small exponent range and precision posed challenges: training a large network purely in FP16 can fail due to gradient underflow/overflow or weight updates not registering (because changes are below the 0.001 quantization step). To address this, frameworks introduced &#039;&#039;&#039;mixed-precision training&#039;&#039;&#039;, where compute-intensive parts (matrix multiplies, convolutions) run in FP16, but certain accumulations and model parameters remain in FP32. For instance, NVIDIA’s &#039;&#039;&#039;Tensor Core&#039;&#039;&#039; units require FP16 (or BF16) inputs but accumulate results in FP32, effectively ensuring that reductions are done in higher precision. Research showed that with proper loss scaling and mixed precision, models can train to FP32-level accuracy using mostly FP16 arithmetic, achieving significant speedups. &amp;lt;ref name=&amp;quot;ref6&amp;quot;/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;BFloat16 (BF16)&#039;&#039;&#039; emerged as an alternative 16-bit format primarily from Google’s TPU training systems. BF16 has a larger exponent (8 bits, same as FP32) but only 7 fraction bits. This means BF16 can handle the &#039;&#039;&#039;same dynamic range&#039;&#039;&#039; as FP32 (no overflows where FP32 wouldn’t) but at the cost of even lower precision than FP16 (BF16 has about 2–3 decimal digits of precision vs FP16’s ~3–4). Despite its coarse precision, BF16 turns out to be effective for training deep networks because the algorithms (stochastic gradient descent, etc.) are somewhat robust to that noise, and having the wide range prevents catastrophic loss of signal (no gradient will overflow to Inf unless it was going to in FP32 anyway). Many training frameworks (PyTorch, TensorFlow) support BF16 training on hardware like &#039;&#039;&#039;TPU, NVIDIA A100+, Intel CPUs with AVX-512 BF16, etc&#039;&#039;&#039;. Typically, when using BF16, &#039;&#039;&#039;accumulations are still done in FP32&#039;&#039;&#039; (for example, a TPU v2/v3 multiplies BF16 matrices and accumulates into an FP32 result for the dot product). &amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref6&amp;quot;/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
In inference, both FP16 and BF16 are used to deploy models for faster speed or lower memory. FP16 is popular on NVIDIA GPUs (which have specialized FP16 units). BF16 is advantageous on TPUs and some CPUs that support it, and increasingly on GPUs (Ampere and newer GPUs also support BF16 arithmetic). The choice between FP16 and BF16 can depend on hardware: BF16’s advantage is easier software conversion (you can often take an FP32-trained model and cast weights to BF16 with minimal loss, since the dynamic range is intact), whereas FP16 might require adjusting the model or ensuring values fit in range (sometimes needing techniques like maintaining a master copy of weights in FP32 during training, or handling out-of-range values).&lt;br /&gt;
&lt;br /&gt;
For ONNX, both Float16 and BFloat16 types are part of the standard. ONNX models can be converted to FP16 (truncating weights and inserting cast nodes for computations) using tools like &amp;lt;code&amp;gt;float16_converter&amp;lt;/code&amp;gt; in ONNX Runtime. ONNX Runtime on GPUs will typically execute those FP16 ops with mixed precision on tensor cores (FP16 input, FP32 accumulate), whereas on CPU it may emulate FP16 or upcast to FP32 (as many CPU implementations do not natively support half-precision math). In fact, as an example, running an ONNX model in FP16 on a CPU might insert hidden Cast operations to FP32 behind the scenes, because the CPU kernel for that op only exists in FP32. This ensures correctness (the computation is done in higher precision) but can reduce performance. We will revisit such backend-dependent behavior in Section 5.3. &amp;lt;ref name=&amp;quot;ref8&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
In summary, &#039;&#039;&#039;FP16 and BF16&#039;&#039;&#039; are now established as important low-precision formats in deep learning. FP16 offers more precision bits, which can matter for e.g. small differences, but has a limited range (care needed to avoid overflow). BF16 offers safety in range but less precision (which can slightly affect convergence or final accuracy in some cases). Both typically rely on FP32 for accumulation to achieve acceptable accuracy. When deploying models, one should choose a format supported efficiently by the target hardware and verify that the accuracy is not significantly degraded. Often, CNNs and transformers can run in FP16/BF16 with virtually no drop in score, but certain operations (like softmax, see Section 6.3) might need special handling.&lt;br /&gt;
&lt;br /&gt;
=== 4.3. FP8 Formats (E4M3, E5M2) and Scaling ===&lt;br /&gt;
&lt;br /&gt;
The push for ever lower precision has led to experimental &#039;&#039;&#039;8-bit floating point (FP8)&#039;&#039;&#039; formats for deep learning. In 2022, researchers from NVIDIA, Intel, and Arm proposed an FP8 standard with two variants: &#039;&#039;&#039;E4M3&#039;&#039;&#039; (4 exponent bits, 3 fraction bits) and &#039;&#039;&#039;E5M2&#039;&#039;&#039; (5 exponent bits, 2 fraction bits). Both have 1 sign bit, for a total of 8 bits. E5M2 has a wider exponent range (5 bits gives bias 15) but one fewer mantissa bit than E4M3. The idea is to use E4M3 for numbers that need more precision (like weights or activations around 1.0) and E5M2 for numbers that need more dynamic range (like gradient updates which can be very small or large). In fact, the study found that using E4M3 for forward activations/weights and E5M2 for backward gradients can allow training of large networks with FP8 to accuracy on par with FP16 training. &amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref11&amp;quot;/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
FP8 formats are &#039;&#039;not&#039;&#039; IEEE 754 standard (though IEEE is considering lower precision in its 2019 revision). Notably, the E4M3 format as used by NVIDIA does not allocate any bit-pattern for &#039;&#039;+∞&#039;&#039; or &#039;&#039;-∞&#039;&#039; – it has only NaN for invalid results, using that freed bit pattern to extend the representable range by one exponent value. Essentially, E4M3 can represent numbers up to ~&#039;&#039;2&amp;lt;sup&amp;gt;15&amp;lt;/sup&amp;gt;&#039;&#039; (around 3e4) instead of capping at infinity at &#039;&#039;2&amp;lt;sup&amp;gt;16&amp;lt;/sup&amp;gt;&#039;&#039;, by foregoing infinities. Variations also exist regarding negative zero and how NaNs are handled. For instance, GraphCore’s IPU uses FP8 formats that disallow negative zero and infinities, to maximize useable range. &amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Using 8-bit floats for neural networks requires additional techniques, chiefly &#039;&#039;&#039;scaling&#039;&#039;&#039;. Because 8-bit has very limited precision and range, one typically applies a power-of-two scale factor to groups of values to normalize them into an FP8-friendly range. This is akin to block floating-point or “microscaling” per layer or per tensor. For example, one might keep a FP32 scale for each channel or tensor, and represent values as &#039;&#039;(FP8_value) × 2&amp;lt;sup&amp;gt;scale&amp;lt;/sup&amp;gt;&#039;&#039;. The ONNX standard has introduced support for FP8 in version 1.15, including types for E4M3 and E5M2 (with variations FNUZ: finite-only and no negative zero), and even a type called &#039;&#039;&#039;E8M0&#039;&#039;&#039; which is an 8-bit exponent-only number used purely as a scale factor. The E8M0 type has 8 exponent bits, no mantissa, effectively representing powers of two from &#039;&#039;2&amp;lt;sup&amp;gt;-127&amp;lt;/sup&amp;gt;&#039;&#039; to &#039;&#039;2&amp;lt;sup&amp;gt;128&amp;lt;/sup&amp;gt;&#039;&#039; for scaling purposes. This indicates an approach where an FP8 tensor would be accompanied by an E8M0 scale (or scales) to interpret its value correctly. &amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Rounding&#039;&#039;&#039; becomes critical when casting values to FP8. The quantization error is much larger relative to FP16 or FP32. Studies have shown that choosing the right rounding mode (e.g., round-to-nearest, stochastic rounding, etc.) and clamping behavior (saturating to max instead of inf) can affect model accuracy. ONNX added a &amp;lt;code&amp;gt;round_mode&amp;lt;/code&amp;gt; attribute to its Cast operator to allow controlling this for FP8. In particular, one paper found that &#039;&#039;&#039;rounding up with saturation&#039;&#039;&#039; (round-to-nearest ties away from 0, and clamp overflows to max finite rather than inf) gave better accuracy in large language model training than other rounding modes. &amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
At present, FP8 is on the cutting edge. NVIDIA’s H100 GPU supports FP8 tensor cores (for both E4M3 and E5M2), and early results show minimal accuracy loss on some vision and NLP tasks when properly tuned. However, using FP8 requires fine-tuning hyperparameters like scaling factors for each layer and perhaps tweaking training procedures. ONNX’s inclusion of FP8 types means you can represent and transport models using FP8 weights or activations, but actual hardware support is limited and typically paired with calibration or dynamic scaling logic outside of ONNX graph (or using custom ONNX functions for scaling). It remains an &#039;&#039;&#039;open research area&#039;&#039;&#039; how to fully automate FP8 quantization for arbitrary models without accuracy loss.&lt;br /&gt;
&lt;br /&gt;
In summary, FP8 formats promise further compression of neural network computation and data. They are an order of magnitude more error-prone than FP16, but when combined with careful scaling, mixed usage of E4M3/E5M2, and maybe modified training techniques, they have shown surprising success. ONNX is actively evolving to support these ultra-low precision types, signaling their potential importance in future AI inference deployments.&lt;br /&gt;
&lt;br /&gt;
== Accumulation Semantics in ONNX ==&lt;br /&gt;
&lt;br /&gt;
One of the roles of the ONNX specification is to define the mathematical behavior of operators (Conv, MatMul, Sum, etc.) in a framework-agnostic way. However, ONNX mostly defines &#039;&#039;what&#039;&#039; an operator computes in exact arithmetic, not &#039;&#039;how&#039;&#039; the computation is carried out in finite precision. This leaves certain details – like accumulator precision or summation order – up to the implementation (runtime or hardware). We&#039;ll examine how ONNX treats accumulation and what guarantees (or lack thereof) are provided.&lt;br /&gt;
&lt;br /&gt;
=== 5.1. Operator Definitions and Numerical Guarantees ===&lt;br /&gt;
&lt;br /&gt;
Each ONNX operator is described as performing an ideal mathematical function on its inputs. For example, &amp;lt;code&amp;gt;MatMul&amp;lt;/code&amp;gt; is defined as the standard matrix product of two tensors, and &amp;lt;code&amp;gt;ReduceSum&amp;lt;/code&amp;gt; computes a summation along given axes. The ONNX spec, as of current versions, &#039;&#039;&#039;does not mandate the precision of intermediate calculations&#039;&#039;&#039; beyond the types of the inputs and outputs. If an operator’s inputs and outputs are float16, the spec implies the computation is done in float16 in a sense, but it does not forbid an implementation from using higher precision internally – it simply doesn&#039;t mention it. There is &#039;&#039;&#039;no explicit guarantee of identical bit-for-bit results&#039;&#039;&#039; across different ONNX backends for floating-point ops. Instead, results are expected to be numerically close, within tolerances one would normally expect from floating-point differences (due to different hardware or algorithms). The ONNX Backend Test suite typically uses a relative or absolute error tolerance when validating outputs for this reason.&lt;br /&gt;
&lt;br /&gt;
Because of this, important details like the use of extended precision or accumulation in higher precision are left ambiguous. This has led to discussions in the ONNX community about making such behavior more explicit. For example, a proposal was raised to allow specification of accumulation precision for certain ops like MatMul. The reasoning is that as lower-precision types (float16, bfloat16, float8) become more common, knowing or controlling whether an op internally accumulates in a higher precision is important for both accuracy and reproducibility. In June 2025, an issue on the ONNX GitHub suggested adding an attribute to MatMul (and similarly Gemm, Softmax, etc.) to specify the accumulator type. By default it would use input precision (status quo), but an attribute could request (for instance) 32-bit accumulation even if inputs are 16-bit. As of this writing, however, no such attribute exists in the official spec – it&#039;s an open issue. Thus, the numerical semantics are effectively: &#039;&#039;“compute the result as if in the same type as inputs, but actual implementations may use more precision internally as an optimization.”&#039;&#039; &amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
From a guarantee standpoint, ONNX operators promise the mathematically correct result assuming real-number arithmetic, but they &#039;&#039;&#039;do not guarantee bit-exact results&#039;&#039;&#039; due to floating-point effects. There is an understanding that differences can arise from different summation orders, fused operations (e.g., an implementation might use an FMA (fused multiply-add) which has one rounding instead of two), or use of extended precision registers. ONNX does not formally define rounding modes either – it relies on IEEE 754 defaults, but if a backend used something like deterministic summation or alternative rounding, ONNX has no mechanism to express that.&lt;br /&gt;
&lt;br /&gt;
In summary, ONNX&#039;s current stance is largely &#039;&#039;&#039;implementation-dependent&#039;&#039;&#039; for numerical precision. It provides minimal explicit numerical guarantees beyond type constraints. This is why an ONNX model run on two different hardware or runtime libraries can have slightly different results, especially in reduced precision. The burden is on backends to produce results &amp;quot;close enough&amp;quot; to each other that they don&#039;t affect the model&#039;s decisions in any significant way.&lt;br /&gt;
&lt;br /&gt;
=== 5.2. MatMul, Conv, and Reduction Operators ===&lt;br /&gt;
&lt;br /&gt;
Consider ONNX &amp;lt;code&amp;gt;MatMul&amp;lt;/code&amp;gt;: it takes two tensors (often two matrices) and produces their matrix product. In pure math, &amp;lt;math&amp;gt;C_{ij} = \sum_k A_{ik} &#039;&#039; B_{kj}&amp;lt;/math&amp;gt;. If A and B are float16, should that sum be computed in float16 or float32? As discussed, ONNX doesn’t specify – it simply says the output is a float16 matrix containing the product. In practice, many backends will &#039;&#039;promote&#039;&#039; the computation: for example, NVIDIA GPUs will do the multiplication and addition in a mixed FP16/FP32 mode by default (because of tensor cores), then convert the final result back to float16. ONNX Runtime on CPU might not support float16 multiplication natively, so it will insert cast ops to float32, perform the MatMul in float32, then cast back to float16. From ONNX’s perspective, as long as the final result is within acceptable error of the “ideal” float16 computation, it’s fine. But note, an “ideal” float16 computation (where every multiply and add is rounded to FP16) might actually be &#039;&#039;less accurate* than what the backend did with FP32. This means that a backend using higher precision can produce a result that is not bit-equal to a pure FP16 implementation – it&#039;s actually closer to the true real number result. This is usually considered a good thing (improved accuracy), but it does highlight a lack of consistency: another backend might do everything in pure FP16 and get a slightly different answer. &amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The same situation occurs for &#039;&#039;&#039;convolution&#039;&#039;&#039; (&amp;lt;code&amp;gt;Conv&amp;lt;/code&amp;gt; op) which is essentially a batched sum of products (like MatMul in sliding window), and for explicit reduction ops like &amp;lt;code&amp;gt;ReduceSum&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;ReduceMean&amp;lt;/code&amp;gt;, etc. If you have an ONNX &amp;lt;code&amp;gt;ReduceSum&amp;lt;/code&amp;gt; that sums 1000 float16 values, the spec doesn’t say how to sum them. A GPU kernel might sum in a tree using FP32 accumulators; a naive implementation might sum in order with FP16 and possibly overflow or underflow intermediate steps. Both are allowed as long as they output a float16 result that is “reasonable.” In practice, ONNX Runtime and others take care to avoid obviously bad behavior (like overflow) by using higher precision or by splitting the sum. But again, it’s not mandated by the ONNX standard explicitly.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;\mathrm{Softmax}(x)_i = \frac{\exp(x_i)}{\sum_j \exp(x_j)},&amp;lt;/math&amp;gt;&lt;br /&gt;
typically applied along a specified axis. Directly computing this in float16 can be problematic for large vectors, as discussed in Section 6.3. Indeed, the issue of accumulation precision arises: should the sum &amp;lt;math&amp;gt;\sum_j \exp(x_j)&amp;lt;/math&amp;gt; be done in FP16 or can it be in FP32? Many implementations will do it in FP32 even if &#039;&#039;x&#039;&#039; is FP16, because the dynamic range of &amp;lt;math&amp;gt;\exp(x)&amp;lt;/math&amp;gt; is large. The ONNX Softmax spec doesn’t detail this, but it expects the result to be a proper probability distribution (summing to 1.0 within normal floating error). A pure FP16 softmax on, say, 1000 elements could sum to something like 0.996 or 1.004 due to rounding, whereas FP32 accumulation would be much closer to 1.000. ONNX likely allows both; the small difference is usually inconsequential, but if it were large (say the FP16 sum overflowed to inf), that would be a correctness issue. A robust backend will prevent that (by using the common max-subtraction trick and higher precision accumulation). The ONNX issue 7072 indeed calls out Softmax as another op where explicit control of accumulation precision would be useful. &amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
In summary, &#039;&#039;&#039;MatMul, Conv, and reduction ops&#039;&#039;&#039; in ONNX are defined mathematically but with &#039;&#039;implicit&#039;&#039; numerical behavior. Implementations commonly use higher precision for intermediate sums especially when input is low precision, but this is not standardized. Consequently, results can differ slightly between implementations. For critical applications, one must be aware of these differences. A model exported to ONNX with float16 weights might perform slightly differently on one runtime vs another if one does all float16 math and the other uses float32 accumulations. Both conform to ONNX. As a user, if you require a certain precision for accumulation, currently you have to ensure it by the way you export or run the model (for example, insert manual cast nodes in the ONNX graph to force certain ops to FP32). We may see future ONNX versions address this with attributes or metadata as mentioned, but for now these ops are effectively “compute with best effort accuracy”.&lt;br /&gt;
&lt;br /&gt;
=== 5.3. Backend and Hardware-Dependent Behavior ===&lt;br /&gt;
&lt;br /&gt;
Because ONNX is implemented by many backends (ONNX Runtime, TensorRT, CoreML, PyTorch JIT, etc.), and these in turn run on varied hardware (CPU, GPU, accelerators), the numeric behavior can be backend-dependent. Some examples:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;CPU backends:&#039;&#039;&#039; Many CPUs do not have native float16 vector arithmetic (with a few exceptions in recent CPUs). ONNX Runtime’s default CPU execution provider will often upcast float16 to float32, compute, then downcast. This ensures accuracy but means that if you thought you were getting a full float16 pipeline, you’re not – the CPU is essentially ignoring the low precision in compute. On the other hand, x86 CPUs have extended 80-bit precision in x87 registers (though less used now) and 64-bit SIMD registers for doubles; some compiler optimizations might carry intermediate results in higher precision. This could make CPU results slightly different from a GPU’s if, say, a sum is done in 80-bit on CPU versus 32-bit on GPU. Generally, modern compilers avoid such extended precision unless explicitly asked, to improve reproducibility. &amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;GPU backends:&#039;&#039;&#039; GPUs often perform summations in parallel using tree reductions. The order of operations can change depending on how threads are scheduled. This means even on the same GPU, summing an array in one launch versus another might not give bit-identical results if the internal parallel reduction algorithm differs (though it should be deterministically the same given the same library version and input, but if you change GPU model or BLAS library, it might differ). Also, GPUs have fused multiply-add (FMA) instructions that do &#039;&#039;(a*b + c)&#039;&#039; in one step with one rounding, which can produce slightly different results than doing the multiply and add separately (which would round twice). If an ONNX backend uses a fused kernel (like a GEMM library) it might yield different low-order bits than a simple loop.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Vendor libraries:&#039;&#039;&#039; ONNX Runtime can use vendor BLAS or DNN libraries (MKL, cuDNN, TensorRT, etc.). These libraries often implement their own strategies for numerical stability. For example, NVIDIA’s cuDNN might use FP32 accumulation for FP16 inputs by default. TensorRT (NVIDIA’s inference engine) will sometimes automatically promote precisions or insert scaling to maintain stability. On the other hand, if you explicitly request FP16 in TensorRT with no FP32 fallback, it might do the entire op in FP16 and you’d see more error. Some libraries offer knobs: TensorRT allows choosing FP32, FP16, or INT8 precision for each layer, and will use FP32 internally for safety in some cases unless forced otherwise. &amp;lt;ref name=&amp;quot;ref13&amp;quot;/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Hardware-specific quirks:&#039;&#039;&#039; As another example, NVIDIA’s &#039;&#039;&#039;TensorFloat-32 (TF32)&#039;&#039;&#039; on Ampere GPUs is a precision mode where matrix multiplications use 10-bit mantissa (so about FP16 precision for the multiply) but keep 8-bit exponent (range like FP32). By default, CUDA libraries use TF32 for FP32 matrix multiplies on Ampere to speed them up, unless you disable it. This means if you run an ONNX model with FP32 on an A100 GPU, your matmuls might actually be lower precision than FP32 (though higher than FP16). It’s an example of hardware choosing an intermediate precision for performance. From ONNX’s perspective, the output is still FP32 and (hopefully) within error tolerances, but a user might be surprised that it’s not a “true” FP32 computation internally. If exactness is needed, one has to turn off TF32. &amp;lt;ref name=&amp;quot;ref6&amp;quot;/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Non-determinism:&#039;&#039;&#039; Some backends (especially on GPUs) allow non-deterministic execution for performance. For instance, atomic adds in parallel reductions can produce results that vary run-to-run because the order of accumulation is race-condition-dependent. ONNX’s spec doesn’t cover this explicitly, but it’s something to consider. In ONNX Runtime, there are settings for deterministic computing, but by default it may use all available performance optimizations, even if results vary at the 1e-7 level.&lt;br /&gt;
&lt;br /&gt;
In short, the numerical behavior under ONNX can depend on the combination of backend software and hardware capabilities. The lack of a strict spec for accumulation means implementations have freedom to use clever strategies (higher precision, reordering) to improve accuracy or speed. Usually this is positive (e.g. getting better accuracy than a naive approach). But it could also lead to slight inconsistencies. For example, you might find that exporting a PyTorch model to ONNX and running it with one engine yields outputs slightly different from PyTorch’s – not because ONNX is wrong, but because the summation or convolution was done differently (maybe PyTorch summed in FP32 as well, but maybe not in exactly the same way).&lt;br /&gt;
&lt;br /&gt;
The key takeaway is that ONNX &#039;&#039;by design&#039;&#039; abstracts away these details, which is good for portability but means you need to &#039;&#039;&#039;trust the backend&#039;&#039;&#039; or verify its precision behavior for sensitive models. If necessary, you can enforce certain behavior by inserting ops (for example, if you want to ensure FP32 accumulation, you could insert a cast to FP32 for inputs of a MatMul and cast the result back to FP16 – this way the ONNX graph itself mandates FP32 compute in between). This of course sacrifices some performance or device-specific magic.&lt;br /&gt;
&lt;br /&gt;
Looking forward, as mentioned, there are proposals to enrich ONNX’s specification regarding precision. This might include explicit flags for “exact summation” or “accumulate in int32” etc., which would make models more self-descriptive but also potentially less portable (if a backend doesn’t support that, what then?). For now, users should be aware of these backend differences. Most of the time, well-engineered ONNX runtimes yield results close enough that they don&#039;t affect the application (e.g., classification top-1 remains the same even if low-level sums differ by 1e-5). But for high-stakes numerical computations, testing and maybe adjusting the model or backend settings is prudent.&lt;br /&gt;
&lt;br /&gt;
== Numerical Stability in Deep Learning Models ==&lt;br /&gt;
&lt;br /&gt;
Deep learning computations involve many operations that, while theoretically well-defined, can be sensitive to numerical issues when implemented in finite precision. We&#039;ll discuss a few common scenarios in neural networks where numerical stability is a concern, and how precision and accumulation play a role.&lt;br /&gt;
&lt;br /&gt;
=== 6.1. Dot Products and Large-Scale Reductions ===&lt;br /&gt;
&lt;br /&gt;
Neural networks are full of dot products: every fully-connected layer computes &amp;lt;math&amp;gt;\mathbf{y} = W\mathbf{x}&amp;lt;/math&amp;gt;, which is a series of inner products between weight vectors and the input. Convolutional layers compute weighted sums of local regions. Attention mechanisms compute large matrix products of query/key/value matrices. All these boil down to summing a lot of terms: &amp;lt;math&amp;gt;\sum_{k=1}^K a_k b_k&amp;lt;/math&amp;gt;. When &#039;&#039;K&#039;&#039; is large (it could be in the thousands or more for a single neuron in a fully connected layer of a large model), the potential for floating-point error in that sum increases.&lt;br /&gt;
&lt;br /&gt;
If using FP32, the error may still be negligible in many cases, but in lower precision like FP16, it can become significant. For example, consider a dot product of length 2048 in FP16. Each product is in FP16 (with ~3 decimal digits precision) and if accumulated in FP16, the worst-case error could be on the order of &#039;&#039;2048 × ε_FP16 ≈ 2048 × 0.0009766 ≈ 2&#039;&#039; (i.e., you could lose a couple units in the third decimal place relative accuracy in worst case). Even in random error accumulation, you might see error on the order of &amp;lt;math&amp;gt;\sqrt{2048} \times 0.0009766 \approx 0.044&amp;lt;/math&amp;gt; or 4.4% (though this is a rough estimate). That’s quite significant. Fortunately, as discussed, most hardware will accumulate such sums in a higher precision like FP32, dramatically reducing that error (~7 orders of magnitude smaller &#039;&#039;ε&#039;&#039;). But if it didn’t, the network’s outputs could get noisy. A fully-connected layer with many inputs might output slightly different results depending on summation order if done in low precision.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Large-scale reductions&#039;&#039;&#039; also occur in operations like global average pooling (summing all elements of a feature map) or computing the total loss by summing loss over batch samples. If one sums 100,000 values in FP32, the worst-case error could be on the order of 100,000 &#039;&#039; 1e-7 ≈ 1e-2 (1% uncertainty), though typical error would be much less. In FP16, summing 100k values is likely impossible without overflow: as noted, adding ~10^4 moderate numbers can overflow FP16. So large reductions &#039;&#039;must* be handled carefully (either in chunks or in higher precision). Frameworks generally do: for example, when performing a sum reduction on GPU, libraries break it into parallel blocks that accumulate in registers (usually 32-bit registers, even if inputs are 16-bit). &amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Another aspect is that many dot products in neural nets involve values of varying magnitude. If the distribution of terms is such that some are much larger than others, the smaller terms might not contribute much if summed last. Ideally, one might sort by magnitude or use pairwise summation to improve that. Most BLAS libraries effectively do a pairwise strategy due to their parallel structure. So, while worst-case analyses are grim, practical implementations mitigate a lot of it.&lt;br /&gt;
&lt;br /&gt;
However, certain layers can suffer if not handled properly. One famous example is in RNNs or LSTMs: if one uses FP16 naive summation for the gating mechanism, errors could accumulate over many time steps and cause drift or instability. In modern practice, recurrent layers are often run in FP16 with FP32 accumulators to avoid this.&lt;br /&gt;
&lt;br /&gt;
In summary, &#039;&#039;&#039;dot products and large reductions need sufficient precision&#039;&#039;&#039; or robust algorithms. If you are implementing a custom kernel (say a custom ONNX op) and you have to sum a huge array, you should consider using a technique from Section 3 (pairwise summation, Kahan, or at least double accumulator if possible). Many deep learning computations, by virtue of hardware and library design, already do something like this under the hood. But it&#039;s always good to be aware, especially if pushing into lower precision territories.&lt;br /&gt;
&lt;br /&gt;
=== 6.2. Cancellation, Dynamic Range, and Conditioning ===&lt;br /&gt;
&lt;br /&gt;
We introduced &#039;&#039;cancellation&#039;&#039; earlier in the context of summation. In deep learning, cancellation can appear in a few places. One is in the computation of certain norms or variances. For instance, Batch Normalization computes mean and variance of activations. The variance formula &amp;lt;math&amp;gt;\mathrm{Var}(x) = E[x^2] - (E[x])^2&amp;lt;/math&amp;gt; is subject to catastrophic cancellation if &#039;&#039;E[x]&#039;&#039; and &amp;lt;math&amp;gt;\sqrt{E[x^2]}&amp;lt;/math&amp;gt; are close in magnitude (which can happen if the variance is small compared to the mean). If implemented naively in single precision, you might get a negative variance due to numerical error (which would then be clamped to zero or sqrt would yield NaN). Robust implementations compute variance in a single pass or increase precision. Indeed, in many frameworks, batchnorm accumulation is done in FP32 even if the activations are FP16, precisely to avoid numerical issues in computing mean and variance. ONNX itself doesn’t specify this, but an ONNX backend is likely to do batchnorm stats in higher precision for safety. The &#039;&#039;conditioning&#039;&#039; of the variance calculation is poor when variance is tiny relative to mean, so it&#039;s a spot where higher precision is needed.&lt;br /&gt;
&lt;br /&gt;
Another example is in certain loss functions or regularizations. If you subtract two large numbers to get a small difference, you lose precision. Say you have two nearly identical predictions and you subtract them as part of some operation – if done in float16, you might get zero whereas float32 would have a slight difference. This could affect, say, a gradient if that difference was supposed to propagate information. Fortunately, neural nets often avoid directly subtracting large nearly equal numbers; but it can happen. In attention mechanisms, the &#039;&#039;score normalization&#039;&#039; (softmax) subtracts a max to avoid huge exponentials, which is good for range but if multiple scores are close to the max, you subtract numbers that are close. However, those subtractions are not catastrophic in the same way; they&#039;re intentional to reduce range and are typically fine in FP32 or even FP16.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Dynamic range&#039;&#039;&#039; issues are everywhere in deep learning. Activation functions like ReLU and exponential can produce very large or very small values. If subsequent computations don’t account for that, you get infinities or zeros. For example, consider a network output that produces a logit of 100 in FP16. &amp;lt;math&amp;gt;\exp(100)&amp;lt;/math&amp;gt; in FP16 overflows to infinity (since FP16 max ~ 6e4 and &#039;&#039;e&amp;lt;sup&amp;gt;100&amp;lt;/sup&amp;gt; ∼ 3.7e43&#039;&#039;). If you then do softmax, that one infinity will dominate the sum and you might get a probability of 1 for that class (or NaNs if not handled). The standard softmax implementation subtracts the max logit, so in this case it would subtract 100 from all logits, making the largest exponent &amp;lt;math&amp;gt;\exp(0)=1&amp;lt;/math&amp;gt; and others tiny – this avoids overflow. But what about underflow? If you had a very negative logit, &amp;lt;math&amp;gt;\exp(-100)&amp;lt;/math&amp;gt; in FP16 would underflow to 0. In softmax, that just contributes ~0 probability, which is fine (it effectively is zero anyway). Underflow tends to be less dangerous than overflow in inference — underflow usually just means “this probability/neuron is effectively 0,” whereas overflow can poison the results with NaNs or Infs. &lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Conditioning&#039;&#039;&#039; refers to how sensitive a function is to input perturbations. Neural networks themselves as functions can be poorly conditioned (e.g., tiny changes in weights cause big changes in output for some architectures), but typically they are designed to be reasonably well-behaved. Certain internal computations like matrix inversion (if one does a pseudoinverse or something for a layer) can be extremely ill-conditioned if the matrix is near singular. That again would need double precision perhaps. In ML, one common ill-conditioned operation is the &#039;&#039;&#039;softmax denominator&#039;&#039;&#039;: &amp;lt;math&amp;gt;\sum_j e^{x_j}&amp;lt;/math&amp;gt; can be very large and also very sensitive to changes in the largest &#039;&#039;x&amp;lt;sub&amp;gt;j&amp;lt;/sub&amp;gt;&#039;&#039;. However, the softmax is more stable if computed carefully (max trick). Another is &#039;&#039;&#039;normalization&#039;&#039;&#039;: computing &amp;lt;math&amp;gt;\frac{x - \mu}{\sigma}&amp;lt;/math&amp;gt; for layer norm or batch norm – if &amp;lt;math&amp;gt;\sigma&amp;lt;/math&amp;gt; is very small, then the division blows up noise. In those cases, a small &#039;&#039;ε&#039;&#039; is usually added to &amp;lt;math&amp;gt;\sigma^2&amp;lt;/math&amp;gt; to prevent instability (this is a model-level fix, not a numeric one per se, but it acknowledges numeric limits).&lt;br /&gt;
&lt;br /&gt;
In training, &#039;&#039;&#039;optimizers&#039;&#039;&#039; can suffer from precision issues. For example, the Adam optimizer keeps a moving average of squared gradients; in half precision, these small updates might flush to zero or not change at some point. Some research found that certain adaptive optimizers break in FP16 because the small differences are lost (&amp;quot;Why FP16 training breaks NAdam&amp;quot; etc.). The solution is often to keep the optimizer states in FP32 while doing the bulk of compute in FP16. This again is a form of mixed precision.&lt;br /&gt;
&lt;br /&gt;
From an inference perspective, once the model is trained, the main concerns are avoiding overflow in activations and ensuring summations (like in softmax or reductions) are done with enough precision. The model’s weights are fixed, so we don&#039;t have to worry about cumulative error over iterations as in training, just the forward pass.&lt;br /&gt;
&lt;br /&gt;
=== 6.3. Normalization Layers and Softmax ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Normalization layers&#039;&#039;&#039; (BatchNorm, LayerNorm, etc.) and &#039;&#039;&#039;Softmax&#039;&#039;&#039; are specifically worth examining for numerical stability:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Batch Normalization:&#039;&#039;&#039; During inference, batchnorm applies an affine transform using precomputed mean and variance (from training). It’s basically: &amp;lt;math&amp;gt;y = \frac{x - \mu}{\sqrt{\sigma^2 + \epsilon}} \gamma + \beta&amp;lt;/math&amp;gt;. This is a stable operation as long as &#039;&#039;ε&#039;&#039; is provided (to avoid division by zero). Since &amp;lt;math&amp;gt;\mu, \sigma^2&amp;lt;/math&amp;gt; are constants in inference, it reduces to linear ops which are fine. The main numerical work (computing mean/variance) happened during training or model calibration, and frameworks usually did that in FP32. So batchnorm at inference time is not a source of instability (it’s just a scale and shift of the data).&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Layer Normalization / Group Normalization:&#039;&#039;&#039; These compute mean/var on the fly for each sample (and possibly channel group). In inference, they still need to compute these statistics for each forward pass. If implemented in low precision, the variance calculation can suffer cancellation as mentioned. However, it&#039;s likely that inference engines compute the mean and variance in FP32 even if inputs are FP16 (especially since these are per-sample operations, not huge batch reductions – it&#039;s feasible to do in higher precision). If someone implemented layer norm purely in FP16, they might see issues when the variance is very small. Usually there’s an &#039;&#039;ε&#039;&#039; added which might dominate a tiny variance and thus mitigate catastrophic cancellation (the result might just output something mostly from &#039;&#039;ε&#039;&#039;). In summary, normalization layers are usually fine as long as the implementation uses enough precision for the summation of squares.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Softmax:&#039;&#039;&#039; Softmax is notorious if done improperly. The safe algorithm: subtract the maximum input from all inputs (to bring the largest exponent to 1.0), exponentiate, sum them up, divide each exponent by the sum. The max subtraction handles overflow in &amp;lt;math&amp;gt;\exp()&amp;lt;/math&amp;gt;. But what about the sum? As discussed, if there are &#039;&#039;N&#039;&#039; elements in the softmax, the sum is at most &#039;&#039;N&#039;&#039; (when all inputs equal the max). In classification, &#039;&#039;N&#039;&#039; might be the number of classes. For ImageNet, &#039;&#039;N=1000&#039;&#039;, so worst sum ~1000 (which is fine in FP16, as 1000 &amp;lt;&amp;lt; 65504). For an extreme case like a language model, &#039;&#039;N&#039;&#039; could be 50,000 (vocab size). Sum could be 50,000 in worst case (everyone equal probability). 50,000 is still below 65504, so actually an FP16 sum would &#039;&#039;not&#039;&#039; overflow in that scenario, and the rounding error on a sum of 50k identical terms would be maybe a few ULPs (which at 50k magnitude might be ~0.5 difference worst-case). So maybe softmax summation in FP16 is not catastrophic for output probabilities – you’d get essentially 1.0 after normalization instead of 0.99999 in some case. Usually fine.&lt;br /&gt;
&lt;br /&gt;
:The bigger issue is if one or a few terms dominate the softmax. If one term is much larger, the others underflow to 0 after the max subtraction. That’s fine: you get a one-hot distribution essentially. If the model expected a softer distribution, that could be a problem. But if the logits were really that far apart, even FP32 would give nearly one-hot. So not really a precision issue, more of a model property.&lt;br /&gt;
&lt;br /&gt;
:However, the &#039;&#039;&#039;stability during training&#039;&#039;&#039; is a big issue. The GitHub thread we saw (ColossalAI) mentioned training a 130B model where FP16 softmax in attention caused training to occasionally blow up&amp;lt;ref name=&amp;quot;ref14&amp;quot;/&amp;gt;. They observed attention scores in some heads ranging up to 1e4 or as low as 1e-3&amp;lt;ref name=&amp;quot;ref14&amp;quot;/&amp;gt;. 1e4 exponent in FP16 is definitely Inf (because &amp;lt;math&amp;gt;\exp(10^4)&amp;lt;/math&amp;gt; is astronomically huge, but even exp(100) is inf in half). The method CogView introduced (Precision Bottleneck Relaxation) involved subtracting the max &#039;&#039;per head&#039;&#039; to reduce range&amp;lt;ref name=&amp;quot;ref14&amp;quot;/&amp;gt;—which is basically the softmax max trick at a finer granularity or something similar. In the end, they found &#039;&#039;&#039;the simplest fix was to compute softmax in FP32&#039;&#039;&#039; for those layers&amp;lt;ref name=&amp;quot;ref14&amp;quot;/&amp;gt;. They reported that it had negligible speed impact but significantly improved stability&amp;lt;ref name=&amp;quot;ref14&amp;quot;/&amp;gt;. This highlights a general principle: certain operations like softmax (and sometimes matrix multiplications in attention) benefit from higher precision, and using FP32 in a few critical spots can save a mixed-precision training from crashing. In inference, using FP32 for softmax is also a safeguard if one is uncertain. It costs little because softmax is not a heavy compute compared to matrix multiplies.&lt;br /&gt;
&lt;br /&gt;
:ONNX does have a Softmax operator; if the input is float16, a backend might do exactly what they described: internally cast to float32 for the softmax computation, then cast output back to float16. Some inference engines (like DeepSpeed’s inference or others) keep attention softmax in FP32 even if rest is FP16, for robustness&amp;lt;ref name=&amp;quot;ref14&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Apart from softmax, other places where exponentials occur are in loss functions like cross-entropy (which uses log-softmax internally), or in activation functions like Sigmoid and Tanh (which are basically scaled logistic functions). Sigmoid(&#039;&#039;x)=1/(1+e&amp;lt;sup&amp;gt;-x&amp;lt;/sup&amp;gt;)&#039;&#039;. If &#039;&#039;x&#039;&#039; is moderately large positive, &#039;&#039;e&amp;lt;sup&amp;gt;-x&amp;lt;/sup&amp;gt;&#039;&#039; underflows to 0 in FP16 if &amp;lt;math&amp;gt;x&amp;gt;~11&amp;lt;/math&amp;gt;. That gives a sigmoid output of 1.0 exactly. In FP32, &#039;&#039;e&amp;lt;sup&amp;gt;-x&amp;lt;/sup&amp;gt;&#039;&#039; for &#039;&#039;x=11&#039;&#039; is about &#039;&#039;5e-5&#039;&#039;, not underflow, so sigmoid ~0.99995. So FP16 will saturate some outputs to exactly 0 or 1 sooner than FP32 would. This can slightly change behaviors (e.g., a neuron might go to full saturation a bit early). Usually, this isn’t a huge deal, but it could make gradients zero earlier. In inference, we don’t care about gradients, just the output value. Getting exactly 1.0 vs 0.99995 probably has no practical difference for a classification output. But one should be aware that &#039;&#039;&#039;activation functions can saturate faster&#039;&#039;&#039; in lower precision due to limited range or precision. ReLU has no issue (it’s linear or 0), but exponential-based activations do. In practice, again, frameworks often simply compute those in the given precision and accept the slight difference. If it were a problem, one could do those in FP32 as well.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Summary:&#039;&#039;&#039; Numerical stability in inference is maintained by using robust computation methods: subtracting maxima in softmax/LogSumExp, adding epsilon in denominators for normalization, and using higher precision for summations or critical steps. Most of these are built into the algorithms. ONNX models assume these standard practices (they don&#039;t need to explicitly do them in the graph because the backend inherently will for ops like Softmax). It&#039;s wise when building or exporting models to ONNX to test them in the target precision and ensure there are no surprises (like an output becomes NaN or Inf due to precision). Usually if training was done in mixed precision, the model will have been vetted in that environment already.&lt;br /&gt;
&lt;br /&gt;
== Performance vs. Accuracy Trade-offs ==&lt;br /&gt;
&lt;br /&gt;
There is an inherent trade-off between numerical precision and computational performance (speed, memory usage) in AI systems. Choosing lower precision can dramatically increase throughput and reduce memory, but it may introduce arithmetic errors. Engineers must balance these considerations:&lt;br /&gt;
&lt;br /&gt;
=== 7.1. Throughput, Memory Bandwidth, and Precision ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Throughput (speed)&#039;&#039;&#039; is often higher for lower-precision operations. Modern hardware can execute more low-precision ops in parallel than high-precision ops. For example, an NVIDIA V100 GPU can perform 2× more half-precision FLOPs than single-precision per clock, and newer architectures with Tensor Cores can do 8× more (since a Tensor Core warp operation might perform 64 FP16 FMAs per cycle vs 16 FP32). Similarly, int8 operations can be even faster and more energy efficient. Lower precision numbers also use less memory bandwidth – fetching 16-bit data is twice as fast as 32-bit data from memory (assuming the same bus width), which can relieve memory bottlenecks. Many deep learning workloads are memory-bound, so halving the data size can nearly double throughput if compute is not the limiting factor. This is a huge incentive to use lower precision.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Memory and storage&#039;&#039;&#039;: Using FP16 or int8 weights reduces model size, which is important for deploying large models to memory-limited environments (like edge devices or GPUs with limited VRAM). It also means you can fit larger batch sizes in memory, or more models on a chip, etc. There is also a cascade effect: smaller data means better cache utilization, which further improves performance.&lt;br /&gt;
&lt;br /&gt;
However, &#039;&#039;&#039;accuracy&#039;&#039;&#039; suffers if precision is too low. We have discussed how rounding error and limited range can cause deviations in outputs. The question is: do those deviations matter for the application? Often for neural networks, a small amount of noise or error in intermediate calculations does not change the final prediction. Neural nets are somewhat error-tolerant due to their redundant distributed representations. This is why quantization and FP16 techniques can work at all. The final model accuracy (e.g. top-1 accuracy on ImageNet) might drop only 0.5% or less when using FP16 instead of FP32, or int8 instead of FP16, if done properly. For many use-cases, this loss is acceptable given the speed gains.&lt;br /&gt;
&lt;br /&gt;
If one goes too low (like naive FP8 or int4 without special care), accuracy might drop severely – perhaps making the model unusable. Thus the trade-off: as precision decreases, performance increases but at some point accuracy degrades beyond an acceptable threshold. Engineers aim to find the lowest precision that still preserves accuracy.&lt;br /&gt;
&lt;br /&gt;
There is also the consideration of &#039;&#039;&#039;accumulation strategy&#039;&#039;&#039; affecting performance: for instance, Kahan summation (compensated) is more accurate but ~4× slower than naive summation. If you need that accuracy, you pay a performance cost. In many AI inference scenarios, the slight extra accuracy from something like Kahan isn&#039;t needed, so it’s not used. But in a scenario where you sum millions of elements for a final result that is thresholded, maybe you do need it.&lt;br /&gt;
&lt;br /&gt;
Another dimension is &#039;&#039;&#039;determinism vs speed&#039;&#039;&#039;. Enforcing a deterministic reduction order (for reproducibility) might require disabling some parallelism, which could slow things down. If exact repeatability is critical (like certain scientific or financial applications of neural nets), one might sacrifice some performance to ensure the summation happens the same way every time. ONNX allows both modes depending on the runtime.&lt;br /&gt;
&lt;br /&gt;
From a memory perspective, if precision is higher than needed, you waste memory. For example, storing intermediate activations in FP32 when they could be FP16 uses 2× memory, which might mean lower batch size or more memory traffic. So there&#039;s strong motivation to keep things as low-precision as possible &#039;&#039;except&#039;&#039; where accuracy truly demands.&lt;br /&gt;
&lt;br /&gt;
In summary, the trade-off often manifests as &#039;&#039;diminishing returns&#039;&#039;: FP64 vs FP32 – huge slowdown for usually negligible accuracy gain in ML; FP32 vs FP16 – moderate speedup for usually minor accuracy hit; FP16 vs int8 – another big speedup but maybe more accuracy loss unless calibration is done; int8 vs int4 – speed gain but often big accuracy drop that might not be worth it for current models.&lt;br /&gt;
&lt;br /&gt;
Engineers use techniques like &#039;&#039;&#039;mixed precision&#039;&#039;&#039; to target high precision only where needed. For example, run most of the model in FP16, but keep a few sensitive layers in FP32. This yields most of the performance benefits while ensuring critical parts are accurate. We already saw softmax as one example. Another might be final output layer: sometimes using FP32 for the logits and softmax can be safer.&lt;br /&gt;
&lt;br /&gt;
Hardware is also trending to support mixed modes – like FP8 Tensor Cores accumulate in FP32, etc. So hardware tries to give accuracy cheaply.&lt;br /&gt;
&lt;br /&gt;
=== 7.2. Hardware Accelerators and Tensor Cores ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Hardware accelerators&#039;&#039;&#039; (GPUs, TPUs, FPGAs, ASICs like GraphCore IPU, etc.) have specific support for certain precisions. Leveraging these can provide massive speedups:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;NVIDIA Tensor Cores:&#039;&#039;&#039; These started in Volta (V100) with support for 4x4 matrix multiply-adds on FP16 inputs with FP32 accumulation. Using Tensor Cores can give up to 8x throughput compared to using regular FP32 cores. But to use them, the data needs to be FP16 (or now BF16 or FP8 on newer GPUs) and aligned to certain dimensions (multiples of 8 etc. in matrix sizes, which frameworks handle by padding if needed). If an ONNX model is all FP32, by default it would not use Tensor Cores unless the runtime automatically converts some ops to mixed precision (which some do via a flag or environment variable enabling TF32 or FP16). The introduction of &#039;&#039;&#039;TensorFloat-32 (TF32)&#039;&#039;&#039; in Ampere GPUs is essentially a compromise: treat FP32 inputs as two 16-bit significand halves (10-bit mantissa actually) so that it can run on Tensor Cores, giving ~8x speed, while still outputting FP32. TF32 has about the precision of FP16 but range of FP32, and it was chosen because it typically doesn’t hurt convnet accuracy. ONNX Runtime or others on Ampere will use TF32 by default for conv and matmul unless disabled, giving a free speed boost at minimal accuracy cost. If exact FP32 is needed, users have to turn it off (sacrificing speed). &amp;lt;ref name=&amp;quot;ref6&amp;quot;/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;TPU (Google):&#039;&#039;&#039; TPUs have native support for BF16 matrix ops with FP32 accumulators. They were designed specifically for the mixed precision approach in training. For inference, TPUs also support int8 and other quantized ops (e.g., via Edge TPU for lower-power). If exporting ONNX to run on a TPU (via some bridge), one might get automatic BF16 usage. BF16 has the advantage of no loss in range, so TPUs can more straightforwardly run models in BF16 without modifications.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;FPGAs and ASICs:&#039;&#039;&#039; Some custom accelerators might use fixed-point or block floating-point arithmetic for efficiency. They often treat the network in quantized terms. The ONNX quantization specification (QuantizeLinear/DequantizeLinear ops) exists for representing quantized models (INT8 etc.), which is another domain beyond floating-point, but it overlaps as a precision issue. In those systems, accumulators for INT8 × INT8 multiplies are usually 32-bit (to avoid overflow for dot products up to certain lengths). The trade-off there is between integer vs float representation, which can give big speedups on CPUs/DSAs via SIMD instructions (many CPUs have vector int8 multiply-add instructions now for inference).&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Memory bandwidth on hardware:&#039;&#039;&#039; Some accelerators include special high-bandwidth memory or cache specifically for lower precision. For example, an accelerator might fetch two FP16 values in one 32-bit memory transaction – effectively doubling memory bandwidth for half precision. On some hardware, using FP16 can even reduce memory &#039;&#039;latency&#039;&#039; if multiple values can be packed.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Parallelism and Vectorization:&#039;&#039;&#039; Many hardware units process data in fixed vector widths (e.g., 256-bit registers). That could be 8 floats at once or 16 half-floats at once in the same 256 bits. If your code is in FP16, you might use the full vector width (16 values) whereas FP32 might only use half (8 values). This means FP16 can make better use of vector units. Similarly, matrix units often have fixed tile sizes, and smaller data types allow larger tiles or more tiles concurrently.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Energy efficiency:&#039;&#039;&#039; Lower precision often means less energy per operation, which on large deployments (data centers or battery-powered devices) is a significant advantage. This isn&#039;t directly performance, but it&#039;s an important trade: you might choose a slightly lower precision to save power even if accuracy drops a tiny bit.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Software overhead:&#039;&#039;&#039; Using mixed precision on hardware involves some overhead of casting and moving data between precisions. For instance, if you have to frequently cast FP16 to FP32 and back, that adds instructions and memory copies. A well-designed accelerator (like GPUs with unified memory for FP16/32 or special hardware casting) minimize this. Still, the runtime might insert extra operations (as we saw, ONNX Runtime CPU inserted many Cast nodes converting FP16 to FP32 and back, which actually &#039;&#039;slowed down&#039;&#039; that FP16 model by 4x vs FP32 on a CPU, because the CPU had no native FP16). So ironically, on some hardware, using lower precision in ONNX might degrade performance if not supported (the CPU case). Always ensure the target hardware actually supports the low precision you plan to use; otherwise, you&#039;ll pay overhead for emulation. &amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
To illustrate trade-off: Suppose you have a model that achieves 99% accuracy in FP32 and drops to 98.5% in FP16. If FP16 runs 2x faster, many would accept the 0.5% accuracy drop for double throughput. If int8 runs 4x faster but accuracy drops to 95%, that might be too much drop for some applications, but acceptable for others that value speed. It&#039;s application-dependent.&lt;br /&gt;
&lt;br /&gt;
In critical applications, one might maintain higher precision accumulators or run certain validation steps in high precision to ensure nothing drifted. In research, sometimes a model is trained in mixed precision but validated in double to ensure any small numeric differences don’t influence the reported result (rare, but for scientific computing integration maybe).&lt;br /&gt;
&lt;br /&gt;
In the ONNX world, performance-vs-accuracy is often managed via &#039;&#039;&#039;Execution Providers&#039;&#039;&#039; (EPs) that target specific hardware. For example, the TensorRT EP in ONNX Runtime can take an ONNX model and run it using NVIDIA&#039;s high-performance inference library, which will use FP16 or INT8 if allowed, to maximize speed. The user can configure the EP with precision flags. If the accuracy drop is too high, one might restrict it to FP32. If it&#039;s fine, allow FP16. Tools exist to measure the accuracy difference (like calibrating int8 quantization).&lt;br /&gt;
&lt;br /&gt;
To conclude, the trade-offs revolve around picking the lowest precision that yields acceptable accuracy, and utilizing hardware features fully. ONNX being a high-level spec abstracts this, but it&#039;s ultimately up to the deployment to decide and sometimes up to the exporter to incorporate quantization. ONNX’s role is to be able to &#039;&#039;represent&#039;&#039; these choices (they have added quantized types, FP8 types, etc., to allow the graph to include those when needed).&lt;br /&gt;
&lt;br /&gt;
== Best Practices and Design Guidelines ==&lt;br /&gt;
&lt;br /&gt;
Given the above insights, here are some best practices and guidelines for dealing with numerical precision and accumulators in AI inference (and training) design:&lt;br /&gt;
&lt;br /&gt;
=== 8.1. Choosing an Accumulator Precision ===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Match precision to data length and range:&#039;&#039;&#039; If an operation accumulates a large number of terms, consider using a higher precision accumulator than the input data. For example, for summing &amp;gt;1000 values of type FP16, an FP32 accumulator is recommended to avoid large rounding error or overflow. If summing &amp;gt; million values of FP32, consider double (FP64) accumulation if high accuracy is needed (though this is rare in DL). &#039;&#039;Rule of thumb:&#039;&#039; if &amp;lt;math&amp;gt;N \cdot \varepsilon&amp;lt;/math&amp;gt; is not tiny (say &amp;gt;1e-3), you may benefit from higher precision. For FP16 (&#039;&#039;ε ≈ 1e-3&#039;&#039;), that threshold N is a few hundred. For FP32 (&#039;&#039;ε ≈ 1e-7&#039;&#039;), N would be in the millions before 1e-3 error.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Use higher precision in critical layers:&#039;&#039;&#039; Identify if certain layers of the model are more sensitive to precision. For instance, the output layer or a layer that feeds into a very sharp decision boundary might require more precision. If a small change there could flip the prediction, you don&#039;t want excessive numerical error. Using FP32 for logits and softmax is a common practice to maintain classification confidence stability.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Follow hardware defaults, but override if needed:&#039;&#039;&#039; Many hardware accelerators have sensible defaults (like accumulate in FP32 for tensor ops). It&#039;s usually best to use those defaults as they were chosen to balance accuracy and speed. Only override if you have evidence of a problem. For example, if using ONNX Runtime on CPU, you discovered it upcasts to float for you – great. But if using some custom accelerator that does pure FP16, you might manually insert casts in the ONNX graph around certain ops to force higher precision on those ops.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Maintain precision for gradient accumulation (training):&#039;&#039;&#039; This is more training-oriented, but worth noting: when summing gradients over batches or updating weights, use higher precision (FP32 or even 64) for those accumulations. This ensures training doesn&#039;t diverge due to numeric error. Many frameworks keep a &amp;quot;master copy&amp;quot; of weights in FP32 when training in FP16. For inference, this translates to: any running statistics or counters in the model (like if you had a running average inside the model) should probably be in higher precision to avoid drift. &amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Be mindful of intermediate cast costs:&#039;&#039;&#039; If you do decide to cast to higher precision for an operation in ONNX, remember it incurs memory copies and compute overhead. Make sure it&#039;s worth it. For example, if you cast a huge tensor to double, that could slow things drastically. Usually FP16-&amp;gt;FP32 is fine (hardware handles it well), but FP32-&amp;gt;FP64 might be expensive on GPUs.&lt;br /&gt;
&lt;br /&gt;
=== 8.2. Safe Reduction Lengths and Heuristics ===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Chunk long reductions:&#039;&#039;&#039; If you must do a very long sum in low precision (due to memory constraints or hardware), chunk the sum into parts, sum each to a higher precision local accumulator, then sum the partial sums. This is essentially pairwise summation. Many libraries use a fixed chunk size heuristic (e.g., sum in blocks of 256 in registers, then add those up). This bounds the error growth. If implementing manually, you can choose a chunk size such that chunk_length &#039;&#039; ε is small (like 256 &#039;&#039; 1e-3 = 0.256 for FP16, hmm maybe choose 100).&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Sort by magnitude if possible:&#039;&#039;&#039; In some cases (not typical in DL, but possible in post-processing), if you have numbers varying by orders of magnitude, summing from smallest to largest improves accuracy. It prevents small ones from being swallowed by big ones. This is more relevant if you output a sum of mixed-scale values (like some algorithm mixing big and small contributions). It’s not always feasible in real-time inference to sort, but keep in mind.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Use compensation for critical sums:&#039;&#039;&#039; If you find a particular sum or difference in your inference computation is crucial (like computing a small residual from two close numbers), consider using Kahan summation or another compensated scheme just for that part. For example, if you had an ONNX custom function that needs to sum a lot of almost cancelling terms, you might implement it with compensation internally.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Heuristics for FP8/low-bit:&#039;&#039;&#039; When using extremely low precision like FP8, adopt the recommended practices: use scaling (don&#039;t feed raw data into FP8, normalize it first), and perhaps only use FP8 on portions of the network that are empirically robust. The industry is still establishing heuristics, but one might be: use FP8 for matrix multiplications, but accumulate in FP16 or FP32; keep normalization in FP16; use FP16 for first and last layer maybe. This is evolving, so keep an eye on literature.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Keep an eye on sum length in softmax and normalization:&#039;&#039;&#039; If you have extremely large class counts or group sizes (e.g., a softmax over 1e6 items, or layer norm over tens of thousands of features), realize this is a giant reduction. You may want to ensure that reduction is done in higher precision. ONNX Softmax by default will likely not handle a million-class case in FP16 well (the sum would overflow since 1e6 &amp;gt; 65504). So either do that on CPU in FP32 or break it down (could do hierarchical softmax, etc.). This is a corner case, but relevant for huge vocab language models. In practice, vocab ~50k is fine as we said.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Test numeric differences:&#039;&#039;&#039; A practical tip: after quantizing or reducing precision, run some test inputs through both the high-precision model and low-precision model. Compare outputs (and maybe some intermediate signals if possible). If differences are consistently small relative to the application tolerance, you&#039;re good. If you see occasional large differences or instabilities (like outputs become NaN or Inf in low precision), that&#039;s a red flag. Use that testing to guide where you might need to increase precision or change algorithms.&lt;br /&gt;
&lt;br /&gt;
=== 8.3. When Higher Precision Is Required ===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Ill-conditioned calculations:&#039;&#039;&#039; If your model or pipeline performs any operation known to be ill-conditioned (matrix inversion, subtraction of nearly equal quantities, computing a tiny difference of large numbers), you likely need higher precision for that part. For example, if your ONNX model includes a custom op to solve a linear system, use double precision in that op if possible (or at least float with iterative refinement). In ML, this might occur in something like a physics-informed neural network or a differentiable simulation within the model.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Strict accuracy requirements:&#039;&#039;&#039; Some applications (medical, aerospace, finance) might have strict tolerances. If an inference model’s output needs to be correct to 5 decimal places, you probably cannot use FP16, which has only 3-4 decimal digits precision. FP32 might be barely enough (~7 digits). FP64 might be warranted to guarantee that level of accuracy in worst case. Most deep learning applications (like image classification) don&#039;t demand that level of numeric precision in outputs – as long as the top class is correct, it doesn&#039;t matter if its probability was 0.901 vs 0.899. But if you&#039;re doing something like summing probabilities to make a risk estimate or doing a cumulative sum over many predictions, be careful to accumulate precisely.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Large sums or products in post-processing:&#039;&#039;&#039; Sometimes the neural net is fine, but the &#039;&#039;post-processing&#039;&#039; of results might need precision. E.g., summing up probabilities of many events, or computing confidence intervals, etc. Do those calculations in double if needed outside the model.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Preventing drift in long sequences:&#039;&#039;&#039; For sequential models (RNNs processing thousands of steps), rounding error can accumulate over steps. If each step has a small error, after 10000 steps it could add up. In such cases, using a higher precision for the state might reduce drift. Some practitioners use FP64 for the hidden state accumulation in very long sequence processing to maintain numerical stability (though this is not common due to speed loss). Alternatively, periodically renormalize or reset small errors.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Simulations or differential equations:&#039;&#039;&#039; If your ONNX model involves simulating something (like a differential equation solver inside a model, or an ODE/RNN hybrid), those often need double precision to be stable for long durations. Recognize those situations and allocate precision accordingly.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Verification and debugging:&#039;&#039;&#039; Use higher precision as a tool to verify correctness. For example, run your model in FP64 (if possible) and compare to FP32 to see if there are differences. If not, FP32 is fine. If yes, you might find a particular calculation losing too much info in FP32, implying maybe you should keep it in FP64 in production as well. This is rare in inference but can be part of rigorous validation.&lt;br /&gt;
&lt;br /&gt;
In ONNX, using higher precision may involve casting to &amp;lt;code&amp;gt;tensor(double)&amp;lt;/code&amp;gt; for certain ops. Not all runtimes support double on GPU though (some do via emulation or just slower path). ONNX CPU does support double for general ops. So be mindful of the target.&lt;br /&gt;
&lt;br /&gt;
Finally, &#039;&#039;&#039;document your precision choices&#039;&#039;&#039;. If you deliver an ONNX model that uses mixed types (float16 inputs, float32 accumulations, int8 weights, etc.), ensure that is clear to whoever maintains or uses it. Precision issues can be sneaky, so clarity helps.&lt;br /&gt;
&lt;br /&gt;
== Limitations and Open Problems ==&lt;br /&gt;
&lt;br /&gt;
Despite advances in handling numerical precision, there remain limitations and open challenges in this domain:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Lack of Precision Specification in ONNX:&#039;&#039;&#039; As noted, ONNX currently has no way to formally specify that an op &#039;&#039;should&#039;&#039; use a higher precision internally. This can lead to nondeterministic differences across platforms. While proposals exist to add attributes for this, it is not yet standard. Thus an ONNX model might behave slightly differently on two compliant backends, and there&#039;s no built-in mechanism to ensure identical results. This is a limitation for applications requiring cross-platform consistency. Solving it may involve extending ONNX (e.g., an attribute like &amp;lt;code&amp;gt;accumulatePrecision=&amp;quot;FLOAT32&amp;quot;&amp;lt;/code&amp;gt; for MatMul) or tightening the spec, but those come with backward compatibility and implementation burden concerns. &amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Reproducibility and Determinism:&#039;&#039;&#039; Even with the same precision, parallel computation can introduce nondeterminism (due to different summation orders). Ensuring bit-identical results across runs (important for some sensitive domains) is non-trivial. Frameworks often provide &amp;quot;deterministic mode&amp;quot; which might use fixed ordering at cost of speed. In ONNX, there&#039;s no global flag for this; it&#039;s up to the runtime. More research could be done on algorithms that produce bit-wise identical results independent of parallel execution order (perhaps using techniques like pairwise summation or consistent partitioning). Stochastic rounding is another area: sometimes using probabilistic rounding can reduce bias in accumulated error, but it introduces randomness – reconciling that with reproducibility is tricky.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Ultra-low Precision (FP8 and below):&#039;&#039;&#039; While FP8 is being explored, going to 4-bit floats or mixed schemes is an open challenge. The OpenAI community is investigating 4-bit (quantization-wise) for LLMs with some success, but a true 4-bit float (like E3M1) would be extremely limited. Perhaps hybrid schemes (like block floating point, log scales, etc.) will come. ONNX already has some support (they mention 4-bit in documentation placeholders), but it&#039;s nascent. Research into training techniques, calibration, and new number formats (like &#039;&#039;&#039;posit numbers&#039;&#039;&#039; or tapering precision floats) is ongoing. If a new format proves useful, ONNX will need to adapt to represent it. &amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Quantization Noise and Model Robustness:&#039;&#039;&#039; Reducing precision can sometimes expose models to issues like adversarial vulnerability or fragility – e.g., a slight perturbation might flip a decision if the margin was small and precision loss nudged it. Ensuring model robustness under quantization is an open problem. It might require re-training the model with quantization in the loop (quantization-aware training) or designing models that have large &amp;quot;margin&amp;quot; between classes to tolerate quantization noise.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Dynamic range during training vs inference:&#039;&#039;&#039; In training, activations can have wider distributions (especially early in training). Models often learn to constrain their ranges (via activation functions saturating or normalization), which makes inference more stable in lower precision. But if one tries to train in FP8 from scratch, it&#039;s very hard. This gap between what&#039;s possible in a post-trained inference model vs training is an open area – how to train directly in low precision without significant accuracy loss. Solving that could simplify workflows (no need for high-precision training then quantize; just train low-prec end to end). Techniques like loss scaling, gradient clipping, etc., are partial solutions. The development of optimizers that are resilient to low precision is ongoing.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Hardware variability:&#039;&#039;&#039; As new hardware emerges (with new numeric capabilities or quirks), ensuring that ONNX models run consistently is an ongoing task. For instance, one device might implement a fused op that reduces rounding error, another does it separately. Even something like convolution might use a different algorithm (FFT-based vs direct) which has different numeric properties. Open problem: could ONNX allow specifying numeric tolerances per op or preferred algorithms? That may be overkill for most, but for some HPC ML use cases, maybe desired.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Testing and validation of numerics:&#039;&#039;&#039; There isn&#039;t a standardized set of &amp;quot;numeric compliance&amp;quot; tests for ONNX runtimes beyond simple correctness. Perhaps in the future, test suites might include challenging numeric scenarios (large sums, nearly singular matrices) to ensure a runtime handles them gracefully (maybe by switching precision). Right now, it&#039;s mostly on the user to test their model under expected conditions.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Combining quantization with floating point:&#039;&#039;&#039; Many production models use a mix (e.g., int8 for some layers, FP16 for others). ONNX can represent this (with QuantizeLinear ops or MixedPrecision through casting), but tooling to automatically find the optimal mix is still developing. It&#039;s somewhat trial-and-error or using heuristics. An open question is how to automate precision assignment: given a model and a target hardware, how to automatically choose which layers can be int8, which should be FP16, etc., to meet an accuracy target. This crosses into NAS (neural architecture search) territory or automated quantization. &lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Numerical debugging tools:&#039;&#039;&#039; When a model goes wrong due to precision, it&#039;s often hard to pinpoint where. Better tooling to analyze numerical error layer by layer could be useful. For instance, one could run a model in double and float and diff the intermediate results to see where the largest divergence happens. Doing this systematically for big nets is an open tooling challenge. Some frameworks have hooks for this, but not standardized.&lt;br /&gt;
&lt;br /&gt;
In conclusion, while we&#039;ve come far with using lower precision in AI without sacrificing much accuracy, the landscape is always evolving. The key open problem is how to push precision lower (for efficiency gains) without crossing the threshold where accuracy falls off a cliff. Each new generation of hardware and techniques (like FP8, novel quantization schemes) extends that boundary, but also brings new questions on how to manage the numerical errors. ONNX as an interchange will need to keep pace, possibly by adding more expressive power for these numeric considerations.&lt;br /&gt;
&lt;br /&gt;
== References ==&lt;br /&gt;
&lt;br /&gt;
&amp;lt;references&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref1&amp;quot;&amp;gt;Accelerate and simplify Scikit-learn model inference with ONNX Runtime - Microsoft Open Source Blog — opensource.microsoft.com — https://opensource.microsoft.com/blog/2020/12/17/accelerate-simplify-scikit-learn-model-inference-onnx-runtime&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref2&amp;quot;&amp;gt;Floating Point Precision: Understanding FP64, FP32, and FP16 in Large Language Models - DEV Community — dev.to — https://dev.to/lukehinds/floating-point-precision-understanding-fp64-fp32-and-fp16-in-large-language-models-3gk6&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref3&amp;quot;&amp;gt;Floating Point Precision and Its Limitations | by Umair Akbar | Medium — akbu.medium.com — https://akbu.medium.com/floating-point-precision-and-its-limitations-cfb7247d7789&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref4&amp;quot;&amp;gt;Kahan summation algorithm - Wikipedia — en.wikipedia.org — https://en.wikipedia.org/wiki/Kahan_summation_algorithm&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref5&amp;quot;&amp;gt;Pairwise summation - Wikipedia — en.wikipedia.org — https://en.wikipedia.org/wiki/Pairwise_summation&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref6&amp;quot;&amp;gt;Numerical behavior of NVIDIA tensor cores - PMC — pmc.ncbi.nlm.nih.gov — https://pmc.ncbi.nlm.nih.gov/articles/PMC7959640/&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref7&amp;quot;&amp;gt;Mixed precision and performance-accuracy tuning (neuron-cc) — AWS Neuron Documentation — awsdocs-neuron.readthedocs-hosted.com — https://awsdocs-neuron.readthedocs-hosted.com/en/latest/about-neuron/appnotes/neuron-cc/mixed-precision.html&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref8&amp;quot;&amp;gt;Create Float16 and Mixed Precision Models - ONNX Runtime — onnxruntime.ai — https://onnxruntime.ai/docs/performance/model-optimizations/float16.html&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref9&amp;quot;&amp;gt;[Performance] ONNX FP16 model is having performance bottle neck when compared to FP32 variant · Issue #25824 · microsoft/onnxruntime · GitHub — github.com — https://github.com/microsoft/onnxruntime/issues/25824&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref10&amp;quot;&amp;gt;Float stored in 8 bits - ONNX 1.21.0 documentation — onnx.ai — https://onnx.ai/onnx/technical/float8.html&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref11&amp;quot;&amp;gt;[2209.05433] FP8 Formats for Deep Learning — arxiv.org — https://arxiv.org/abs/2209.05433&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref12&amp;quot;&amp;gt;Should MatMul op allow specification of accumulation precision? · Issue #7072 · onnx/onnx · GitHub — github.com — https://github.com/onnx/onnx/issues/7072&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref13&amp;quot;&amp;gt;Quantize ONNX models | onnxruntime — onnxruntime.ai — https://onnxruntime.ai/docs/performance/model-optimizations/quantization.html&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref14&amp;quot;&amp;gt;Keep Attention Softmax FP32 during FP16/ZeRO Training · Issue #1485 · hpcaitech/ColossalAI · GitHub — github.com — https://github.com/hpcaitech/ColossalAI/issues/1485&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;/references&amp;gt;&lt;br /&gt;
[[Category:Numerical Precision]]&lt;/div&gt;</summary>
		<author><name>Timo.stripf</name></author>
	</entry>
	<entry>
		<id>https://www.emmtrix.com/w139/index.php?title=ULP_Difference_of_Float_Numbers&amp;diff=2910</id>
		<title>ULP Difference of Float Numbers</title>
		<link rel="alternate" type="text/html" href="https://www.emmtrix.com/w139/index.php?title=ULP_Difference_of_Float_Numbers&amp;diff=2910"/>
		<updated>2026-02-03T09:19:30Z</updated>

		<summary type="html">&lt;p&gt;Timo.stripf: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;The &#039;&#039;&#039;Unit in the Last Place (ULP)&#039;&#039;&#039; is the smallest difference between two adjacent floating-point numbers at a specific precision. It measures the granularity of floating-point representations and is essential for understanding rounding errors and numerical stability. For example, a ULP near 1.0 is typically smaller than a ULP near 10.0, as the step size depends on the exponent of the number.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;ULP differences&#039;&#039;&#039; are often used to evaluate the accuracy of floating-point operations. When comparing two floating-point numbers, a difference of 1 ULP indicates that the numbers are as close as they can be while still being distinct. Smaller ULP differences imply better precision and numerical accuracy. When implementing floating-point operations or algorithms (e.g., the tanhf function), the smallest possible ULP difference is 0.5 indicating that the exact result is correctly rounded to the nearest floating-point number.&lt;br /&gt;
&lt;br /&gt;
==Calculating ULP==&lt;br /&gt;
To calculate the ULP of a floating-point number, you can use the following C function:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;c&amp;quot;&amp;gt;&lt;br /&gt;
#include &amp;lt;math.h&amp;gt;&lt;br /&gt;
&lt;br /&gt;
float flt_ulp_size(float x) {&lt;br /&gt;
    x = fabs(x);&lt;br /&gt;
&lt;br /&gt;
    return nextafterf(x, INFINITY) - x;&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This function calculates the ULP size of a given floating-point number &amp;lt;code&amp;gt;x&amp;lt;/code&amp;gt;. It first takes the absolute value of &amp;lt;code&amp;gt;x&amp;lt;/code&amp;gt; using &amp;lt;code&amp;gt;fabs(x)&amp;lt;/code&amp;gt;, then uses &amp;lt;code&amp;gt;nextafterf(x, INFINITY)&amp;lt;/code&amp;gt; to find the next representable float value after &amp;lt;code&amp;gt;x&amp;lt;/code&amp;gt; in the direction of positive infinity. The difference between these two values gives the ULP size for &amp;lt;code&amp;gt;x&amp;lt;/code&amp;gt;.&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+ULP Example Values 32-bit Float&lt;br /&gt;
!Floating-Point Number !! ULP (Macro) !! ULP (Float) !! ULP (Float Hex Representation)&lt;br /&gt;
!Description&lt;br /&gt;
|-&lt;br /&gt;
|1.0 || FLT_EPSILON || 1.1920929e-07 || 0x1.0p-23&lt;br /&gt;
|Smallest step near 1.0&lt;br /&gt;
|-&lt;br /&gt;
|0.0 || FLT_TRUE_MIN || 1.4012985e-45 || 0x1.0p-149&lt;br /&gt;
|Smallest positive float&lt;br /&gt;
|-&lt;br /&gt;
|0.5 || FLT_EPSILON/2 || 5.9604645e-08 || 0x1.0p-24&lt;br /&gt;
|Half the step size near 1.0&lt;br /&gt;
|-&lt;br /&gt;
|10.0 || FLT_EPSILON*8 || 9.536743e-07 || 0x1.0p-20&lt;br /&gt;
|Larger step size near 10.0&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
==Calculating ULP Difference==&lt;br /&gt;
&lt;br /&gt;
=== Simple ULP Difference ===&lt;br /&gt;
&lt;br /&gt;
It is relative hard to correctly calculate the ULP difference between two floating-point numbers. The following C function is a simple way to calculate the ULP difference between a reference value and a given value:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;c&amp;quot;&amp;gt;&lt;br /&gt;
float flt_ulp_diff(float ref, float val) {&lt;br /&gt;
    return fabs(ref - val) / flt_ulp_size(ref);&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This function simple divides the absolute difference by the ULP. The problem with this approach is which ULP to use. You can use the ULP of the reference value, approximated value or maximum/minimum/average or both values. In the given implementation the ULP of the reference value is used following the idea that the value of an potentially inaccurate approximation should not influence the precision for comparison. However, whatever ULP you choose, it will not be perfect. E.g. in the provided function &amp;lt;code&amp;gt;flt_ulp_diff(0.9999, 2.0)&amp;lt;/code&amp;gt; will be around factor 2 greater than &amp;lt;code&amp;gt;flt_ulp_diff(1.0, 2.0)&amp;lt;/code&amp;gt; which is not very intuitive.&lt;br /&gt;
&lt;br /&gt;
=== Correct ULP Difference ===&lt;br /&gt;
&lt;br /&gt;
To correctly calculate the ULP difference between two floating-point numbers, you can use the following C function:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;c&amp;quot;&amp;gt;&lt;br /&gt;
uint32_t ulp_intdiff_float(float f1, float f2) {&lt;br /&gt;
    if (signbit(f1) != signbit(f2)) {&lt;br /&gt;
        return ulp_intdiff_float(0.0f, fabs(f1)) + ulp_intdiff_float(0.0f, fabs(f2)) + 1;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    uint32_t i1 = *(uint32_t*)&amp;amp;f1;&lt;br /&gt;
    uint32_t i2 = *(uint32_t*)&amp;amp;f2;&lt;br /&gt;
&lt;br /&gt;
    return i1 &amp;gt; i2 ? i1 - i2 : i2 - i1;&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This function calculates the integer difference in ULP between two floating-point numbers &amp;lt;code&amp;gt;f1&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;f2&amp;lt;/code&amp;gt;. The trick is to reinterpret the floating-point numbers as 32-bit unsigned integers and then calculate the difference between them. The floating-point IEEE 754 representation is designed in such a way that the integer difference between two floating-point numbers (with the same sign) is the ULP difference between them.&lt;br /&gt;
&lt;br /&gt;
To also consider floating-point numbers with different signs, the function calculates the ULP difference towards zero for both numbers and adds the differences together. Additionally, it adds 1 to the result that ULP difference between negative and positive zero (&amp;lt;code&amp;gt;ulp_intdiff_float(-0.0f, 0.0f)&amp;lt;/code&amp;gt;) is 1. Adding 1 to the result is not necessary if you do not care about the difference between negative and positive zero. &lt;br /&gt;
&lt;br /&gt;
This implementation satisfies both the properties of triangular additivity (&amp;lt;code&amp;gt;ulp_intdiff_float(x, y) + ulp_intdiff_float(y, z) == ulp_intdiff_float(x, z) for x &amp;lt; y &amp;lt; z&amp;lt;/code&amp;gt;) and commutativity (&amp;lt;code&amp;gt;ulp_intdiff_float(x, y) == ulp_intdiff_float(y, x)&amp;lt;/code&amp;gt;) of ULP differences, ensuring that the ULP difference between any two floating-point numbers is consistent and order-independent when summed across intermediates.&lt;br /&gt;
&lt;br /&gt;
=== Examples ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+ Example Results for `ulp_intdiff_float` Function&lt;br /&gt;
! Input 1 !! Input 2 !! ULP Difference !! Explanation&lt;br /&gt;
|-&lt;br /&gt;
| 1.0 || 1.0 + FLT_EPSILON || 1 || Adjacent floating-point values&lt;br /&gt;
|-&lt;br /&gt;
| 1.0 || 1.0 - FLT_EPSILON || 2 || One step away in the opposite direction&lt;br /&gt;
|-&lt;br /&gt;
| FLT_TRUE_MIN || 0.0 || 1 || Smallest positive float to zero&lt;br /&gt;
|-&lt;br /&gt;
| -FLT_TRUE_MIN || FLT_TRUE_MIN || 3 || Crossing zero, including sign change&lt;br /&gt;
|-&lt;br /&gt;
| -1.0 || -1.0 - FLT_EPSILON || 1 || Adjacent floating-point values on the negative side&lt;br /&gt;
|-&lt;br /&gt;
| -1.0 || -1.0 + FLT_EPSILON || 2 || One step away in the opposite direction on the negative side&lt;br /&gt;
|-&lt;br /&gt;
| -0.0 || 0.0 || 1 || Difference between signed zero values&lt;br /&gt;
|-&lt;br /&gt;
| 0.0 || 0.0 || 0 || No difference between identical values&lt;br /&gt;
|-&lt;br /&gt;
| FLT_MAX || INFINITY || 1 || Transition from the largest finite float to infinity&lt;br /&gt;
|-&lt;br /&gt;
| -FLT_MAX || -INFINITY || 1 || Transition from the largest negative finite float to negative infinity&lt;br /&gt;
|-&lt;br /&gt;
| 1.0 || 0.5 || 8388608 || Number of representable floats between 1.0 and 0.5 (2^23)&lt;br /&gt;
|-&lt;br /&gt;
| 1.0 || 2.0 || 8388608 || Number of representable floats between 1.0 and 2.0 (2^23)&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
==Calculating Rational ULP Differences==&lt;br /&gt;
&lt;br /&gt;
When comparing a reference value provided as a &amp;lt;code&amp;gt;double&amp;lt;/code&amp;gt; and an approximated value as a &amp;lt;code&amp;gt;float&amp;lt;/code&amp;gt;, rational ULP differences can provide a finer granularity of comparison. The function below implements this computation by combining integer ULP differences with a fractional component derived from the difference between the &amp;lt;code&amp;gt;double&amp;lt;/code&amp;gt; reference value and its &amp;lt;code&amp;gt;float&amp;lt;/code&amp;gt; representation.&lt;br /&gt;
&lt;br /&gt;
The following function calculates rational ULP differences:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;c&amp;quot;&amp;gt;&lt;br /&gt;
double ulp_diff(double ref, float b) {&lt;br /&gt;
    // Handle NaN cases: returns 0 if both are NaN, INFINITY otherwise&lt;br /&gt;
    if (isnan(b) || isnan(ref)) {&lt;br /&gt;
        return isnan(b) == isnan(ref) ? 0.0 : INFINITY;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // Convert reference value to float for integer ULP difference calculation&lt;br /&gt;
    float fref = (float)ref;&lt;br /&gt;
&lt;br /&gt;
    // Calculate integer ULP difference between the float reference and test value&lt;br /&gt;
    double iulp = (double) ulp_intdiff_float(fref, b);&lt;br /&gt;
&lt;br /&gt;
    // Ensure positive values for further calculations&lt;br /&gt;
    ref = fabs(ref);&lt;br /&gt;
    fref = fabsf(fref);&lt;br /&gt;
    b = fabsf(b);&lt;br /&gt;
&lt;br /&gt;
    // Calculate the ULP size of the rounded-down reference value&lt;br /&gt;
    float ulpRef = flt_ulp_size(double_to_float_rounddown(ref));&lt;br /&gt;
&lt;br /&gt;
    // Calculate the fractional difference in ULPs&lt;br /&gt;
    double diff = (ref - (double)fref);&lt;br /&gt;
    double fulp = diff / ulpRef;&lt;br /&gt;
    if (b &amp;gt; ref)&lt;br /&gt;
        fulp = -fulp;&lt;br /&gt;
&lt;br /&gt;
    // Return the sum of integer ULPs and fractional ULPs&lt;br /&gt;
    return iulp + fulp;&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The function first handles special cases like NaN values, then calculates the integer ULP difference between the float-converted reference value and the approximated value. It then computes the fractional ULP difference based on the difference between the original double reference value and its float representation. We can use the method from the simple ULP difference to calculate the fractional ULP difference. That is working here because the ULP size for the fractional part is clearly defined by the location of the reference value. &lt;br /&gt;
&lt;br /&gt;
One problem is the rounding when converting the double to float. E.g. 1.0 - FLT_EPSILON/4 require the ULP size of the range [0.5, 1.0) but would be converted to 1.0 in a normal double to float conversion. This is why the function uses &amp;lt;code&amp;gt;double_to_float_rounddown&amp;lt;/code&amp;gt; to convert the double to float. This function converts the double to float and rounds down to the next smaller float value:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;c&amp;quot;&amp;gt;&lt;br /&gt;
float double_to_float_rounddown(double value) {&lt;br /&gt;
    float float_value = (float)value;&lt;br /&gt;
&lt;br /&gt;
    if (float_value &amp;gt; value) {&lt;br /&gt;
        return nextafterf(float_value, -INFINITY);&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    return float_value;&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Examples ===&lt;br /&gt;
&lt;br /&gt;
The following table provides examples of the results obtained using the &amp;lt;code&amp;gt;ulp_diff&amp;lt;/code&amp;gt; function:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+ Example Results for &amp;lt;code&amp;gt;ulp_diff&amp;lt;/code&amp;gt; Function&lt;br /&gt;
! Reference Value !! Test Value !! ULP Difference !!Explanation&lt;br /&gt;
|-&lt;br /&gt;
|1.0 + FLT_EPSILON/2||1.0||0.5||The test value is halfway between two floats.&lt;br /&gt;
|-&lt;br /&gt;
|1.0 + FLT_EPSILON/4|| 1.0 ||0.25||The test value is a quarter ULP away.&lt;br /&gt;
|-&lt;br /&gt;
| 1.0 - FLT_EPSILON/2|| 1.0|| 1.0||The test value is 1 ULP lower.&lt;br /&gt;
|-&lt;br /&gt;
| 1.0 - FLT_EPSILON/4||1.0||0.5||The test value is halfway to the next float.&lt;br /&gt;
|-&lt;br /&gt;
|1.0 + FLT_EPSILON/2||1.0 + FLT_EPSILON*10|| 9.5||Integer difference with fractional part.&lt;br /&gt;
|-&lt;br /&gt;
|1.0 + FLT_EPSILON/4||1.0 + FLT_EPSILON*10||9.75||Includes both integer and fractional differences.&lt;br /&gt;
|-&lt;br /&gt;
|1.0 - FLT_EPSILON/2||1.0 - FLT_EPSILON*10||19.0||The difference spans multiple ULPs.&lt;br /&gt;
|-&lt;br /&gt;
|1.0 - FLT_EPSILON/4||1.0 - FLT_EPSILON*10||19.5||Combined fractional and integer ULP difference.&lt;br /&gt;
|-&lt;br /&gt;
|0.0 + FLT_TRUE_MIN/100||0.0||0.01||A very small fractional ULP difference.&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Accuracy of Mathematical Functions in Math Libraries ==&lt;br /&gt;
The following table gives an overview of the ULP errors in widely used math libraries for single-precision floating-point math functions. The ULP error is the largest error in ULP between the exact result and the result of the function. The provided ULP error is exact for math functions with one parameter and the largest known error for two-parameter functions like atan2. &lt;br /&gt;
{| class=&amp;quot;wikitable sortable&amp;quot;&lt;br /&gt;
|+Largest (known) error in ULP for single precision math functions&amp;lt;ref name=&amp;quot;:0&amp;quot; /&amp;gt;&lt;br /&gt;
!library&lt;br /&gt;
&lt;br /&gt;
version&lt;br /&gt;
!GNU libc&lt;br /&gt;
&lt;br /&gt;
2.40&lt;br /&gt;
!IML&lt;br /&gt;
&lt;br /&gt;
2024.0.2&lt;br /&gt;
!AMD&lt;br /&gt;
&lt;br /&gt;
4.2&lt;br /&gt;
!Newlib&lt;br /&gt;
&lt;br /&gt;
4.4.0&lt;br /&gt;
!OpenLibm&lt;br /&gt;
&lt;br /&gt;
0.8.3&lt;br /&gt;
!Musl&lt;br /&gt;
&lt;br /&gt;
1.2.5&lt;br /&gt;
!Apple&lt;br /&gt;
&lt;br /&gt;
14.5&lt;br /&gt;
!LLVM&lt;br /&gt;
&lt;br /&gt;
18.1.8&lt;br /&gt;
!MSVC&lt;br /&gt;
&lt;br /&gt;
2022&lt;br /&gt;
!FreeBSD&lt;br /&gt;
&lt;br /&gt;
14.1&lt;br /&gt;
!ArmPL&lt;br /&gt;
&lt;br /&gt;
24.04&lt;br /&gt;
!CUDA&lt;br /&gt;
&lt;br /&gt;
12.2.1&lt;br /&gt;
!ROCm&lt;br /&gt;
&lt;br /&gt;
5.7.0&lt;br /&gt;
|-&lt;br /&gt;
|acos&lt;br /&gt;
|0.899&lt;br /&gt;
|0.528&lt;br /&gt;
|0.897&lt;br /&gt;
|0.899&lt;br /&gt;
|0.918&lt;br /&gt;
|0.918&lt;br /&gt;
|0.634&lt;br /&gt;
|&#039;&#039;&#039;0.500&#039;&#039;&#039;&lt;br /&gt;
|0.669&lt;br /&gt;
|0.918&lt;br /&gt;
|1.32&lt;br /&gt;
|1.34&lt;br /&gt;
|1.47&lt;br /&gt;
|-&lt;br /&gt;
|acosh&lt;br /&gt;
|2.01&lt;br /&gt;
|0.501&lt;br /&gt;
|0.504&lt;br /&gt;
|2.01&lt;br /&gt;
|2.01&lt;br /&gt;
|2.01&lt;br /&gt;
|0.502&lt;br /&gt;
|&#039;&#039;&#039;0.500&#039;&#039;&#039;&lt;br /&gt;
|2.89&lt;br /&gt;
|2.01&lt;br /&gt;
|2.79&lt;br /&gt;
|2.18&lt;br /&gt;
|0.564&lt;br /&gt;
|-&lt;br /&gt;
|asin&lt;br /&gt;
|0.898&lt;br /&gt;
|0.528&lt;br /&gt;
|0.781&lt;br /&gt;
|0.926&lt;br /&gt;
|0.743&lt;br /&gt;
|0.743&lt;br /&gt;
|0.634&lt;br /&gt;
|&#039;&#039;&#039;0.500&#039;&#039;&#039;&lt;br /&gt;
|0.861&lt;br /&gt;
|0.743&lt;br /&gt;
|2.41&lt;br /&gt;
|1.36&lt;br /&gt;
|2.54&lt;br /&gt;
|-&lt;br /&gt;
|asinh&lt;br /&gt;
|1.78&lt;br /&gt;
|0.527&lt;br /&gt;
|0.518&lt;br /&gt;
|1.78&lt;br /&gt;
|1.78&lt;br /&gt;
|1.78&lt;br /&gt;
|0.515&lt;br /&gt;
|&#039;&#039;&#039;0.500&#039;&#039;&#039;&lt;br /&gt;
|1.99&lt;br /&gt;
|1.78&lt;br /&gt;
|3.57&lt;br /&gt;
|1.78&lt;br /&gt;
|0.573&lt;br /&gt;
|-&lt;br /&gt;
|atan&lt;br /&gt;
|0.853&lt;br /&gt;
|0.541&lt;br /&gt;
|0.501&lt;br /&gt;
|0.853&lt;br /&gt;
|0.853&lt;br /&gt;
|0.853&lt;br /&gt;
|0.722&lt;br /&gt;
|&#039;&#039;&#039;0.500&#039;&#039;&#039;&lt;br /&gt;
|0.501&lt;br /&gt;
|0.853&lt;br /&gt;
|2.88&lt;br /&gt;
|1.21&lt;br /&gt;
|2.10&lt;br /&gt;
|-&lt;br /&gt;
|atanh&lt;br /&gt;
|1.73&lt;br /&gt;
|0.507&lt;br /&gt;
|0.547&lt;br /&gt;
|1.73&lt;br /&gt;
|1.73&lt;br /&gt;
|1.73&lt;br /&gt;
|0.511&lt;br /&gt;
|&#039;&#039;&#039;0.500&#039;&#039;&#039;&lt;br /&gt;
|2.35&lt;br /&gt;
|1.73&lt;br /&gt;
|3.09&lt;br /&gt;
|3.16&lt;br /&gt;
|0.574&lt;br /&gt;
|-&lt;br /&gt;
|cbrt&lt;br /&gt;
|0.969&lt;br /&gt;
|0.520&lt;br /&gt;
|0.548&lt;br /&gt;
|3.56&lt;br /&gt;
|&#039;&#039;&#039;0.500&#039;&#039;&#039;&lt;br /&gt;
|&#039;&#039;&#039;0.500&#039;&#039;&#039;&lt;br /&gt;
|&#039;&#039;&#039;0.500&#039;&#039;&#039;&lt;br /&gt;
|&lt;br /&gt;
|1.83&lt;br /&gt;
|&#039;&#039;&#039;0.500&#039;&#039;&#039;&lt;br /&gt;
|1.53&lt;br /&gt;
|1.17&lt;br /&gt;
|1.14&lt;br /&gt;
|-&lt;br /&gt;
|cos&lt;br /&gt;
|0.561&lt;br /&gt;
|0.548&lt;br /&gt;
|0.729&lt;br /&gt;
|2.91&lt;br /&gt;
|0.501&lt;br /&gt;
|0.501&lt;br /&gt;
|0.846&lt;br /&gt;
|&#039;&#039;&#039;0.500&#039;&#039;&#039;&lt;br /&gt;
|0.530&lt;br /&gt;
|0.501&lt;br /&gt;
|0.561&lt;br /&gt;
|1.52&lt;br /&gt;
|1.61&lt;br /&gt;
|-&lt;br /&gt;
|cosh&lt;br /&gt;
|1.89&lt;br /&gt;
|0.506&lt;br /&gt;
|1.03&lt;br /&gt;
|2.51&lt;br /&gt;
|1.36&lt;br /&gt;
|1.03&lt;br /&gt;
|0.579&lt;br /&gt;
|&#039;&#039;&#039;0.500&#039;&#039;&#039;&lt;br /&gt;
|&#039;&#039;&#039;0.500&#039;&#039;&#039;&lt;br /&gt;
|1.36&lt;br /&gt;
|1.89&lt;br /&gt;
|2.34&lt;br /&gt;
|0.567&lt;br /&gt;
|-&lt;br /&gt;
|erf&lt;br /&gt;
|0.968&lt;br /&gt;
|0.780&lt;br /&gt;
|0.531&lt;br /&gt;
|0.968&lt;br /&gt;
|0.943&lt;br /&gt;
|0.968&lt;br /&gt;
|0.501&lt;br /&gt;
|&#039;&#039;&#039;0.500&#039;&#039;&#039;&lt;br /&gt;
|3.99&lt;br /&gt;
|0.890&lt;br /&gt;
|1.93&lt;br /&gt;
|1.04&lt;br /&gt;
|1.51&lt;br /&gt;
|-&lt;br /&gt;
|erfc&lt;br /&gt;
|3.13&lt;br /&gt;
|0.934&lt;br /&gt;
|&lt;br /&gt;
|63.9&lt;br /&gt;
|3.17&lt;br /&gt;
|3.13&lt;br /&gt;
|&#039;&#039;&#039;0.750&#039;&#039;&#039;&lt;br /&gt;
|&lt;br /&gt;
|6.66&lt;br /&gt;
|3.18&lt;br /&gt;
|1.64&lt;br /&gt;
|4.49&lt;br /&gt;
|3.33&lt;br /&gt;
|-&lt;br /&gt;
|exp&lt;br /&gt;
|0.502&lt;br /&gt;
|0.506&lt;br /&gt;
|0.501&lt;br /&gt;
|0.911&lt;br /&gt;
|0.911&lt;br /&gt;
|0.502&lt;br /&gt;
|0.514&lt;br /&gt;
|&#039;&#039;&#039;0.500&#039;&#039;&#039;&lt;br /&gt;
|0.501&lt;br /&gt;
|0.911&lt;br /&gt;
|0.502&lt;br /&gt;
|1.94&lt;br /&gt;
|1.00&lt;br /&gt;
|-&lt;br /&gt;
|exp10&lt;br /&gt;
|0.502&lt;br /&gt;
|0.507&lt;br /&gt;
|0.501&lt;br /&gt;
|1.06&lt;br /&gt;
|&lt;br /&gt;
|3.88&lt;br /&gt;
|0.514&lt;br /&gt;
|&#039;&#039;&#039;0.500&#039;&#039;&#039;&lt;br /&gt;
|&lt;br /&gt;
|&lt;br /&gt;
|&lt;br /&gt;
|2.07&lt;br /&gt;
|1.00&lt;br /&gt;
|-&lt;br /&gt;
|exp2&lt;br /&gt;
|0.502&lt;br /&gt;
|0.519&lt;br /&gt;
|0.501&lt;br /&gt;
|1.02&lt;br /&gt;
|0.501&lt;br /&gt;
|0.502&lt;br /&gt;
|0.514&lt;br /&gt;
|&#039;&#039;&#039;0.500&#039;&#039;&#039;&lt;br /&gt;
|2.14&lt;br /&gt;
|0.501&lt;br /&gt;
|0.502&lt;br /&gt;
|2.39&lt;br /&gt;
|0.871&lt;br /&gt;
|-&lt;br /&gt;
|expm1&lt;br /&gt;
|0.813&lt;br /&gt;
|0.544&lt;br /&gt;
|0.536&lt;br /&gt;
|0.813&lt;br /&gt;
|0.813&lt;br /&gt;
|0.813&lt;br /&gt;
|0.687&lt;br /&gt;
|&#039;&#039;&#039;0.500&#039;&#039;&#039;&lt;br /&gt;
|3.02&lt;br /&gt;
|0.813&lt;br /&gt;
|1.51&lt;br /&gt;
|1.45&lt;br /&gt;
|1.45&lt;br /&gt;
|-&lt;br /&gt;
|j0&lt;br /&gt;
|9.00&lt;br /&gt;
|&#039;&#039;&#039;0.678&#039;&#039;&#039;&lt;br /&gt;
|&lt;br /&gt;
|6.18e6&lt;br /&gt;
|3.66e6&lt;br /&gt;
|3.66e6&lt;br /&gt;
|&lt;br /&gt;
|&lt;br /&gt;
|&lt;br /&gt;
|3.66e6&lt;br /&gt;
|&lt;br /&gt;
|3.78e10&lt;br /&gt;
|7.60e7&lt;br /&gt;
|-&lt;br /&gt;
|j1&lt;br /&gt;
|9.00&lt;br /&gt;
|&#039;&#039;&#039;1.69&#039;&#039;&#039;&lt;br /&gt;
|&lt;br /&gt;
|1.68e7&lt;br /&gt;
|2.25e6&lt;br /&gt;
|2.25e6&lt;br /&gt;
|&lt;br /&gt;
|&lt;br /&gt;
|&lt;br /&gt;
|2.25e6&lt;br /&gt;
|&lt;br /&gt;
|7.48e9&lt;br /&gt;
|7.53e7&lt;br /&gt;
|-&lt;br /&gt;
|lgamma&lt;br /&gt;
|6.78&lt;br /&gt;
|0.510&lt;br /&gt;
|&lt;br /&gt;
|7.50e6&lt;br /&gt;
|7.50e6&lt;br /&gt;
|7.50e6&lt;br /&gt;
|&#039;&#039;&#039;0.501&#039;&#039;&#039;&lt;br /&gt;
|&lt;br /&gt;
|2.92e5&lt;br /&gt;
|7.50e6&lt;br /&gt;
|&lt;br /&gt;
|1.35e7&lt;br /&gt;
|7.50e6&lt;br /&gt;
|-&lt;br /&gt;
|log&lt;br /&gt;
|0.818&lt;br /&gt;
|0.519&lt;br /&gt;
|0.577&lt;br /&gt;
|0.888&lt;br /&gt;
|0.888&lt;br /&gt;
|0.818&lt;br /&gt;
|0.511&lt;br /&gt;
|&#039;&#039;&#039;0.500&#039;&#039;&#039;&lt;br /&gt;
|0.562&lt;br /&gt;
|0.888&lt;br /&gt;
|0.818&lt;br /&gt;
|0.865&lt;br /&gt;
|1.89&lt;br /&gt;
|-&lt;br /&gt;
|log10&lt;br /&gt;
|2.07&lt;br /&gt;
|0.516&lt;br /&gt;
|1.40&lt;br /&gt;
|2.10&lt;br /&gt;
|0.832&lt;br /&gt;
|0.832&lt;br /&gt;
|0.502&lt;br /&gt;
|&#039;&#039;&#039;0.500&#039;&#039;&#039;&lt;br /&gt;
|0.626&lt;br /&gt;
|0.832&lt;br /&gt;
|0.82&lt;br /&gt;
|2.09&lt;br /&gt;
|1.71&lt;br /&gt;
|-&lt;br /&gt;
|log1p&lt;br /&gt;
|1.30&lt;br /&gt;
|0.525&lt;br /&gt;
|0.501&lt;br /&gt;
|1.30&lt;br /&gt;
|0.839&lt;br /&gt;
|0.835&lt;br /&gt;
|0.513&lt;br /&gt;
|&#039;&#039;&#039;0.500&#039;&#039;&#039;&lt;br /&gt;
|1.44&lt;br /&gt;
|0.839&lt;br /&gt;
|2.02&lt;br /&gt;
|0.887&lt;br /&gt;
|0.579&lt;br /&gt;
|-&lt;br /&gt;
|log2&lt;br /&gt;
|0.752&lt;br /&gt;
|0.508&lt;br /&gt;
|0.766&lt;br /&gt;
|1.65&lt;br /&gt;
|0.865&lt;br /&gt;
|0.752&lt;br /&gt;
|0.502&lt;br /&gt;
|&#039;&#039;&#039;0.500&#039;&#039;&#039;&lt;br /&gt;
|2.04&lt;br /&gt;
|0.865&lt;br /&gt;
|0.752&lt;br /&gt;
|0.919&lt;br /&gt;
|1.00&lt;br /&gt;
|-&lt;br /&gt;
|sin&lt;br /&gt;
|0.561&lt;br /&gt;
|0.546&lt;br /&gt;
|0.530&lt;br /&gt;
|1.37&lt;br /&gt;
|0.501&lt;br /&gt;
|0.501&lt;br /&gt;
|0.846&lt;br /&gt;
|&#039;&#039;&#039;0.500&#039;&#039;&#039;&lt;br /&gt;
|0.530&lt;br /&gt;
|0.501&lt;br /&gt;
|0.561&lt;br /&gt;
|1.50&lt;br /&gt;
|1.61&lt;br /&gt;
|-&lt;br /&gt;
|sinh&lt;br /&gt;
|1.89&lt;br /&gt;
|0.538&lt;br /&gt;
|&#039;&#039;&#039;0.500&#039;&#039;&#039;&lt;br /&gt;
|2.51&lt;br /&gt;
|1.83&lt;br /&gt;
|1.83&lt;br /&gt;
|0.579&lt;br /&gt;
|&#039;&#039;&#039;0.500&#039;&#039;&#039;&lt;br /&gt;
|0.501&lt;br /&gt;
|1.83&lt;br /&gt;
|2.26&lt;br /&gt;
|2.94&lt;br /&gt;
|0.922&lt;br /&gt;
|-&lt;br /&gt;
|sqrt&lt;br /&gt;
|&#039;&#039;&#039;0.500&#039;&#039;&#039;&lt;br /&gt;
|&#039;&#039;&#039;0.500&#039;&#039;&#039;&lt;br /&gt;
|&#039;&#039;&#039;0.500&#039;&#039;&#039;&lt;br /&gt;
|&#039;&#039;&#039;0.500&#039;&#039;&#039;&lt;br /&gt;
|&#039;&#039;&#039;0.500&#039;&#039;&#039;&lt;br /&gt;
|&#039;&#039;&#039;0.500&#039;&#039;&#039;&lt;br /&gt;
|&#039;&#039;&#039;0.500&#039;&#039;&#039;&lt;br /&gt;
|&#039;&#039;&#039;0.500&#039;&#039;&#039;&lt;br /&gt;
|&#039;&#039;&#039;0.500&#039;&#039;&#039;&lt;br /&gt;
|&#039;&#039;&#039;0.500&#039;&#039;&#039;&lt;br /&gt;
|&lt;br /&gt;
|&#039;&#039;&#039;0.500&#039;&#039;&#039;&lt;br /&gt;
|&#039;&#039;&#039;0.500&#039;&#039;&#039;&lt;br /&gt;
|-&lt;br /&gt;
|tan&lt;br /&gt;
|1.48&lt;br /&gt;
|0.520&lt;br /&gt;
|0.509&lt;br /&gt;
|3.48&lt;br /&gt;
|0.800&lt;br /&gt;
|0.800&lt;br /&gt;
|0.746&lt;br /&gt;
|&#039;&#039;&#039;0.500&#039;&#039;&#039;&lt;br /&gt;
|0.502&lt;br /&gt;
|0.800&lt;br /&gt;
|3.30&lt;br /&gt;
|3.10&lt;br /&gt;
|2.33&lt;br /&gt;
|-&lt;br /&gt;
|tanh&lt;br /&gt;
|2.19&lt;br /&gt;
|0.514&lt;br /&gt;
|&#039;&#039;&#039;0.500&#039;&#039;&#039;&lt;br /&gt;
|2.19&lt;br /&gt;
|2.19&lt;br /&gt;
|2.19&lt;br /&gt;
|0.817&lt;br /&gt;
|&#039;&#039;&#039;0.500&#039;&#039;&#039;&lt;br /&gt;
|1.27&lt;br /&gt;
|2.19&lt;br /&gt;
|2.59&lt;br /&gt;
|1.82&lt;br /&gt;
|1.41&lt;br /&gt;
|-&lt;br /&gt;
|tgamma&lt;br /&gt;
|7.91&lt;br /&gt;
|0.510&lt;br /&gt;
|&lt;br /&gt;
|239&lt;br /&gt;
|&#039;&#039;&#039;0.501&#039;&#039;&#039;&lt;br /&gt;
|&#039;&#039;&#039;0.501&#039;&#039;&#039;&lt;br /&gt;
|&#039;&#039;&#039;0.501&#039;&#039;&#039;&lt;br /&gt;
|&lt;br /&gt;
|3.58e5&lt;br /&gt;
|&#039;&#039;&#039;0.501&#039;&#039;&#039;&lt;br /&gt;
|&lt;br /&gt;
|4.34&lt;br /&gt;
|1.68e7&lt;br /&gt;
|-&lt;br /&gt;
|y0&lt;br /&gt;
|8.98&lt;br /&gt;
|&#039;&#039;&#039;3.40&#039;&#039;&#039;&lt;br /&gt;
|&lt;br /&gt;
|4.84e6&lt;br /&gt;
|4.84e6&lt;br /&gt;
|4.84e6&lt;br /&gt;
|&lt;br /&gt;
|&lt;br /&gt;
|&lt;br /&gt;
|4.84e6&lt;br /&gt;
|&lt;br /&gt;
|2.36e10&lt;br /&gt;
|7.53e7&lt;br /&gt;
|-&lt;br /&gt;
|y1&lt;br /&gt;
|9.00&lt;br /&gt;
|&#039;&#039;&#039;2.07&#039;&#039;&#039;&lt;br /&gt;
|&lt;br /&gt;
|6.18e6&lt;br /&gt;
|4.17e6&lt;br /&gt;
|3.66e6&lt;br /&gt;
|&lt;br /&gt;
|&lt;br /&gt;
|&lt;br /&gt;
|4.17e6&lt;br /&gt;
|&lt;br /&gt;
|4.96e10&lt;br /&gt;
|9.35e7&lt;br /&gt;
|-&lt;br /&gt;
|atan2&lt;br /&gt;
|1.52&lt;br /&gt;
|&#039;&#039;&#039;0.550&#039;&#039;&#039;&lt;br /&gt;
|0.584&lt;br /&gt;
|1.52&lt;br /&gt;
|1.55&lt;br /&gt;
|1.55&lt;br /&gt;
|0.722&lt;br /&gt;
|&lt;br /&gt;
|0.584&lt;br /&gt;
|1.55&lt;br /&gt;
|2.93&lt;br /&gt;
|2.18&lt;br /&gt;
|2.01&lt;br /&gt;
|-&lt;br /&gt;
|atan2pi&lt;br /&gt;
|&lt;br /&gt;
|&#039;&#039;&#039;0.841&#039;&#039;&#039;&lt;br /&gt;
|&lt;br /&gt;
|&lt;br /&gt;
|&lt;br /&gt;
|&lt;br /&gt;
|&lt;br /&gt;
|&lt;br /&gt;
|&lt;br /&gt;
|&lt;br /&gt;
|&lt;br /&gt;
|&lt;br /&gt;
|&lt;br /&gt;
|-&lt;br /&gt;
|compound&lt;br /&gt;
|&lt;br /&gt;
|&#039;&#039;&#039;0.501&#039;&#039;&#039;&lt;br /&gt;
|&lt;br /&gt;
|&lt;br /&gt;
|&lt;br /&gt;
|&lt;br /&gt;
|&lt;br /&gt;
|&lt;br /&gt;
|&lt;br /&gt;
|&lt;br /&gt;
|&lt;br /&gt;
|&lt;br /&gt;
|&lt;br /&gt;
|-&lt;br /&gt;
|hypot&lt;br /&gt;
|0.501&lt;br /&gt;
|0.501&lt;br /&gt;
|0.501&lt;br /&gt;
|1.21&lt;br /&gt;
|1.21&lt;br /&gt;
|0.927&lt;br /&gt;
|0.501&lt;br /&gt;
|&#039;&#039;&#039;0.500&#039;&#039;&#039;&lt;br /&gt;
|0.501&lt;br /&gt;
|1.21&lt;br /&gt;
|&lt;br /&gt;
|1.03&lt;br /&gt;
|1.57&lt;br /&gt;
|-&lt;br /&gt;
|pow&lt;br /&gt;
|0.817&lt;br /&gt;
|0.515&lt;br /&gt;
|1.56&lt;br /&gt;
|169.&lt;br /&gt;
|0.970&lt;br /&gt;
|0.817&lt;br /&gt;
|0.515&lt;br /&gt;
|&#039;&#039;&#039;0.500&#039;&#039;&#039;&lt;br /&gt;
|0.568&lt;br /&gt;
|0.970&lt;br /&gt;
|0.817&lt;br /&gt;
|2.60&lt;br /&gt;
|1.40&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Further Reading ==&lt;br /&gt;
&lt;br /&gt;
* [[wikipedia:Unit in the last place|Unit in the last place]] article on Wikipedia&lt;br /&gt;
* [https://citeseerx.ist.psu.edu/document?repid=rep1&amp;amp;type=pdf&amp;amp;doi=bc07e8d52dbbac5bef9551bbc111b95d82dc4766 On the definition of ulp (x)] &amp;lt;ref&amp;gt;Jean-Michel Muller. On the definition of ulp(x). [Research Report] RR-5504, LIP RR-2005-09, INRIA, LIP. 2005, pp.16. [https://inria.hal.science/inria-00070503 ⟨inria-00070503⟩]&amp;lt;/ref&amp;gt;&lt;br /&gt;
* [https://members.loria.fr/PZimmermann/papers/accuracy.pdf Accuracy of Mathematical Functions in Single, Double, Double Extended, and Quadruple Precision] &amp;lt;ref name=&amp;quot;:0&amp;quot;&amp;gt;Brian Gladman, Vincenzo Innocente, John Mather, Paul Zimmermann. Accuracy of Mathematical Functions in Single, Double, Double Extended, and Quadruple Precision. 2024. [https://inria.hal.science/hal-03141101 ⟨hal-03141101v7⟩]&amp;lt;/ref&amp;gt;&lt;br /&gt;
* [https://www.gnu.org/software/libc/manual/html_node/Errors-in-Math-Functions.html Official glibc documentation about their math function errors]&lt;br /&gt;
&lt;br /&gt;
== References ==&lt;br /&gt;
&amp;lt;references /&amp;gt;&lt;br /&gt;
[[Category:Numerical Precision]]&lt;/div&gt;</summary>
		<author><name>Timo.stripf</name></author>
	</entry>
	<entry>
		<id>https://www.emmtrix.com/w139/index.php?title=Numerical_Precision_in_ONNX_and_AI_Inference&amp;diff=2909</id>
		<title>Numerical Precision in ONNX and AI Inference</title>
		<link rel="alternate" type="text/html" href="https://www.emmtrix.com/w139/index.php?title=Numerical_Precision_in_ONNX_and_AI_Inference&amp;diff=2909"/>
		<updated>2026-02-03T09:16:13Z</updated>

		<summary type="html">&lt;p&gt;Timo.stripf: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&#039;&#039;&#039;Open Neural Network Exchange (ONNX)&#039;&#039;&#039; is an open standard format for representing machine learning models and neural network computations across different frameworks and hardware. As models are exported and deployed via ONNX, the &#039;&#039;&#039;numerical precision&#039;&#039;&#039; of computations becomes critical. Deep learning inference involves a variety of floating-point operations, and small numerical discrepancies can accumulate and affect model accuracy or reproducibility. This article provides an in-depth overview of floating-point precision issues in numerical computing, with a focus on how they manifest in AI inference and the ONNX ecosystem. We cover floating-point formats and rounding behavior, accumulation strategies for summations, the use of low-precision formats (FP16, BFloat16, FP8) in modern AI, and how ONNX defines (or leaves unspecified) the precision of accumulators in operations. Strategies for maintaining numerical stability in deep neural networks are discussed, along with the trade-offs between computational performance and arithmetic accuracy. The goal is to give engineers and researchers a comprehensive, encyclopedic reference on numerical precision in ONNX-based AI inference systems. &amp;lt;ref name=&amp;quot;ref1&amp;quot;/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Floating-Point Precision in Numerical Computing ==&lt;br /&gt;
&lt;br /&gt;
=== 2.1. IEEE 754 Floating-Point Formats ===&lt;br /&gt;
&lt;br /&gt;
Modern computers represent real numbers using the IEEE 754 floating-point standard, which defines formats like 32-bit &#039;&#039;single precision&#039;&#039; (FP32) and 64-bit &#039;&#039;double precision&#039;&#039; (FP64). In IEEE 754 binary formats, a floating-point number is composed of three fields: 1 sign bit, several exponent bits (with a bias), and several fraction (mantissa) bits. For example, FP32 consists of 1 sign bit, 8 exponent bits, and 23 fraction bits (24 bits of significand precision including the implicit leading 1). This provides roughly 7 decimal digits of precision and an exponent range allowing values on the order of &#039;&#039;10&amp;lt;sup&amp;gt;±38&amp;lt;/sup&amp;gt;&#039;&#039;. FP64 allocates 1 sign bit, 11 exponent bits, and 52 fraction bits (53-bit precision), yielding about 15–16 decimal digits of precision and a vastly larger range (~&#039;&#039;10&amp;lt;sup&amp;gt;±308&amp;lt;/sup&amp;gt;&#039;&#039;). These larger formats (especially FP64) are the &amp;quot;gold standard&amp;quot; for numerical accuracy in scientific computing, as they reduce round-off error in complex calculations. However, the increased precision comes at the cost of doubled storage and typically reduced computational throughput (e.g. many GPUs execute FP64 instructions at a much lower rate than FP32). &amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
In addition to single and double precision, the IEEE 754-2008 standard introduced a 16-bit &#039;&#039;half precision&#039;&#039; format (FP16, or binary16). FP16 has 1 sign bit, 5 exponent bits, and 10 fraction bits (11-bit significand including the implicit 1). This format covers a much smaller dynamic range (approximately &#039;&#039;6.1 × 10&amp;lt;sup&amp;gt;-5&amp;lt;/sup&amp;gt;&#039;&#039; to &#039;&#039;6.5 × 10&amp;lt;sup&amp;gt;4&amp;lt;/sup&amp;gt;&#039;&#039; for normalized values) and offers only around 3 decimal digits of precision. Half precision was originally used in graphics, but it has gained popularity in machine learning for its speed and memory advantages (discussed later in Section 4). Another format relevant to AI is &#039;&#039;&#039;BFloat16&#039;&#039;&#039; (Brain Float 16), a 16-bit float with &#039;&#039;&#039;8 exponent bits and 7 fraction bits&#039;&#039;&#039;. BFloat16 sacrifices precision (only ~7 fraction bits ≈ 2–3 decimal digits) in order to have the &#039;&#039;&#039;same exponent range as FP32&#039;&#039;&#039;. This means BFloat16 can represent very large or very small numbers (∼&#039;&#039;10&amp;lt;sup&amp;gt;±38&amp;lt;/sup&amp;gt;&#039;&#039;, similar range as FP32) but with much less precision between representable values. BFloat16 was introduced for deep learning by Google (for TPUs) to allow reduced precision &#039;&#039;training&#039;&#039; without frequent overflow/underflow, relying on the wide range and accepting the coarse precision. &amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Each floating-point format has a &#039;&#039;&#039;unit roundoff&#039;&#039;&#039; or machine precision, denoted &#039;&#039;ε&#039;&#039;. This is effectively the spacing between 1.0 and the next representable number. For FP32, &#039;&#039;ε ≈ 2&amp;lt;sup&amp;gt;-23&amp;lt;/sup&amp;gt; ≈ 1.19 × 10&amp;lt;sup&amp;gt;-7&amp;lt;/sup&amp;gt;&#039;&#039;, corresponding to about 7–8 decimal digits of precision. In FP64, &#039;&#039;ε = 2&amp;lt;sup&amp;gt;-52&amp;lt;/sup&amp;gt; ≈ 2.22 × 10&amp;lt;sup&amp;gt;-16&amp;lt;/sup&amp;gt;&#039;&#039; (15–16 decimal digits). Half precision is much less precise: FP16 has &#039;&#039;ε = 2&amp;lt;sup&amp;gt;-10&amp;lt;/sup&amp;gt; ≈ 9.76 × 10&amp;lt;sup&amp;gt;-4&amp;lt;/sup&amp;gt;&#039;&#039;, and BFloat16 has &#039;&#039;ε = 2&amp;lt;sup&amp;gt;-7&amp;lt;/sup&amp;gt; = 7.8125 × 10&amp;lt;sup&amp;gt;-3&amp;lt;/sup&amp;gt;&#039;&#039;. These values quantify how finely the continuum of real numbers is quantized in each format. &amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 2.2. Machine Epsilon and Rounding Error ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Machine epsilon (&#039;&#039;ε&#039;&#039;)&#039;&#039;&#039; is formally defined as the smallest positive number such that &#039;&#039;1.0 + ε &amp;gt; 1.0&#039;&#039; in the floating-point format. It effectively measures the &#039;&#039;&#039;precision&#039;&#039;&#039; of the representation. In FP32, &#039;&#039;ε = 2&amp;lt;sup&amp;gt;-23&amp;lt;/sup&amp;gt; ≈ 1.19 × 10&amp;lt;sup&amp;gt;-7&amp;lt;/sup&amp;gt;&#039;&#039;, meaning any result is rounded to about 7 decimal digits. FP16’s &#039;&#039;ε = 2&amp;lt;sup&amp;gt;-10&amp;lt;/sup&amp;gt; ≈ 9.77 × 10&amp;lt;sup&amp;gt;-4&amp;lt;/sup&amp;gt;&#039;&#039;, over 1000× larger (so only ~3 decimal digits of precision), and BFloat16’s &#039;&#039;ε = 2&amp;lt;sup&amp;gt;-7&amp;lt;/sup&amp;gt; ≈ 7.8 × 10&amp;lt;sup&amp;gt;-3&amp;lt;/sup&amp;gt;&#039;&#039;, reflecting its mere ~8-bit precision. Rounding in IEEE 754 by default uses &#039;&#039;&#039;round-to-nearest (ties to even)&#039;&#039;&#039;, which ensures the rounding error for any single operation is at most half a unit in the last place (0.5 ULP). We can model a floating-point addition as: &amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;\text{fl}(a+b) = (a+b)\,(1 + \delta),&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
where &#039;&#039;|δ| ≤ ε&#039;&#039; for that format. Here &#039;&#039;ε/2&#039;&#039; is the maximum &#039;&#039;&#039;relative error&#039;&#039;&#039; in one rounding (also called unit roundoff). For example, when adding two FP32 numbers, the result is accurate to within about &#039;&#039;10&amp;lt;sup&amp;gt;-7&amp;lt;/sup&amp;gt;&#039;&#039; of the true sum (relative), whereas in BF16, the result can deviate by up to &#039;&#039;∼7.8 × 10&amp;lt;sup&amp;gt;-3&amp;lt;/sup&amp;gt;&#039;&#039; (nearly 0.78% relative error per operation). Thus, &#039;&#039;&#039;each arithmetic operation incurs a small rounding error&#039;&#039;&#039; (on the order of machine epsilon) that can propagate or accumulate through a sequence of calculations. &amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Because floating-point representations have finite exponent range, we must also consider &#039;&#039;&#039;overflow&#039;&#039;&#039; and &#039;&#039;&#039;underflow&#039;&#039;&#039;. If a result’s magnitude exceeds the maximum representable value, it overflows to infinity; if it falls below the minimum normal (or minimum subnormal) value, it underflows to zero (or a denormalized tiny value). FP32 and BF16 share an exponent size (8 bits), so their range for normal numbers is roughly &#039;&#039;[10&amp;lt;sup&amp;gt;-38&amp;lt;/sup&amp;gt;, 10&amp;lt;sup&amp;gt;38&amp;lt;/sup&amp;gt;]&#039;&#039; (approx &amp;lt;math&amp;gt;~\!3.4\times10^{38}&amp;lt;/math&amp;gt; max). FP16’s 5-bit exponent allows max &#039;&#039;∼ 6.55 × 10&amp;lt;sup&amp;gt;4&amp;lt;/sup&amp;gt;&#039;&#039; and min normal &#039;&#039;∼ 6.10 × 10&amp;lt;sup&amp;gt;-5&amp;lt;/sup&amp;gt;&#039;&#039;. In practice, &#039;&#039;&#039;underflow/overflow&#039;&#039;&#039; can occur in low-precision arithmetic if values are not appropriately scaled. For instance, summing a large number of moderately sized terms in FP16 could overflow where FP32 would not. As an example, summing on the order of &#039;&#039;10&amp;lt;sup&amp;gt;4&amp;lt;/sup&amp;gt;&#039;&#039; values of magnitude ~&#039;&#039;10&amp;lt;sup&amp;gt;1&amp;lt;/sup&amp;gt;&#039;&#039; (say, adding 10,000 terms around 10 each) would produce a total &#039;&#039;∼10&amp;lt;sup&amp;gt;5&amp;lt;/sup&amp;gt;&#039;&#039;, which exceeds FP16’s max finite value ~&#039;&#039;6.5 × 10&amp;lt;sup&amp;gt;4&amp;lt;/sup&amp;gt;&#039;&#039; and would overflow to &#039;&#039;+∞&#039;&#039; in half precision. Underflow is usually a concern when subtracting nearly equal numbers (catastrophic cancellation yielding a tiny difference that might flush to zero in low precision). &amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 2.3. Accumulation Error in Reductions ===&lt;br /&gt;
&lt;br /&gt;
Floating-point addition is &#039;&#039;&#039;not associative&#039;&#039;&#039;: &#039;&#039;(a+b)+c&#039;&#039; can differ from &#039;&#039;a+(b+c)&#039;&#039; due to rounding, and thus the order in which we sum a list of numbers can change the result slightly. When summing a large sequence of &#039;&#039;N&#039;&#039; numbers &#039;&#039;x&amp;lt;sub&amp;gt;1&amp;lt;/sub&amp;gt;, x&amp;lt;sub&amp;gt;2&amp;lt;/sub&amp;gt;,..., x&amp;lt;sub&amp;gt;N&amp;lt;/sub&amp;gt;&#039;&#039;, the &#039;&#039;&#039;accumulated rounding error&#039;&#039;&#039; can be significant. In the &#039;&#039;worst case&#039;&#039; (e.g., adding many positive and negative terms that cause maximal cancellation or always rounding one way), the error can grow on the order of &#039;&#039;O(N ε)&#039;&#039; relative to the exact sum. Simply summing &#039;&#039;N&#039;&#039; numbers in sequence (the &#039;&#039;&#039;naive linear summation&#039;&#039;&#039;) has a worst-case error bound proportional to &amp;lt;math&amp;gt;N\,\varepsilon&amp;lt;/math&amp;gt;. This worst-case occurs for adversarial arrangements of values; in more typical scenarios with rounding errors of random signs, one can model the error as a random walk, yielding an expected root-mean-square (RMS) error growth on the order of &amp;lt;math&amp;gt;O(\sqrt{N}\,\varepsilon)&amp;lt;/math&amp;gt;. Still, with very large &#039;&#039;N&#039;&#039;, even &amp;lt;math&amp;gt;\sqrt{N}&amp;lt;/math&amp;gt; times &#039;&#039;ε&#039;&#039; can become large enough to matter. &amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Cancellation&#039;&#039;&#039; is a particular problem: if we add two nearly equal numbers of opposite sign, most leading digits cancel and the result loses significant bits of precision. For example, in single precision, subtracting two numbers that agree in the first 6 decimal digits will yield a result with only ~1 digit of accuracy left. Catastrophic cancellation can dramatically increase relative error. Summation is especially sensitive to cancellation if large positive and negative terms are summed naively. The &#039;&#039;&#039;condition number&#039;&#039;&#039; of a summation (the ratio of the sum of absolute values &amp;lt;math&amp;gt;\sum|x_i|&amp;lt;/math&amp;gt; to the absolute value of the true sum &amp;lt;math&amp;gt;|\sum x_i|&amp;lt;/math&amp;gt;) indicates how sensitive the result is to perturbations. If the numbers have mixed signs and nearly cancel out (making the condition number large), even an optimal summation order will have a large relative error. For instance, summing [1.0, &#039;&#039;10&amp;lt;sup&amp;gt;100&amp;lt;/sup&amp;gt;&#039;&#039;, 1.0, &#039;&#039;-10&amp;lt;sup&amp;gt;100&amp;lt;/sup&amp;gt;&#039;&#039;] in double precision has an exact sum of 2.0, but naive summation may yield 0.0 due to cancellation; an improved summation algorithm yields the correct 2.0. &amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
In practical terms, when summing a long list of floating-point numbers (such as accumulating a dot product or reducing a tensor), the &#039;&#039;&#039;accumulation error&#039;&#039;&#039; can become noticeable if no precautions are taken. Techniques to mitigate this error include carefully choosing the summation order or using higher precision for intermediate sums. We will explore these strategies in the next section.&lt;br /&gt;
&lt;br /&gt;
== Accumulators and Reduction Strategies ==&lt;br /&gt;
&lt;br /&gt;
When performing reductions (sums, dot products, etc.), the method of accumulation can greatly affect the accuracy of the result. Below, we discuss various strategies:&lt;br /&gt;
&lt;br /&gt;
=== 3.1. Naive Linear Accumulation ===&lt;br /&gt;
&lt;br /&gt;
The simplest accumulation strategy is to use a single accumulator and add each element sequentially. In C-like pseudocode:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;c&amp;quot;&amp;gt;&lt;br /&gt;
float sum = 0.0f;&lt;br /&gt;
for (int i = 0; i &amp;lt; N; ++i) {&lt;br /&gt;
    sum += array[i];&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This &#039;&#039;&#039;naive linear summation&#039;&#039;&#039; adds values one by one to the running total. While straightforward and fast, it is prone to the rounding error accumulation described earlier. The partial sum &amp;lt;code&amp;gt;sum&amp;lt;/code&amp;gt; continually grows (in absolute value) as terms are added, so new small terms may fall outside its precision. For example, if &amp;lt;code&amp;gt;sum&amp;lt;/code&amp;gt; is large and positive, adding a comparatively tiny positive number may produce no change because the small term&#039;s significant digits vanish in the presence of the large sum (the increment is below the rounding threshold of &amp;lt;code&amp;gt;sum&amp;lt;/code&amp;gt;). Conversely, adding a small number of opposite sign can suffer from cancellation. The order of addition matters: summing from smallest magnitude to largest (ascending order) tends to yield a more accurate result than summing in descending order, because adding small terms first preserves their contributions before the sum becomes very large. The naive loop above implicitly sums in the given index order; if the data is unsorted, this order is essentially arbitrary.&lt;br /&gt;
&lt;br /&gt;
The performance of linear accumulation is excellent (minimal overhead), and for many applications the resulting error is within acceptable limits. However, for &#039;&#039;&#039;very large N&#039;&#039;&#039; or ill-conditioned sums, the inaccuracy can become a problem. In a reduction involving millions of elements, a single-precision sum could lose several digits of accuracy. In deep learning, this could translate to small discrepancies in layer outputs or aggregated gradients if done naively in low precision.&lt;br /&gt;
&lt;br /&gt;
=== 3.2. Pairwise and Tree-Based Reductions ===&lt;br /&gt;
&lt;br /&gt;
A better approach to summation is &#039;&#039;&#039;pairwise summation&#039;&#039;&#039;, which sums numbers in a binary tree structure rather than linearly. The idea is to recursively split the array into halves, sum each half, then add the two partial sums. This can be implemented as:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;text&amp;quot;&amp;gt;&lt;br /&gt;
function pairwise_sum(x[1..n]):&lt;br /&gt;
    if n == 0:&lt;br /&gt;
        return 0&lt;br /&gt;
    if n == 1:&lt;br /&gt;
        return x[1]&lt;br /&gt;
    m = floor(n/2)&lt;br /&gt;
    left_sum  = pairwise_sum(x[1..m])&lt;br /&gt;
    right_sum = pairwise_sum(x[m+1..n])&lt;br /&gt;
    return left_sum + right_sum&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
By always adding numbers of similar magnitudes first (small subsets are reduced to subtotals, then those are added, etc.), pairwise summation avoids the situation of a very large running total absorbing small additions until the final steps. The error of pairwise summation grows logarithmically with &#039;&#039;N&#039;&#039; in the worst case (roughly &#039;&#039;O(ε log&amp;lt;sub&amp;gt;2&amp;lt;/sub&amp;gt; N)&#039;&#039;), instead of linearly. Intuitively, at each level of the tree, rounding error is constrained, and there are about &#039;&#039;log&amp;lt;sub&amp;gt;2&amp;lt;/sub&amp;gt; N&#039;&#039; levels. In fact, the worst-case relative error bound for pairwise summation (base case of 1 element) is: &amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;|E_N| \le \frac{\varepsilon \log_2 N}{1 - \varepsilon \log_2 N} \sum_{i=1}^N |x_i|,&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
which for practical &#039;&#039;N&#039;&#039; simplifies to on the order of &#039;&#039;ε log&amp;lt;sub&amp;gt;2&amp;lt;/sub&amp;gt; N&#039;&#039; times the condition number of the sum. In typical random-error scenarios, the error behaves even better (on the order of &amp;lt;math&amp;gt;\sqrt{\log N}&amp;lt;/math&amp;gt;). The key is that &#039;&#039;&#039;pairwise (tree) reduction balances the accumulations&#039;&#039;&#039;, preventing one sum from overwhelming the others too early. &amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
From a performance standpoint, pairwise summation has the same &#039;&#039;O(N)&#039;&#039; total additions as linear summation, just a different order. It is also highly amenable to parallelization: independent partial sums can be computed in parallel at each level of the tree. Many numerical libraries and BLAS implementations use a form of blocked or pairwise summation in dot products for improved accuracy at negligible performance cost. For example, NumPy and Julia perform partial pairwise summation by default for better numerical behavior. &amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 3.3. Compensated Summation (Kahan, Neumaier) ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Compensated summation&#039;&#039;&#039; is an alternative technique to improve summation accuracy by tracking the small “lost” quantities. The most famous algorithm is the &#039;&#039;&#039;Kahan summation algorithm&#039;&#039;&#039; (also called Kahan–Babuška). Kahan summation uses an extra variable to accumulate the round-off error. In pseudocode: &amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;c&amp;quot;&amp;gt;&lt;br /&gt;
double sum = 0.0;&lt;br /&gt;
double c = 0.0;    // compensation for lost low-order bits&lt;br /&gt;
for (i = 1; i &amp;lt;= N; ++i) {&lt;br /&gt;
    double y = x[i] - c;    // recover low-order bits by subtracting compensation&lt;br /&gt;
    double t = sum + y;     // perform the addition&lt;br /&gt;
    c = (t - sum) - y;      // compute new compensation (the error in t)&lt;br /&gt;
    sum = t;&lt;br /&gt;
}&lt;br /&gt;
return sum;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Here &amp;lt;code&amp;gt;c&amp;lt;/code&amp;gt; accumulates the tiny leftovers that would otherwise be dropped. Each iteration essentially subtracts the last error from the new addend (&amp;lt;code&amp;gt;y = x[i] - c&amp;lt;/code&amp;gt;) so that &amp;lt;code&amp;gt;t = sum + y&amp;lt;/code&amp;gt; includes the small bits from the previous operation. The new error is then stored in &amp;lt;code&amp;gt;c&amp;lt;/code&amp;gt; for the next iteration. The result is that the final &amp;lt;code&amp;gt;sum&amp;lt;/code&amp;gt; is as if computed with higher precision than the machine’s: Kahan’s algorithm can &#039;&#039;&#039;greatly reduce the error&#039;&#039;&#039; in summing a sequence of floats. In fact, with exact arithmetic for the compensation, Kahan summation yields a result accurate to within 1 or 2 ULP of the true sum, regardless of N (assuming the sequence of partial sums doesn’t overflow). More precisely, Kahan summation achieves a &#039;&#039;&#039;worst-case error independent of N&#039;&#039;&#039; (error bound depends only on machine precision, not the number of terms). In practice, using a double accumulator for summing single-precision numbers via Kahan can give near double-precision accuracy. &amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
An important subtlety is that the compensation variable &amp;lt;code&amp;gt;c&amp;lt;/code&amp;gt; should be kept in &#039;&#039;full precision&#039;&#039;. In the implementation above, we used &amp;lt;code&amp;gt;double&amp;lt;/code&amp;gt; for both sum and c. If summing FP32 values, one might use &amp;lt;code&amp;gt;float sum&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;float c&amp;lt;/code&amp;gt; (both 32-bit), which still improves accuracy, but using a higher precision for the accumulator and compensation (i.e. double) yields even better results. Many implementations of Kahan summation use the same type for sum and c, but it&#039;s assumed to be higher precision than the inputs when possible.&lt;br /&gt;
&lt;br /&gt;
Kahan’s algorithm carries a performance cost: each iteration does five arithmetic operations (two adds, one subtract, etc.) and has a data dependency, which can hinder parallelism or pipelining. Thus, it runs slower than naive summation (roughly 2–3× slower or more, depending on CPU pipeline). In loops where summation is a bottleneck, this may be significant. Nonetheless, for modest N or where maximum accuracy is required, compensated summation is extremely useful. In fact, Python’s &amp;lt;code&amp;gt;math.fsum&amp;lt;/code&amp;gt; function uses a variant of Kahan’s algorithm (actually a more advanced multi-part compensation) to sum iterables with high precision. &amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
There are variations and improvements on basic Kahan summation. &#039;&#039;&#039;Neumaier’s algorithm&#039;&#039;&#039; (sometimes called &#039;&#039;Kahan-Babuška-Neumaier&#039;&#039;) improves Kahan’s method by also handling cases where the next input is larger in magnitude than the running sum. In such cases, the original Kahan formula could suffer error; Neumaier’s version effectively swaps roles when a huge value comes in. Its pseudocode is: &amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;text&amp;quot;&amp;gt;&lt;br /&gt;
function NeumaierSum(array):&lt;br /&gt;
    sum = 0.0&lt;br /&gt;
    compensation = 0.0&lt;br /&gt;
    for each x in array:&lt;br /&gt;
        t = sum + x&lt;br /&gt;
        if (fabs(sum) &amp;gt;= fabs(x))&lt;br /&gt;
            compensation += (sum - t) + x;&lt;br /&gt;
        else&lt;br /&gt;
            compensation += (x - t) + sum;&lt;br /&gt;
        sum = t;&lt;br /&gt;
    return sum + compensation;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This algorithm ensures the compensation covers both scenarios: when the sum is the bigger addend or when the new term is bigger. In the end, it adds the final &amp;lt;code&amp;gt;compensation&amp;lt;/code&amp;gt; once to the result. Neumaier’s method can produce the correct result in cases where basic Kahan fails. For instance, summing [1.0, &#039;&#039;+10&amp;lt;sup&amp;gt;100&amp;lt;/sup&amp;gt;&#039;&#039;, 1.0, &#039;&#039;-10&amp;lt;sup&amp;gt;100&amp;lt;/sup&amp;gt;&#039;&#039;] in double precision: regular Kahan summation might give 0.0 (losing the small terms), whereas Neumaier’s algorithm yields 2.0 exactly. There are also higher-order compensation methods (carrying multiple compensation terms, e.g., the &#039;&#039;&#039;Klein summation&#039;&#039;&#039; variant or using arbitrary precision for intermediate sums) that further reduce error at increased computational cost. &amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
In summary, compensated summation algorithms trade extra computation for greatly improved numerical accuracy. They are especially valuable when summing large arrays of numbers with varying magnitudes, where naive summation could lose most of the low-order bits. Many scientific computing environments provide compensated sum routines for this reason. In deep learning, however, a different approach to improved precision is often taken: &#039;&#039;&#039;using a wider accumulator type&#039;&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
=== 3.4. Mixed-Precision Accumulation ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Mixed-precision accumulation&#039;&#039;&#039; refers to performing arithmetic in a higher precision than the input data. A common scenario is accumulating sums in 32-bit floats when the operands are 16-bit floats. Rather than implementing a compensation algorithm, one can simply use a wider accumulator so that intermediate rounding error is smaller. This is effectively what modern GPUs do for tensor operations: for example, NVIDIA &#039;&#039;&#039;Tensor Cores operate on FP16 input data with an FP32 accumulator&#039;&#039;&#039; (the 16-bit products are accumulated in 32-bit). In this way, summing 2048 products in a dot product has far less precision loss than it would with a 16-bit accumulator. Hardware that supports mixed precision can give the best of both worlds: fast low-precision multiplies and high-precision accumulation. &amp;lt;ref name=&amp;quot;ref6&amp;quot;/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Software can also leverage mixed precision. For instance, one can sum an array of &amp;lt;code&amp;gt;float&amp;lt;/code&amp;gt; values into a &amp;lt;code&amp;gt;double&amp;lt;/code&amp;gt; variable, accumulating the result in double precision and then converting back to float at the end:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;c&amp;quot;&amp;gt;&lt;br /&gt;
double sum = 0.0;&lt;br /&gt;
for (int i=0; i&amp;lt;N; ++i) {&lt;br /&gt;
    sum += (double) arr_float[i];&lt;br /&gt;
}&lt;br /&gt;
float result = (float) sum;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This approach often yields a more accurate &amp;lt;code&amp;gt;result&amp;lt;/code&amp;gt; than summing in single precision directly, because the intermediate sum carries 52-bit precision. The final rounding to float happens only once at the end (minimizing overall rounding error). In fact, the above is one way to implement &amp;lt;code&amp;gt;math.fsum&amp;lt;/code&amp;gt; in Python (it internally does something similar with multiple partial sums in double precision).&lt;br /&gt;
&lt;br /&gt;
In machine learning training and inference, &#039;&#039;&#039;mixed precision&#039;&#039;&#039; is widely used. When using FP16 for speed, it&#039;s common to accumulate with FP32. For example, &#039;&#039;&#039;Google&#039;s TPUs use BFloat16 for multiplication but accumulate in FP32&#039;&#039;&#039; for matrix ops. AWS’s Inferentia (Neuron) hardware similarly supports FP16/BF16 matrix multiplication with FP32 accumulation. The motivation is that summing hundreds or thousands of half-precision values would otherwise introduce intolerable error or risk overflow. With an FP32 accumulator, the reduction error is drastically reduced (by about &#039;&#039;2&amp;lt;sup&amp;gt;13&amp;lt;/sup&amp;gt;&#039;&#039; times, since FP32 has 13 more precision bits than FP16). Mixed-precision accumulation thus provides a simple and effective form of compensation: &#039;&#039;&#039;use a larger bucket to collect many small contributions&#039;&#039;&#039;. &amp;lt;ref name=&amp;quot;ref6&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref7&amp;quot;/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
There is a spectrum of options: summing int8 or int16 integers might use 32-bit integers as accumulators for the same reason (to avoid overflow). Some BLAS libraries even offer &#039;&#039;extended precision accumulators&#039;&#039; for single precision dot products (accumulating in double). The downside of mixed precision is the extra memory and maybe throughput cost of the wider type. But on many platforms, the add latency is dominated by hardware design rather than bit-width, so using a 32-bit adder vs 16-bit has negligible speed impact if supported natively. The key is whether data movement or register pressure increases.&lt;br /&gt;
&lt;br /&gt;
In summary, &#039;&#039;&#039;using a higher precision accumulator is often the easiest way to improve numerical accuracy&#039;&#039;&#039; for reductions. It is a primary strategy in deep learning hardware to maintain stability while exploiting low precision for storage and initial computations.&lt;br /&gt;
&lt;br /&gt;
== Precision Formats in AI and Machine Learning ==&lt;br /&gt;
&lt;br /&gt;
Modern AI systems use a variety of numeric formats to balance speed, memory, and accuracy. We discuss common precisions and their roles:&lt;br /&gt;
&lt;br /&gt;
=== 4.1. FP32 and FP64 in Training and Inference ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;FP32 (single precision)&#039;&#039;&#039; has been the workhorse of deep learning for years. Most training scripts in frameworks like TensorFlow and PyTorch used FP32 by default, as it offers sufficient precision to propagate gradients and accumulate weight updates over millions of iterations without excessive error. With ~7 decimal digits of precision and a wide exponent range, FP32 can handle the dynamic range of activations and gradients in typical neural networks. In inference, FP32 is often more precision than strictly necessary for final predictions, but it has remained common because of its ease and the fact that many GPU operations were optimized for FP32.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;FP64 (double precision)&#039;&#039;&#039; is rarely used in deep learning model training or inference, except in specific scenarios requiring extreme precision. FP64 is common in scientific computing and certain types of numerical simulations. In neural networks, however, the extra precision is usually not needed for model accuracy—using doubles tends to yield negligible improvements in model metrics but at a large performance cost (since on GPUs FP64 throughput can be 1/2, 1/8, or even 1/32 of FP32 throughput depending on hardware). One exception might be when integrating neural nets into larger physics simulations or when solving ill-conditioned problems as part of the model. Some researchers use FP64 to diagnose numerical issues or ensure reproducibility. Generally though, deep learning has favored lower precision for efficiency, and FP64 is considered overkill for most networks. &amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
That said, certain classical machine learning algorithms benefit from double precision. For example, computing the inverse of a matrix (as in a Gaussian Process regression or Kalman filter embedded in a model) can incur large rounding errors in single precision, so using FP64 can significantly improve the result. The ONNX standard supports a float64 tensor type for such needs. In ONNX Runtime, however, emphasis has been on FP32 performance, and some pipelines default to float even if the original model was double. If a model’s prediction computation involves operations that are numerically sensitive (like matrix inversion, Cholesky decomposition, or summing extremely large counts), then FP64 may be required to avoid noticeable discrepancies. A good practice is to test a model in both float and double if you suspect precision issues; significant differences indicate an ill-conditioned calculation that might need higher precision. &amp;lt;ref name=&amp;quot;ref1&amp;quot;/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
In summary, FP32 remains the &#039;&#039;&#039;default&#039;&#039;&#039; format for most training and inference due to its balance of accuracy and efficiency, while FP64 is reserved for niche cases or precision-critical subroutines. FP32 typically achieves the same accuracy as FP64 for end-to-end deep learning tasks, because network training can often compensate for small noise, and other sources of error (like generalization error) dominate. Therefore, the community trend has been moving to &#039;&#039;&#039;lower&#039;&#039;&#039; precisions, not higher, to gain speed — provided the models still converge and perform well. &amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 4.2. FP16 and BFloat16 in Deep Learning ===&lt;br /&gt;
&lt;br /&gt;
To further speed up neural network training and inference, &#039;&#039;&#039;half-precision (FP16)&#039;&#039;&#039; was adopted, especially with the advent of GPUs supporting FP16 math at twice the rate of FP32. Using FP16 reduces memory storage and bandwidth by 50% compared to FP32, which is significant for large models. However, FP16’s small exponent range and precision posed challenges: training a large network purely in FP16 can fail due to gradient underflow/overflow or weight updates not registering (because changes are below the 0.001 quantization step). To address this, frameworks introduced &#039;&#039;&#039;mixed-precision training&#039;&#039;&#039;, where compute-intensive parts (matrix multiplies, convolutions) run in FP16, but certain accumulations and model parameters remain in FP32. For instance, NVIDIA’s &#039;&#039;&#039;Tensor Core&#039;&#039;&#039; units require FP16 (or BF16) inputs but accumulate results in FP32, effectively ensuring that reductions are done in higher precision. Research showed that with proper loss scaling and mixed precision, models can train to FP32-level accuracy using mostly FP16 arithmetic, achieving significant speedups. &amp;lt;ref name=&amp;quot;ref6&amp;quot;/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;BFloat16 (BF16)&#039;&#039;&#039; emerged as an alternative 16-bit format primarily from Google’s TPU training systems. BF16 has a larger exponent (8 bits, same as FP32) but only 7 fraction bits. This means BF16 can handle the &#039;&#039;&#039;same dynamic range&#039;&#039;&#039; as FP32 (no overflows where FP32 wouldn’t) but at the cost of even lower precision than FP16 (BF16 has about 2–3 decimal digits of precision vs FP16’s ~3–4). Despite its coarse precision, BF16 turns out to be effective for training deep networks because the algorithms (stochastic gradient descent, etc.) are somewhat robust to that noise, and having the wide range prevents catastrophic loss of signal (no gradient will overflow to Inf unless it was going to in FP32 anyway). Many training frameworks (PyTorch, TensorFlow) support BF16 training on hardware like &#039;&#039;&#039;TPU, NVIDIA A100+, Intel CPUs with AVX-512 BF16, etc&#039;&#039;&#039;. Typically, when using BF16, &#039;&#039;&#039;accumulations are still done in FP32&#039;&#039;&#039; (for example, a TPU v2/v3 multiplies BF16 matrices and accumulates into an FP32 result for the dot product). &amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref6&amp;quot;/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
In inference, both FP16 and BF16 are used to deploy models for faster speed or lower memory. FP16 is popular on NVIDIA GPUs (which have specialized FP16 units). BF16 is advantageous on TPUs and some CPUs that support it, and increasingly on GPUs (Ampere and newer GPUs also support BF16 arithmetic). The choice between FP16 and BF16 can depend on hardware: BF16’s advantage is easier software conversion (you can often take an FP32-trained model and cast weights to BF16 with minimal loss, since the dynamic range is intact), whereas FP16 might require adjusting the model or ensuring values fit in range (sometimes needing techniques like maintaining a master copy of weights in FP32 during training, or handling out-of-range values).&lt;br /&gt;
&lt;br /&gt;
For ONNX, both Float16 and BFloat16 types are part of the standard. ONNX models can be converted to FP16 (truncating weights and inserting cast nodes for computations) using tools like &amp;lt;code&amp;gt;float16_converter&amp;lt;/code&amp;gt; in ONNX Runtime. ONNX Runtime on GPUs will typically execute those FP16 ops with mixed precision on tensor cores (FP16 input, FP32 accumulate), whereas on CPU it may emulate FP16 or upcast to FP32 (as many CPU implementations do not natively support half-precision math). In fact, as an example, running an ONNX model in FP16 on a CPU might insert hidden Cast operations to FP32 behind the scenes, because the CPU kernel for that op only exists in FP32. This ensures correctness (the computation is done in higher precision) but can reduce performance. We will revisit such backend-dependent behavior in Section 5.3. &amp;lt;ref name=&amp;quot;ref8&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
In summary, &#039;&#039;&#039;FP16 and BF16&#039;&#039;&#039; are now established as important low-precision formats in deep learning. FP16 offers more precision bits, which can matter for e.g. small differences, but has a limited range (care needed to avoid overflow). BF16 offers safety in range but less precision (which can slightly affect convergence or final accuracy in some cases). Both typically rely on FP32 for accumulation to achieve acceptable accuracy. When deploying models, one should choose a format supported efficiently by the target hardware and verify that the accuracy is not significantly degraded. Often, CNNs and transformers can run in FP16/BF16 with virtually no drop in score, but certain operations (like softmax, see Section 6.3) might need special handling.&lt;br /&gt;
&lt;br /&gt;
=== 4.3. FP8 Formats (E4M3, E5M2) and Scaling ===&lt;br /&gt;
&lt;br /&gt;
The push for ever lower precision has led to experimental &#039;&#039;&#039;8-bit floating point (FP8)&#039;&#039;&#039; formats for deep learning. In 2022, researchers from NVIDIA, Intel, and Arm proposed an FP8 standard with two variants: &#039;&#039;&#039;E4M3&#039;&#039;&#039; (4 exponent bits, 3 fraction bits) and &#039;&#039;&#039;E5M2&#039;&#039;&#039; (5 exponent bits, 2 fraction bits). Both have 1 sign bit, for a total of 8 bits. E5M2 has a wider exponent range (5 bits gives bias 15) but one fewer mantissa bit than E4M3. The idea is to use E4M3 for numbers that need more precision (like weights or activations around 1.0) and E5M2 for numbers that need more dynamic range (like gradient updates which can be very small or large). In fact, the study found that using E4M3 for forward activations/weights and E5M2 for backward gradients can allow training of large networks with FP8 to accuracy on par with FP16 training. &amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref11&amp;quot;/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
FP8 formats are &#039;&#039;not&#039;&#039; IEEE 754 standard (though IEEE is considering lower precision in its 2019 revision). Notably, the E4M3 format as used by NVIDIA does not allocate any bit-pattern for &#039;&#039;+∞&#039;&#039; or &#039;&#039;-∞&#039;&#039; – it has only NaN for invalid results, using that freed bit pattern to extend the representable range by one exponent value. Essentially, E4M3 can represent numbers up to ~&#039;&#039;2&amp;lt;sup&amp;gt;15&amp;lt;/sup&amp;gt;&#039;&#039; (around 3e4) instead of capping at infinity at &#039;&#039;2&amp;lt;sup&amp;gt;16&amp;lt;/sup&amp;gt;&#039;&#039;, by foregoing infinities. Variations also exist regarding negative zero and how NaNs are handled. For instance, GraphCore’s IPU uses FP8 formats that disallow negative zero and infinities, to maximize useable range. &amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Using 8-bit floats for neural networks requires additional techniques, chiefly &#039;&#039;&#039;scaling&#039;&#039;&#039;. Because 8-bit has very limited precision and range, one typically applies a power-of-two scale factor to groups of values to normalize them into an FP8-friendly range. This is akin to block floating-point or “microscaling” per layer or per tensor. For example, one might keep a FP32 scale for each channel or tensor, and represent values as &#039;&#039;(FP8_value) × 2&amp;lt;sup&amp;gt;scale&amp;lt;/sup&amp;gt;&#039;&#039;. The ONNX standard has introduced support for FP8 in version 1.15, including types for E4M3 and E5M2 (with variations FNUZ: finite-only and no negative zero), and even a type called &#039;&#039;&#039;E8M0&#039;&#039;&#039; which is an 8-bit exponent-only number used purely as a scale factor. The E8M0 type has 8 exponent bits, no mantissa, effectively representing powers of two from &#039;&#039;2&amp;lt;sup&amp;gt;-127&amp;lt;/sup&amp;gt;&#039;&#039; to &#039;&#039;2&amp;lt;sup&amp;gt;128&amp;lt;/sup&amp;gt;&#039;&#039; for scaling purposes. This indicates an approach where an FP8 tensor would be accompanied by an E8M0 scale (or scales) to interpret its value correctly. &amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Rounding&#039;&#039;&#039; becomes critical when casting values to FP8. The quantization error is much larger relative to FP16 or FP32. Studies have shown that choosing the right rounding mode (e.g., round-to-nearest, stochastic rounding, etc.) and clamping behavior (saturating to max instead of inf) can affect model accuracy. ONNX added a &amp;lt;code&amp;gt;round_mode&amp;lt;/code&amp;gt; attribute to its Cast operator to allow controlling this for FP8. In particular, one paper found that &#039;&#039;&#039;rounding up with saturation&#039;&#039;&#039; (round-to-nearest ties away from 0, and clamp overflows to max finite rather than inf) gave better accuracy in large language model training than other rounding modes. &amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
At present, FP8 is on the cutting edge. NVIDIA’s H100 GPU supports FP8 tensor cores (for both E4M3 and E5M2), and early results show minimal accuracy loss on some vision and NLP tasks when properly tuned. However, using FP8 requires fine-tuning hyperparameters like scaling factors for each layer and perhaps tweaking training procedures. ONNX’s inclusion of FP8 types means you can represent and transport models using FP8 weights or activations, but actual hardware support is limited and typically paired with calibration or dynamic scaling logic outside of ONNX graph (or using custom ONNX functions for scaling). It remains an &#039;&#039;&#039;open research area&#039;&#039;&#039; how to fully automate FP8 quantization for arbitrary models without accuracy loss.&lt;br /&gt;
&lt;br /&gt;
In summary, FP8 formats promise further compression of neural network computation and data. They are an order of magnitude more error-prone than FP16, but when combined with careful scaling, mixed usage of E4M3/E5M2, and maybe modified training techniques, they have shown surprising success. ONNX is actively evolving to support these ultra-low precision types, signaling their potential importance in future AI inference deployments.&lt;br /&gt;
&lt;br /&gt;
== Accumulation Semantics in ONNX ==&lt;br /&gt;
&lt;br /&gt;
One of the roles of the ONNX specification is to define the mathematical behavior of operators (Conv, MatMul, Sum, etc.) in a framework-agnostic way. However, ONNX mostly defines &#039;&#039;what&#039;&#039; an operator computes in exact arithmetic, not &#039;&#039;how&#039;&#039; the computation is carried out in finite precision. This leaves certain details – like accumulator precision or summation order – up to the implementation (runtime or hardware). We&#039;ll examine how ONNX treats accumulation and what guarantees (or lack thereof) are provided.&lt;br /&gt;
&lt;br /&gt;
=== 5.1. Operator Definitions and Numerical Guarantees ===&lt;br /&gt;
&lt;br /&gt;
Each ONNX operator is described as performing an ideal mathematical function on its inputs. For example, &amp;lt;code&amp;gt;MatMul&amp;lt;/code&amp;gt; is defined as the standard matrix product of two tensors, and &amp;lt;code&amp;gt;ReduceSum&amp;lt;/code&amp;gt; computes a summation along given axes. The ONNX spec, as of current versions, &#039;&#039;&#039;does not mandate the precision of intermediate calculations&#039;&#039;&#039; beyond the types of the inputs and outputs. If an operator’s inputs and outputs are float16, the spec implies the computation is done in float16 in a sense, but it does not forbid an implementation from using higher precision internally – it simply doesn&#039;t mention it. There is &#039;&#039;&#039;no explicit guarantee of identical bit-for-bit results&#039;&#039;&#039; across different ONNX backends for floating-point ops. Instead, results are expected to be numerically close, within tolerances one would normally expect from floating-point differences (due to different hardware or algorithms). The ONNX Backend Test suite typically uses a relative or absolute error tolerance when validating outputs for this reason.&lt;br /&gt;
&lt;br /&gt;
Because of this, important details like the use of extended precision or accumulation in higher precision are left ambiguous. This has led to discussions in the ONNX community about making such behavior more explicit. For example, a proposal was raised to allow specification of accumulation precision for certain ops like MatMul. The reasoning is that as lower-precision types (float16, bfloat16, float8) become more common, knowing or controlling whether an op internally accumulates in a higher precision is important for both accuracy and reproducibility. In June 2025, an issue on the ONNX GitHub suggested adding an attribute to MatMul (and similarly Gemm, Softmax, etc.) to specify the accumulator type. By default it would use input precision (status quo), but an attribute could request (for instance) 32-bit accumulation even if inputs are 16-bit. As of this writing, however, no such attribute exists in the official spec – it&#039;s an open issue. Thus, the numerical semantics are effectively: &#039;&#039;“compute the result as if in the same type as inputs, but actual implementations may use more precision internally as an optimization.”&#039;&#039; &amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
From a guarantee standpoint, ONNX operators promise the mathematically correct result assuming real-number arithmetic, but they &#039;&#039;&#039;do not guarantee bit-exact results&#039;&#039;&#039; due to floating-point effects. There is an understanding that differences can arise from different summation orders, fused operations (e.g., an implementation might use an FMA (fused multiply-add) which has one rounding instead of two), or use of extended precision registers. ONNX does not formally define rounding modes either – it relies on IEEE 754 defaults, but if a backend used something like deterministic summation or alternative rounding, ONNX has no mechanism to express that.&lt;br /&gt;
&lt;br /&gt;
In summary, ONNX&#039;s current stance is largely &#039;&#039;&#039;implementation-dependent&#039;&#039;&#039; for numerical precision. It provides minimal explicit numerical guarantees beyond type constraints. This is why an ONNX model run on two different hardware or runtime libraries can have slightly different results, especially in reduced precision. The burden is on backends to produce results &amp;quot;close enough&amp;quot; to each other that they don&#039;t affect the model&#039;s decisions in any significant way.&lt;br /&gt;
&lt;br /&gt;
=== 5.2. MatMul, Conv, and Reduction Operators ===&lt;br /&gt;
&lt;br /&gt;
Consider ONNX &amp;lt;code&amp;gt;MatMul&amp;lt;/code&amp;gt;: it takes two tensors (often two matrices) and produces their matrix product. In pure math, &amp;lt;math&amp;gt;C_{ij} = \sum_k A_{ik} &#039;&#039; B_{kj}&amp;lt;/math&amp;gt;. If A and B are float16, should that sum be computed in float16 or float32? As discussed, ONNX doesn’t specify – it simply says the output is a float16 matrix containing the product. In practice, many backends will &#039;&#039;promote&#039;&#039; the computation: for example, NVIDIA GPUs will do the multiplication and addition in a mixed FP16/FP32 mode by default (because of tensor cores), then convert the final result back to float16. ONNX Runtime on CPU might not support float16 multiplication natively, so it will insert cast ops to float32, perform the MatMul in float32, then cast back to float16. From ONNX’s perspective, as long as the final result is within acceptable error of the “ideal” float16 computation, it’s fine. But note, an “ideal” float16 computation (where every multiply and add is rounded to FP16) might actually be &#039;&#039;less accurate* than what the backend did with FP32. This means that a backend using higher precision can produce a result that is not bit-equal to a pure FP16 implementation – it&#039;s actually closer to the true real number result. This is usually considered a good thing (improved accuracy), but it does highlight a lack of consistency: another backend might do everything in pure FP16 and get a slightly different answer. &amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The same situation occurs for &#039;&#039;&#039;convolution&#039;&#039;&#039; (&amp;lt;code&amp;gt;Conv&amp;lt;/code&amp;gt; op) which is essentially a batched sum of products (like MatMul in sliding window), and for explicit reduction ops like &amp;lt;code&amp;gt;ReduceSum&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;ReduceMean&amp;lt;/code&amp;gt;, etc. If you have an ONNX &amp;lt;code&amp;gt;ReduceSum&amp;lt;/code&amp;gt; that sums 1000 float16 values, the spec doesn’t say how to sum them. A GPU kernel might sum in a tree using FP32 accumulators; a naive implementation might sum in order with FP16 and possibly overflow or underflow intermediate steps. Both are allowed as long as they output a float16 result that is “reasonable.” In practice, ONNX Runtime and others take care to avoid obviously bad behavior (like overflow) by using higher precision or by splitting the sum. But again, it’s not mandated by the ONNX standard explicitly.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;\mathrm{Softmax}(x)_i = \frac{\exp(x_i)}{\sum_j \exp(x_j)},&amp;lt;/math&amp;gt;&lt;br /&gt;
typically applied along a specified axis. Directly computing this in float16 can be problematic for large vectors, as discussed in Section 6.3. Indeed, the issue of accumulation precision arises: should the sum &amp;lt;math&amp;gt;\sum_j \exp(x_j)&amp;lt;/math&amp;gt; be done in FP16 or can it be in FP32? Many implementations will do it in FP32 even if &#039;&#039;x&#039;&#039; is FP16, because the dynamic range of &amp;lt;math&amp;gt;\exp(x)&amp;lt;/math&amp;gt; is large. The ONNX Softmax spec doesn’t detail this, but it expects the result to be a proper probability distribution (summing to 1.0 within normal floating error). A pure FP16 softmax on, say, 1000 elements could sum to something like 0.996 or 1.004 due to rounding, whereas FP32 accumulation would be much closer to 1.000. ONNX likely allows both; the small difference is usually inconsequential, but if it were large (say the FP16 sum overflowed to inf), that would be a correctness issue. A robust backend will prevent that (by using the common max-subtraction trick and higher precision accumulation). The ONNX issue 7072 indeed calls out Softmax as another op where explicit control of accumulation precision would be useful. &amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
In summary, &#039;&#039;&#039;MatMul, Conv, and reduction ops&#039;&#039;&#039; in ONNX are defined mathematically but with &#039;&#039;implicit&#039;&#039; numerical behavior. Implementations commonly use higher precision for intermediate sums especially when input is low precision, but this is not standardized. Consequently, results can differ slightly between implementations. For critical applications, one must be aware of these differences. A model exported to ONNX with float16 weights might perform slightly differently on one runtime vs another if one does all float16 math and the other uses float32 accumulations. Both conform to ONNX. As a user, if you require a certain precision for accumulation, currently you have to ensure it by the way you export or run the model (for example, insert manual cast nodes in the ONNX graph to force certain ops to FP32). We may see future ONNX versions address this with attributes or metadata as mentioned, but for now these ops are effectively “compute with best effort accuracy”.&lt;br /&gt;
&lt;br /&gt;
=== 5.3. Backend and Hardware-Dependent Behavior ===&lt;br /&gt;
&lt;br /&gt;
Because ONNX is implemented by many backends (ONNX Runtime, TensorRT, CoreML, PyTorch JIT, etc.), and these in turn run on varied hardware (CPU, GPU, accelerators), the numeric behavior can be backend-dependent. Some examples:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;CPU backends:&#039;&#039;&#039; Many CPUs do not have native float16 vector arithmetic (with a few exceptions in recent CPUs). ONNX Runtime’s default CPU execution provider will often upcast float16 to float32, compute, then downcast. This ensures accuracy but means that if you thought you were getting a full float16 pipeline, you’re not – the CPU is essentially ignoring the low precision in compute. On the other hand, x86 CPUs have extended 80-bit precision in x87 registers (though less used now) and 64-bit SIMD registers for doubles; some compiler optimizations might carry intermediate results in higher precision. This could make CPU results slightly different from a GPU’s if, say, a sum is done in 80-bit on CPU versus 32-bit on GPU. Generally, modern compilers avoid such extended precision unless explicitly asked, to improve reproducibility. &amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;GPU backends:&#039;&#039;&#039; GPUs often perform summations in parallel using tree reductions. The order of operations can change depending on how threads are scheduled. This means even on the same GPU, summing an array in one launch versus another might not give bit-identical results if the internal parallel reduction algorithm differs (though it should be deterministically the same given the same library version and input, but if you change GPU model or BLAS library, it might differ). Also, GPUs have fused multiply-add (FMA) instructions that do &#039;&#039;(a*b + c)&#039;&#039; in one step with one rounding, which can produce slightly different results than doing the multiply and add separately (which would round twice). If an ONNX backend uses a fused kernel (like a GEMM library) it might yield different low-order bits than a simple loop.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Vendor libraries:&#039;&#039;&#039; ONNX Runtime can use vendor BLAS or DNN libraries (MKL, cuDNN, TensorRT, etc.). These libraries often implement their own strategies for numerical stability. For example, NVIDIA’s cuDNN might use FP32 accumulation for FP16 inputs by default. TensorRT (NVIDIA’s inference engine) will sometimes automatically promote precisions or insert scaling to maintain stability. On the other hand, if you explicitly request FP16 in TensorRT with no FP32 fallback, it might do the entire op in FP16 and you’d see more error. Some libraries offer knobs: TensorRT allows choosing FP32, FP16, or INT8 precision for each layer, and will use FP32 internally for safety in some cases unless forced otherwise. &amp;lt;ref name=&amp;quot;ref13&amp;quot;/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Hardware-specific quirks:&#039;&#039;&#039; As another example, NVIDIA’s &#039;&#039;&#039;TensorFloat-32 (TF32)&#039;&#039;&#039; on Ampere GPUs is a precision mode where matrix multiplications use 10-bit mantissa (so about FP16 precision for the multiply) but keep 8-bit exponent (range like FP32). By default, CUDA libraries use TF32 for FP32 matrix multiplies on Ampere to speed them up, unless you disable it. This means if you run an ONNX model with FP32 on an A100 GPU, your matmuls might actually be lower precision than FP32 (though higher than FP16). It’s an example of hardware choosing an intermediate precision for performance. From ONNX’s perspective, the output is still FP32 and (hopefully) within error tolerances, but a user might be surprised that it’s not a “true” FP32 computation internally. If exactness is needed, one has to turn off TF32. &amp;lt;ref name=&amp;quot;ref6&amp;quot;/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Non-determinism:&#039;&#039;&#039; Some backends (especially on GPUs) allow non-deterministic execution for performance. For instance, atomic adds in parallel reductions can produce results that vary run-to-run because the order of accumulation is race-condition-dependent. ONNX’s spec doesn’t cover this explicitly, but it’s something to consider. In ONNX Runtime, there are settings for deterministic computing, but by default it may use all available performance optimizations, even if results vary at the 1e-7 level.&lt;br /&gt;
&lt;br /&gt;
In short, the numerical behavior under ONNX can depend on the combination of backend software and hardware capabilities. The lack of a strict spec for accumulation means implementations have freedom to use clever strategies (higher precision, reordering) to improve accuracy or speed. Usually this is positive (e.g. getting better accuracy than a naive approach). But it could also lead to slight inconsistencies. For example, you might find that exporting a PyTorch model to ONNX and running it with one engine yields outputs slightly different from PyTorch’s – not because ONNX is wrong, but because the summation or convolution was done differently (maybe PyTorch summed in FP32 as well, but maybe not in exactly the same way).&lt;br /&gt;
&lt;br /&gt;
The key takeaway is that ONNX &#039;&#039;by design&#039;&#039; abstracts away these details, which is good for portability but means you need to &#039;&#039;&#039;trust the backend&#039;&#039;&#039; or verify its precision behavior for sensitive models. If necessary, you can enforce certain behavior by inserting ops (for example, if you want to ensure FP32 accumulation, you could insert a cast to FP32 for inputs of a MatMul and cast the result back to FP16 – this way the ONNX graph itself mandates FP32 compute in between). This of course sacrifices some performance or device-specific magic.&lt;br /&gt;
&lt;br /&gt;
Looking forward, as mentioned, there are proposals to enrich ONNX’s specification regarding precision. This might include explicit flags for “exact summation” or “accumulate in int32” etc., which would make models more self-descriptive but also potentially less portable (if a backend doesn’t support that, what then?). For now, users should be aware of these backend differences. Most of the time, well-engineered ONNX runtimes yield results close enough that they don&#039;t affect the application (e.g., classification top-1 remains the same even if low-level sums differ by 1e-5). But for high-stakes numerical computations, testing and maybe adjusting the model or backend settings is prudent.&lt;br /&gt;
&lt;br /&gt;
== Numerical Stability in Deep Learning Models ==&lt;br /&gt;
&lt;br /&gt;
Deep learning computations involve many operations that, while theoretically well-defined, can be sensitive to numerical issues when implemented in finite precision. We&#039;ll discuss a few common scenarios in neural networks where numerical stability is a concern, and how precision and accumulation play a role.&lt;br /&gt;
&lt;br /&gt;
=== 6.1. Dot Products and Large-Scale Reductions ===&lt;br /&gt;
&lt;br /&gt;
Neural networks are full of dot products: every fully-connected layer computes &amp;lt;math&amp;gt;\mathbf{y} = W\mathbf{x}&amp;lt;/math&amp;gt;, which is a series of inner products between weight vectors and the input. Convolutional layers compute weighted sums of local regions. Attention mechanisms compute large matrix products of query/key/value matrices. All these boil down to summing a lot of terms: &amp;lt;math&amp;gt;\sum_{k=1}^K a_k b_k&amp;lt;/math&amp;gt;. When &#039;&#039;K&#039;&#039; is large (it could be in the thousands or more for a single neuron in a fully connected layer of a large model), the potential for floating-point error in that sum increases.&lt;br /&gt;
&lt;br /&gt;
If using FP32, the error may still be negligible in many cases, but in lower precision like FP16, it can become significant. For example, consider a dot product of length 2048 in FP16. Each product is in FP16 (with ~3 decimal digits precision) and if accumulated in FP16, the worst-case error could be on the order of &#039;&#039;2048 × ε_FP16 ≈ 2048 × 0.0009766 ≈ 2&#039;&#039; (i.e., you could lose a couple units in the third decimal place relative accuracy in worst case). Even in random error accumulation, you might see error on the order of &amp;lt;math&amp;gt;\sqrt{2048} \times 0.0009766 \approx 0.044&amp;lt;/math&amp;gt; or 4.4% (though this is a rough estimate). That’s quite significant. Fortunately, as discussed, most hardware will accumulate such sums in a higher precision like FP32, dramatically reducing that error (~7 orders of magnitude smaller &#039;&#039;ε&#039;&#039;). But if it didn’t, the network’s outputs could get noisy. A fully-connected layer with many inputs might output slightly different results depending on summation order if done in low precision.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Large-scale reductions&#039;&#039;&#039; also occur in operations like global average pooling (summing all elements of a feature map) or computing the total loss by summing loss over batch samples. If one sums 100,000 values in FP32, the worst-case error could be on the order of 100,000 &#039;&#039; 1e-7 ≈ 1e-2 (1% uncertainty), though typical error would be much less. In FP16, summing 100k values is likely impossible without overflow: as noted, adding ~10^4 moderate numbers can overflow FP16. So large reductions &#039;&#039;must* be handled carefully (either in chunks or in higher precision). Frameworks generally do: for example, when performing a sum reduction on GPU, libraries break it into parallel blocks that accumulate in registers (usually 32-bit registers, even if inputs are 16-bit). &amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Another aspect is that many dot products in neural nets involve values of varying magnitude. If the distribution of terms is such that some are much larger than others, the smaller terms might not contribute much if summed last. Ideally, one might sort by magnitude or use pairwise summation to improve that. Most BLAS libraries effectively do a pairwise strategy due to their parallel structure. So, while worst-case analyses are grim, practical implementations mitigate a lot of it.&lt;br /&gt;
&lt;br /&gt;
However, certain layers can suffer if not handled properly. One famous example is in RNNs or LSTMs: if one uses FP16 naive summation for the gating mechanism, errors could accumulate over many time steps and cause drift or instability. In modern practice, recurrent layers are often run in FP16 with FP32 accumulators to avoid this.&lt;br /&gt;
&lt;br /&gt;
In summary, &#039;&#039;&#039;dot products and large reductions need sufficient precision&#039;&#039;&#039; or robust algorithms. If you are implementing a custom kernel (say a custom ONNX op) and you have to sum a huge array, you should consider using a technique from Section 3 (pairwise summation, Kahan, or at least double accumulator if possible). Many deep learning computations, by virtue of hardware and library design, already do something like this under the hood. But it&#039;s always good to be aware, especially if pushing into lower precision territories.&lt;br /&gt;
&lt;br /&gt;
=== 6.2. Cancellation, Dynamic Range, and Conditioning ===&lt;br /&gt;
&lt;br /&gt;
We introduced &#039;&#039;cancellation&#039;&#039; earlier in the context of summation. In deep learning, cancellation can appear in a few places. One is in the computation of certain norms or variances. For instance, Batch Normalization computes mean and variance of activations. The variance formula &amp;lt;math&amp;gt;\mathrm{Var}(x) = E[x^2] - (E[x])^2&amp;lt;/math&amp;gt; is subject to catastrophic cancellation if &#039;&#039;E[x]&#039;&#039; and &amp;lt;math&amp;gt;\sqrt{E[x^2]}&amp;lt;/math&amp;gt; are close in magnitude (which can happen if the variance is small compared to the mean). If implemented naively in single precision, you might get a negative variance due to numerical error (which would then be clamped to zero or sqrt would yield NaN). Robust implementations compute variance in a single pass or increase precision. Indeed, in many frameworks, batchnorm accumulation is done in FP32 even if the activations are FP16, precisely to avoid numerical issues in computing mean and variance. ONNX itself doesn’t specify this, but an ONNX backend is likely to do batchnorm stats in higher precision for safety. The &#039;&#039;conditioning&#039;&#039; of the variance calculation is poor when variance is tiny relative to mean, so it&#039;s a spot where higher precision is needed.&lt;br /&gt;
&lt;br /&gt;
Another example is in certain loss functions or regularizations. If you subtract two large numbers to get a small difference, you lose precision. Say you have two nearly identical predictions and you subtract them as part of some operation – if done in float16, you might get zero whereas float32 would have a slight difference. This could affect, say, a gradient if that difference was supposed to propagate information. Fortunately, neural nets often avoid directly subtracting large nearly equal numbers; but it can happen. In attention mechanisms, the &#039;&#039;score normalization&#039;&#039; (softmax) subtracts a max to avoid huge exponentials, which is good for range but if multiple scores are close to the max, you subtract numbers that are close. However, those subtractions are not catastrophic in the same way; they&#039;re intentional to reduce range and are typically fine in FP32 or even FP16.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Dynamic range&#039;&#039;&#039; issues are everywhere in deep learning. Activation functions like ReLU and exponential can produce very large or very small values. If subsequent computations don’t account for that, you get infinities or zeros. For example, consider a network output that produces a logit of 100 in FP16. &amp;lt;math&amp;gt;\exp(100)&amp;lt;/math&amp;gt; in FP16 overflows to infinity (since FP16 max ~ 6e4 and &#039;&#039;e&amp;lt;sup&amp;gt;100&amp;lt;/sup&amp;gt; ∼ 3.7e43&#039;&#039;). If you then do softmax, that one infinity will dominate the sum and you might get a probability of 1 for that class (or NaNs if not handled). The standard softmax implementation subtracts the max logit, so in this case it would subtract 100 from all logits, making the largest exponent &amp;lt;math&amp;gt;\exp(0)=1&amp;lt;/math&amp;gt; and others tiny – this avoids overflow. But what about underflow? If you had a very negative logit, &amp;lt;math&amp;gt;\exp(-100)&amp;lt;/math&amp;gt; in FP16 would underflow to 0. In softmax, that just contributes ~0 probability, which is fine (it effectively is zero anyway). Underflow tends to be less dangerous than overflow in inference — underflow usually just means “this probability/neuron is effectively 0,” whereas overflow can poison the results with NaNs or Infs. &lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Conditioning&#039;&#039;&#039; refers to how sensitive a function is to input perturbations. Neural networks themselves as functions can be poorly conditioned (e.g., tiny changes in weights cause big changes in output for some architectures), but typically they are designed to be reasonably well-behaved. Certain internal computations like matrix inversion (if one does a pseudoinverse or something for a layer) can be extremely ill-conditioned if the matrix is near singular. That again would need double precision perhaps. In ML, one common ill-conditioned operation is the &#039;&#039;&#039;softmax denominator&#039;&#039;&#039;: &amp;lt;math&amp;gt;\sum_j e^{x_j}&amp;lt;/math&amp;gt; can be very large and also very sensitive to changes in the largest &#039;&#039;x&amp;lt;sub&amp;gt;j&amp;lt;/sub&amp;gt;&#039;&#039;. However, the softmax is more stable if computed carefully (max trick). Another is &#039;&#039;&#039;normalization&#039;&#039;&#039;: computing &amp;lt;math&amp;gt;\frac{x - \mu}{\sigma}&amp;lt;/math&amp;gt; for layer norm or batch norm – if &amp;lt;math&amp;gt;\sigma&amp;lt;/math&amp;gt; is very small, then the division blows up noise. In those cases, a small &#039;&#039;ε&#039;&#039; is usually added to &amp;lt;math&amp;gt;\sigma^2&amp;lt;/math&amp;gt; to prevent instability (this is a model-level fix, not a numeric one per se, but it acknowledges numeric limits).&lt;br /&gt;
&lt;br /&gt;
In training, &#039;&#039;&#039;optimizers&#039;&#039;&#039; can suffer from precision issues. For example, the Adam optimizer keeps a moving average of squared gradients; in half precision, these small updates might flush to zero or not change at some point. Some research found that certain adaptive optimizers break in FP16 because the small differences are lost (&amp;quot;Why FP16 training breaks NAdam&amp;quot; etc.). The solution is often to keep the optimizer states in FP32 while doing the bulk of compute in FP16. This again is a form of mixed precision.&lt;br /&gt;
&lt;br /&gt;
From an inference perspective, once the model is trained, the main concerns are avoiding overflow in activations and ensuring summations (like in softmax or reductions) are done with enough precision. The model’s weights are fixed, so we don&#039;t have to worry about cumulative error over iterations as in training, just the forward pass.&lt;br /&gt;
&lt;br /&gt;
=== 6.3. Normalization Layers and Softmax ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Normalization layers&#039;&#039;&#039; (BatchNorm, LayerNorm, etc.) and &#039;&#039;&#039;Softmax&#039;&#039;&#039; are specifically worth examining for numerical stability:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Batch Normalization:&#039;&#039;&#039; During inference, batchnorm applies an affine transform using precomputed mean and variance (from training). It’s basically: &amp;lt;math&amp;gt;y = \frac{x - \mu}{\sqrt{\sigma^2 + \epsilon}} \gamma + \beta&amp;lt;/math&amp;gt;. This is a stable operation as long as &#039;&#039;ε&#039;&#039; is provided (to avoid division by zero). Since &amp;lt;math&amp;gt;\mu, \sigma^2&amp;lt;/math&amp;gt; are constants in inference, it reduces to linear ops which are fine. The main numerical work (computing mean/variance) happened during training or model calibration, and frameworks usually did that in FP32. So batchnorm at inference time is not a source of instability (it’s just a scale and shift of the data).&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Layer Normalization / Group Normalization:&#039;&#039;&#039; These compute mean/var on the fly for each sample (and possibly channel group). In inference, they still need to compute these statistics for each forward pass. If implemented in low precision, the variance calculation can suffer cancellation as mentioned. However, it&#039;s likely that inference engines compute the mean and variance in FP32 even if inputs are FP16 (especially since these are per-sample operations, not huge batch reductions – it&#039;s feasible to do in higher precision). If someone implemented layer norm purely in FP16, they might see issues when the variance is very small. Usually there’s an &#039;&#039;ε&#039;&#039; added which might dominate a tiny variance and thus mitigate catastrophic cancellation (the result might just output something mostly from &#039;&#039;ε&#039;&#039;). In summary, normalization layers are usually fine as long as the implementation uses enough precision for the summation of squares.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Softmax:&#039;&#039;&#039; Softmax is notorious if done improperly. The safe algorithm: subtract the maximum input from all inputs (to bring the largest exponent to 1.0), exponentiate, sum them up, divide each exponent by the sum. The max subtraction handles overflow in &amp;lt;math&amp;gt;\exp()&amp;lt;/math&amp;gt;. But what about the sum? As discussed, if there are &#039;&#039;N&#039;&#039; elements in the softmax, the sum is at most &#039;&#039;N&#039;&#039; (when all inputs equal the max). In classification, &#039;&#039;N&#039;&#039; might be the number of classes. For ImageNet, &#039;&#039;N=1000&#039;&#039;, so worst sum ~1000 (which is fine in FP16, as 1000 &amp;lt;&amp;lt; 65504). For an extreme case like a language model, &#039;&#039;N&#039;&#039; could be 50,000 (vocab size). Sum could be 50,000 in worst case (everyone equal probability). 50,000 is still below 65504, so actually an FP16 sum would &#039;&#039;not&#039;&#039; overflow in that scenario, and the rounding error on a sum of 50k identical terms would be maybe a few ULPs (which at 50k magnitude might be ~0.5 difference worst-case). So maybe softmax summation in FP16 is not catastrophic for output probabilities – you’d get essentially 1.0 after normalization instead of 0.99999 in some case. Usually fine.&lt;br /&gt;
&lt;br /&gt;
:The bigger issue is if one or a few terms dominate the softmax. If one term is much larger, the others underflow to 0 after the max subtraction. That’s fine: you get a one-hot distribution essentially. If the model expected a softer distribution, that could be a problem. But if the logits were really that far apart, even FP32 would give nearly one-hot. So not really a precision issue, more of a model property.&lt;br /&gt;
&lt;br /&gt;
:However, the &#039;&#039;&#039;stability during training&#039;&#039;&#039; is a big issue. The GitHub thread we saw (ColossalAI) mentioned training a 130B model where FP16 softmax in attention caused training to occasionally blow up&amp;lt;ref name=&amp;quot;ref14&amp;quot;/&amp;gt;. They observed attention scores in some heads ranging up to 1e4 or as low as 1e-3&amp;lt;ref name=&amp;quot;ref14&amp;quot;/&amp;gt;. 1e4 exponent in FP16 is definitely Inf (because &amp;lt;math&amp;gt;\exp(10^4)&amp;lt;/math&amp;gt; is astronomically huge, but even exp(100) is inf in half). The method CogView introduced (Precision Bottleneck Relaxation) involved subtracting the max &#039;&#039;per head&#039;&#039; to reduce range&amp;lt;ref name=&amp;quot;ref14&amp;quot;/&amp;gt;—which is basically the softmax max trick at a finer granularity or something similar. In the end, they found &#039;&#039;&#039;the simplest fix was to compute softmax in FP32&#039;&#039;&#039; for those layers&amp;lt;ref name=&amp;quot;ref14&amp;quot;/&amp;gt;. They reported that it had negligible speed impact but significantly improved stability&amp;lt;ref name=&amp;quot;ref14&amp;quot;/&amp;gt;. This highlights a general principle: certain operations like softmax (and sometimes matrix multiplications in attention) benefit from higher precision, and using FP32 in a few critical spots can save a mixed-precision training from crashing. In inference, using FP32 for softmax is also a safeguard if one is uncertain. It costs little because softmax is not a heavy compute compared to matrix multiplies.&lt;br /&gt;
&lt;br /&gt;
:ONNX does have a Softmax operator; if the input is float16, a backend might do exactly what they described: internally cast to float32 for the softmax computation, then cast output back to float16. Some inference engines (like DeepSpeed’s inference or others) keep attention softmax in FP32 even if rest is FP16, for robustness&amp;lt;ref name=&amp;quot;ref14&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Apart from softmax, other places where exponentials occur are in loss functions like cross-entropy (which uses log-softmax internally), or in activation functions like Sigmoid and Tanh (which are basically scaled logistic functions). Sigmoid(&#039;&#039;x)=1/(1+e&amp;lt;sup&amp;gt;-x&amp;lt;/sup&amp;gt;)&#039;&#039;. If &#039;&#039;x&#039;&#039; is moderately large positive, &#039;&#039;e&amp;lt;sup&amp;gt;-x&amp;lt;/sup&amp;gt;&#039;&#039; underflows to 0 in FP16 if &amp;lt;math&amp;gt;x&amp;gt;~11&amp;lt;/math&amp;gt;. That gives a sigmoid output of 1.0 exactly. In FP32, &#039;&#039;e&amp;lt;sup&amp;gt;-x&amp;lt;/sup&amp;gt;&#039;&#039; for &#039;&#039;x=11&#039;&#039; is about &#039;&#039;5e-5&#039;&#039;, not underflow, so sigmoid ~0.99995. So FP16 will saturate some outputs to exactly 0 or 1 sooner than FP32 would. This can slightly change behaviors (e.g., a neuron might go to full saturation a bit early). Usually, this isn’t a huge deal, but it could make gradients zero earlier. In inference, we don’t care about gradients, just the output value. Getting exactly 1.0 vs 0.99995 probably has no practical difference for a classification output. But one should be aware that &#039;&#039;&#039;activation functions can saturate faster&#039;&#039;&#039; in lower precision due to limited range or precision. ReLU has no issue (it’s linear or 0), but exponential-based activations do. In practice, again, frameworks often simply compute those in the given precision and accept the slight difference. If it were a problem, one could do those in FP32 as well.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Summary:&#039;&#039;&#039; Numerical stability in inference is maintained by using robust computation methods: subtracting maxima in softmax/LogSumExp, adding epsilon in denominators for normalization, and using higher precision for summations or critical steps. Most of these are built into the algorithms. ONNX models assume these standard practices (they don&#039;t need to explicitly do them in the graph because the backend inherently will for ops like Softmax). It&#039;s wise when building or exporting models to ONNX to test them in the target precision and ensure there are no surprises (like an output becomes NaN or Inf due to precision). Usually if training was done in mixed precision, the model will have been vetted in that environment already.&lt;br /&gt;
&lt;br /&gt;
== Performance vs. Accuracy Trade-offs ==&lt;br /&gt;
&lt;br /&gt;
There is an inherent trade-off between numerical precision and computational performance (speed, memory usage) in AI systems. Choosing lower precision can dramatically increase throughput and reduce memory, but it may introduce arithmetic errors. Engineers must balance these considerations:&lt;br /&gt;
&lt;br /&gt;
=== 7.1. Throughput, Memory Bandwidth, and Precision ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Throughput (speed)&#039;&#039;&#039; is often higher for lower-precision operations. Modern hardware can execute more low-precision ops in parallel than high-precision ops. For example, an NVIDIA V100 GPU can perform 2× more half-precision FLOPs than single-precision per clock, and newer architectures with Tensor Cores can do 8× more (since a Tensor Core warp operation might perform 64 FP16 FMAs per cycle vs 16 FP32). Similarly, int8 operations can be even faster and more energy efficient. Lower precision numbers also use less memory bandwidth – fetching 16-bit data is twice as fast as 32-bit data from memory (assuming the same bus width), which can relieve memory bottlenecks. Many deep learning workloads are memory-bound, so halving the data size can nearly double throughput if compute is not the limiting factor. This is a huge incentive to use lower precision.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Memory and storage&#039;&#039;&#039;: Using FP16 or int8 weights reduces model size, which is important for deploying large models to memory-limited environments (like edge devices or GPUs with limited VRAM). It also means you can fit larger batch sizes in memory, or more models on a chip, etc. There is also a cascade effect: smaller data means better cache utilization, which further improves performance.&lt;br /&gt;
&lt;br /&gt;
However, &#039;&#039;&#039;accuracy&#039;&#039;&#039; suffers if precision is too low. We have discussed how rounding error and limited range can cause deviations in outputs. The question is: do those deviations matter for the application? Often for neural networks, a small amount of noise or error in intermediate calculations does not change the final prediction. Neural nets are somewhat error-tolerant due to their redundant distributed representations. This is why quantization and FP16 techniques can work at all. The final model accuracy (e.g. top-1 accuracy on ImageNet) might drop only 0.5% or less when using FP16 instead of FP32, or int8 instead of FP16, if done properly. For many use-cases, this loss is acceptable given the speed gains.&lt;br /&gt;
&lt;br /&gt;
If one goes too low (like naive FP8 or int4 without special care), accuracy might drop severely – perhaps making the model unusable. Thus the trade-off: as precision decreases, performance increases but at some point accuracy degrades beyond an acceptable threshold. Engineers aim to find the lowest precision that still preserves accuracy.&lt;br /&gt;
&lt;br /&gt;
There is also the consideration of &#039;&#039;&#039;accumulation strategy&#039;&#039;&#039; affecting performance: for instance, Kahan summation (compensated) is more accurate but ~4× slower than naive summation. If you need that accuracy, you pay a performance cost. In many AI inference scenarios, the slight extra accuracy from something like Kahan isn&#039;t needed, so it’s not used. But in a scenario where you sum millions of elements for a final result that is thresholded, maybe you do need it.&lt;br /&gt;
&lt;br /&gt;
Another dimension is &#039;&#039;&#039;determinism vs speed&#039;&#039;&#039;. Enforcing a deterministic reduction order (for reproducibility) might require disabling some parallelism, which could slow things down. If exact repeatability is critical (like certain scientific or financial applications of neural nets), one might sacrifice some performance to ensure the summation happens the same way every time. ONNX allows both modes depending on the runtime.&lt;br /&gt;
&lt;br /&gt;
From a memory perspective, if precision is higher than needed, you waste memory. For example, storing intermediate activations in FP32 when they could be FP16 uses 2× memory, which might mean lower batch size or more memory traffic. So there&#039;s strong motivation to keep things as low-precision as possible &#039;&#039;except&#039;&#039; where accuracy truly demands.&lt;br /&gt;
&lt;br /&gt;
In summary, the trade-off often manifests as &#039;&#039;diminishing returns&#039;&#039;: FP64 vs FP32 – huge slowdown for usually negligible accuracy gain in ML; FP32 vs FP16 – moderate speedup for usually minor accuracy hit; FP16 vs int8 – another big speedup but maybe more accuracy loss unless calibration is done; int8 vs int4 – speed gain but often big accuracy drop that might not be worth it for current models.&lt;br /&gt;
&lt;br /&gt;
Engineers use techniques like &#039;&#039;&#039;mixed precision&#039;&#039;&#039; to target high precision only where needed. For example, run most of the model in FP16, but keep a few sensitive layers in FP32. This yields most of the performance benefits while ensuring critical parts are accurate. We already saw softmax as one example. Another might be final output layer: sometimes using FP32 for the logits and softmax can be safer.&lt;br /&gt;
&lt;br /&gt;
Hardware is also trending to support mixed modes – like FP8 Tensor Cores accumulate in FP32, etc. So hardware tries to give accuracy cheaply.&lt;br /&gt;
&lt;br /&gt;
=== 7.2. Hardware Accelerators and Tensor Cores ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Hardware accelerators&#039;&#039;&#039; (GPUs, TPUs, FPGAs, ASICs like GraphCore IPU, etc.) have specific support for certain precisions. Leveraging these can provide massive speedups:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;NVIDIA Tensor Cores:&#039;&#039;&#039; These started in Volta (V100) with support for 4x4 matrix multiply-adds on FP16 inputs with FP32 accumulation. Using Tensor Cores can give up to 8x throughput compared to using regular FP32 cores. But to use them, the data needs to be FP16 (or now BF16 or FP8 on newer GPUs) and aligned to certain dimensions (multiples of 8 etc. in matrix sizes, which frameworks handle by padding if needed). If an ONNX model is all FP32, by default it would not use Tensor Cores unless the runtime automatically converts some ops to mixed precision (which some do via a flag or environment variable enabling TF32 or FP16). The introduction of &#039;&#039;&#039;TensorFloat-32 (TF32)&#039;&#039;&#039; in Ampere GPUs is essentially a compromise: treat FP32 inputs as two 16-bit significand halves (10-bit mantissa actually) so that it can run on Tensor Cores, giving ~8x speed, while still outputting FP32. TF32 has about the precision of FP16 but range of FP32, and it was chosen because it typically doesn’t hurt convnet accuracy. ONNX Runtime or others on Ampere will use TF32 by default for conv and matmul unless disabled, giving a free speed boost at minimal accuracy cost. If exact FP32 is needed, users have to turn it off (sacrificing speed). &amp;lt;ref name=&amp;quot;ref6&amp;quot;/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;TPU (Google):&#039;&#039;&#039; TPUs have native support for BF16 matrix ops with FP32 accumulators. They were designed specifically for the mixed precision approach in training. For inference, TPUs also support int8 and other quantized ops (e.g., via Edge TPU for lower-power). If exporting ONNX to run on a TPU (via some bridge), one might get automatic BF16 usage. BF16 has the advantage of no loss in range, so TPUs can more straightforwardly run models in BF16 without modifications.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;FPGAs and ASICs:&#039;&#039;&#039; Some custom accelerators might use fixed-point or block floating-point arithmetic for efficiency. They often treat the network in quantized terms. The ONNX quantization specification (QuantizeLinear/DequantizeLinear ops) exists for representing quantized models (INT8 etc.), which is another domain beyond floating-point, but it overlaps as a precision issue. In those systems, accumulators for INT8 × INT8 multiplies are usually 32-bit (to avoid overflow for dot products up to certain lengths). The trade-off there is between integer vs float representation, which can give big speedups on CPUs/DSAs via SIMD instructions (many CPUs have vector int8 multiply-add instructions now for inference).&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Memory bandwidth on hardware:&#039;&#039;&#039; Some accelerators include special high-bandwidth memory or cache specifically for lower precision. For example, an accelerator might fetch two FP16 values in one 32-bit memory transaction – effectively doubling memory bandwidth for half precision. On some hardware, using FP16 can even reduce memory &#039;&#039;latency&#039;&#039; if multiple values can be packed.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Parallelism and Vectorization:&#039;&#039;&#039; Many hardware units process data in fixed vector widths (e.g., 256-bit registers). That could be 8 floats at once or 16 half-floats at once in the same 256 bits. If your code is in FP16, you might use the full vector width (16 values) whereas FP32 might only use half (8 values). This means FP16 can make better use of vector units. Similarly, matrix units often have fixed tile sizes, and smaller data types allow larger tiles or more tiles concurrently.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Energy efficiency:&#039;&#039;&#039; Lower precision often means less energy per operation, which on large deployments (data centers or battery-powered devices) is a significant advantage. This isn&#039;t directly performance, but it&#039;s an important trade: you might choose a slightly lower precision to save power even if accuracy drops a tiny bit.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Software overhead:&#039;&#039;&#039; Using mixed precision on hardware involves some overhead of casting and moving data between precisions. For instance, if you have to frequently cast FP16 to FP32 and back, that adds instructions and memory copies. A well-designed accelerator (like GPUs with unified memory for FP16/32 or special hardware casting) minimize this. Still, the runtime might insert extra operations (as we saw, ONNX Runtime CPU inserted many Cast nodes converting FP16 to FP32 and back, which actually &#039;&#039;slowed down&#039;&#039; that FP16 model by 4x vs FP32 on a CPU, because the CPU had no native FP16). So ironically, on some hardware, using lower precision in ONNX might degrade performance if not supported (the CPU case). Always ensure the target hardware actually supports the low precision you plan to use; otherwise, you&#039;ll pay overhead for emulation. &amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
To illustrate trade-off: Suppose you have a model that achieves 99% accuracy in FP32 and drops to 98.5% in FP16. If FP16 runs 2x faster, many would accept the 0.5% accuracy drop for double throughput. If int8 runs 4x faster but accuracy drops to 95%, that might be too much drop for some applications, but acceptable for others that value speed. It&#039;s application-dependent.&lt;br /&gt;
&lt;br /&gt;
In critical applications, one might maintain higher precision accumulators or run certain validation steps in high precision to ensure nothing drifted. In research, sometimes a model is trained in mixed precision but validated in double to ensure any small numeric differences don’t influence the reported result (rare, but for scientific computing integration maybe).&lt;br /&gt;
&lt;br /&gt;
In the ONNX world, performance-vs-accuracy is often managed via &#039;&#039;&#039;Execution Providers&#039;&#039;&#039; (EPs) that target specific hardware. For example, the TensorRT EP in ONNX Runtime can take an ONNX model and run it using NVIDIA&#039;s high-performance inference library, which will use FP16 or INT8 if allowed, to maximize speed. The user can configure the EP with precision flags. If the accuracy drop is too high, one might restrict it to FP32. If it&#039;s fine, allow FP16. Tools exist to measure the accuracy difference (like calibrating int8 quantization).&lt;br /&gt;
&lt;br /&gt;
To conclude, the trade-offs revolve around picking the lowest precision that yields acceptable accuracy, and utilizing hardware features fully. ONNX being a high-level spec abstracts this, but it&#039;s ultimately up to the deployment to decide and sometimes up to the exporter to incorporate quantization. ONNX’s role is to be able to &#039;&#039;represent&#039;&#039; these choices (they have added quantized types, FP8 types, etc., to allow the graph to include those when needed).&lt;br /&gt;
&lt;br /&gt;
== Best Practices and Design Guidelines ==&lt;br /&gt;
&lt;br /&gt;
Given the above insights, here are some best practices and guidelines for dealing with numerical precision and accumulators in AI inference (and training) design:&lt;br /&gt;
&lt;br /&gt;
=== 8.1. Choosing an Accumulator Precision ===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Match precision to data length and range:&#039;&#039;&#039; If an operation accumulates a large number of terms, consider using a higher precision accumulator than the input data. For example, for summing &amp;gt;1000 values of type FP16, an FP32 accumulator is recommended to avoid large rounding error or overflow. If summing &amp;gt; million values of FP32, consider double (FP64) accumulation if high accuracy is needed (though this is rare in DL). &#039;&#039;Rule of thumb:&#039;&#039; if &amp;lt;math&amp;gt;N \cdot \varepsilon&amp;lt;/math&amp;gt; is not tiny (say &amp;gt;1e-3), you may benefit from higher precision. For FP16 (&#039;&#039;ε ≈ 1e-3&#039;&#039;), that threshold N is a few hundred. For FP32 (&#039;&#039;ε ≈ 1e-7&#039;&#039;), N would be in the millions before 1e-3 error.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Use higher precision in critical layers:&#039;&#039;&#039; Identify if certain layers of the model are more sensitive to precision. For instance, the output layer or a layer that feeds into a very sharp decision boundary might require more precision. If a small change there could flip the prediction, you don&#039;t want excessive numerical error. Using FP32 for logits and softmax is a common practice to maintain classification confidence stability.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Follow hardware defaults, but override if needed:&#039;&#039;&#039; Many hardware accelerators have sensible defaults (like accumulate in FP32 for tensor ops). It&#039;s usually best to use those defaults as they were chosen to balance accuracy and speed. Only override if you have evidence of a problem. For example, if using ONNX Runtime on CPU, you discovered it upcasts to float for you – great. But if using some custom accelerator that does pure FP16, you might manually insert casts in the ONNX graph around certain ops to force higher precision on those ops.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Maintain precision for gradient accumulation (training):&#039;&#039;&#039; This is more training-oriented, but worth noting: when summing gradients over batches or updating weights, use higher precision (FP32 or even 64) for those accumulations. This ensures training doesn&#039;t diverge due to numeric error. Many frameworks keep a &amp;quot;master copy&amp;quot; of weights in FP32 when training in FP16. For inference, this translates to: any running statistics or counters in the model (like if you had a running average inside the model) should probably be in higher precision to avoid drift. &amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Be mindful of intermediate cast costs:&#039;&#039;&#039; If you do decide to cast to higher precision for an operation in ONNX, remember it incurs memory copies and compute overhead. Make sure it&#039;s worth it. For example, if you cast a huge tensor to double, that could slow things drastically. Usually FP16-&amp;gt;FP32 is fine (hardware handles it well), but FP32-&amp;gt;FP64 might be expensive on GPUs.&lt;br /&gt;
&lt;br /&gt;
=== 8.2. Safe Reduction Lengths and Heuristics ===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Chunk long reductions:&#039;&#039;&#039; If you must do a very long sum in low precision (due to memory constraints or hardware), chunk the sum into parts, sum each to a higher precision local accumulator, then sum the partial sums. This is essentially pairwise summation. Many libraries use a fixed chunk size heuristic (e.g., sum in blocks of 256 in registers, then add those up). This bounds the error growth. If implementing manually, you can choose a chunk size such that chunk_length &#039;&#039; ε is small (like 256 &#039;&#039; 1e-3 = 0.256 for FP16, hmm maybe choose 100).&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Sort by magnitude if possible:&#039;&#039;&#039; In some cases (not typical in DL, but possible in post-processing), if you have numbers varying by orders of magnitude, summing from smallest to largest improves accuracy. It prevents small ones from being swallowed by big ones. This is more relevant if you output a sum of mixed-scale values (like some algorithm mixing big and small contributions). It’s not always feasible in real-time inference to sort, but keep in mind.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Use compensation for critical sums:&#039;&#039;&#039; If you find a particular sum or difference in your inference computation is crucial (like computing a small residual from two close numbers), consider using Kahan summation or another compensated scheme just for that part. For example, if you had an ONNX custom function that needs to sum a lot of almost cancelling terms, you might implement it with compensation internally.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Heuristics for FP8/low-bit:&#039;&#039;&#039; When using extremely low precision like FP8, adopt the recommended practices: use scaling (don&#039;t feed raw data into FP8, normalize it first), and perhaps only use FP8 on portions of the network that are empirically robust. The industry is still establishing heuristics, but one might be: use FP8 for matrix multiplications, but accumulate in FP16 or FP32; keep normalization in FP16; use FP16 for first and last layer maybe. This is evolving, so keep an eye on literature.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Keep an eye on sum length in softmax and normalization:&#039;&#039;&#039; If you have extremely large class counts or group sizes (e.g., a softmax over 1e6 items, or layer norm over tens of thousands of features), realize this is a giant reduction. You may want to ensure that reduction is done in higher precision. ONNX Softmax by default will likely not handle a million-class case in FP16 well (the sum would overflow since 1e6 &amp;gt; 65504). So either do that on CPU in FP32 or break it down (could do hierarchical softmax, etc.). This is a corner case, but relevant for huge vocab language models. In practice, vocab ~50k is fine as we said.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Test numeric differences:&#039;&#039;&#039; A practical tip: after quantizing or reducing precision, run some test inputs through both the high-precision model and low-precision model. Compare outputs (and maybe some intermediate signals if possible). If differences are consistently small relative to the application tolerance, you&#039;re good. If you see occasional large differences or instabilities (like outputs become NaN or Inf in low precision), that&#039;s a red flag. Use that testing to guide where you might need to increase precision or change algorithms.&lt;br /&gt;
&lt;br /&gt;
=== 8.3. When Higher Precision Is Required ===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Ill-conditioned calculations:&#039;&#039;&#039; If your model or pipeline performs any operation known to be ill-conditioned (matrix inversion, subtraction of nearly equal quantities, computing a tiny difference of large numbers), you likely need higher precision for that part. For example, if your ONNX model includes a custom op to solve a linear system, use double precision in that op if possible (or at least float with iterative refinement). In ML, this might occur in something like a physics-informed neural network or a differentiable simulation within the model.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Strict accuracy requirements:&#039;&#039;&#039; Some applications (medical, aerospace, finance) might have strict tolerances. If an inference model’s output needs to be correct to 5 decimal places, you probably cannot use FP16, which has only 3-4 decimal digits precision. FP32 might be barely enough (~7 digits). FP64 might be warranted to guarantee that level of accuracy in worst case. Most deep learning applications (like image classification) don&#039;t demand that level of numeric precision in outputs – as long as the top class is correct, it doesn&#039;t matter if its probability was 0.901 vs 0.899. But if you&#039;re doing something like summing probabilities to make a risk estimate or doing a cumulative sum over many predictions, be careful to accumulate precisely.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Large sums or products in post-processing:&#039;&#039;&#039; Sometimes the neural net is fine, but the &#039;&#039;post-processing&#039;&#039; of results might need precision. E.g., summing up probabilities of many events, or computing confidence intervals, etc. Do those calculations in double if needed outside the model.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Preventing drift in long sequences:&#039;&#039;&#039; For sequential models (RNNs processing thousands of steps), rounding error can accumulate over steps. If each step has a small error, after 10000 steps it could add up. In such cases, using a higher precision for the state might reduce drift. Some practitioners use FP64 for the hidden state accumulation in very long sequence processing to maintain numerical stability (though this is not common due to speed loss). Alternatively, periodically renormalize or reset small errors.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Simulations or differential equations:&#039;&#039;&#039; If your ONNX model involves simulating something (like a differential equation solver inside a model, or an ODE/RNN hybrid), those often need double precision to be stable for long durations. Recognize those situations and allocate precision accordingly.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Verification and debugging:&#039;&#039;&#039; Use higher precision as a tool to verify correctness. For example, run your model in FP64 (if possible) and compare to FP32 to see if there are differences. If not, FP32 is fine. If yes, you might find a particular calculation losing too much info in FP32, implying maybe you should keep it in FP64 in production as well. This is rare in inference but can be part of rigorous validation.&lt;br /&gt;
&lt;br /&gt;
In ONNX, using higher precision may involve casting to &amp;lt;code&amp;gt;tensor(double)&amp;lt;/code&amp;gt; for certain ops. Not all runtimes support double on GPU though (some do via emulation or just slower path). ONNX CPU does support double for general ops. So be mindful of the target.&lt;br /&gt;
&lt;br /&gt;
Finally, &#039;&#039;&#039;document your precision choices&#039;&#039;&#039;. If you deliver an ONNX model that uses mixed types (float16 inputs, float32 accumulations, int8 weights, etc.), ensure that is clear to whoever maintains or uses it. Precision issues can be sneaky, so clarity helps.&lt;br /&gt;
&lt;br /&gt;
== Limitations and Open Problems ==&lt;br /&gt;
&lt;br /&gt;
Despite advances in handling numerical precision, there remain limitations and open challenges in this domain:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Lack of Precision Specification in ONNX:&#039;&#039;&#039; As noted, ONNX currently has no way to formally specify that an op &#039;&#039;should&#039;&#039; use a higher precision internally. This can lead to nondeterministic differences across platforms. While proposals exist to add attributes for this, it is not yet standard. Thus an ONNX model might behave slightly differently on two compliant backends, and there&#039;s no built-in mechanism to ensure identical results. This is a limitation for applications requiring cross-platform consistency. Solving it may involve extending ONNX (e.g., an attribute like &amp;lt;code&amp;gt;accumulatePrecision=&amp;quot;FLOAT32&amp;quot;&amp;lt;/code&amp;gt; for MatMul) or tightening the spec, but those come with backward compatibility and implementation burden concerns. &amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Reproducibility and Determinism:&#039;&#039;&#039; Even with the same precision, parallel computation can introduce nondeterminism (due to different summation orders). Ensuring bit-identical results across runs (important for some sensitive domains) is non-trivial. Frameworks often provide &amp;quot;deterministic mode&amp;quot; which might use fixed ordering at cost of speed. In ONNX, there&#039;s no global flag for this; it&#039;s up to the runtime. More research could be done on algorithms that produce bit-wise identical results independent of parallel execution order (perhaps using techniques like pairwise summation or consistent partitioning). Stochastic rounding is another area: sometimes using probabilistic rounding can reduce bias in accumulated error, but it introduces randomness – reconciling that with reproducibility is tricky.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Ultra-low Precision (FP8 and below):&#039;&#039;&#039; While FP8 is being explored, going to 4-bit floats or mixed schemes is an open challenge. The OpenAI community is investigating 4-bit (quantization-wise) for LLMs with some success, but a true 4-bit float (like E3M1) would be extremely limited. Perhaps hybrid schemes (like block floating point, log scales, etc.) will come. ONNX already has some support (they mention 4-bit in documentation placeholders), but it&#039;s nascent. Research into training techniques, calibration, and new number formats (like &#039;&#039;&#039;posit numbers&#039;&#039;&#039; or tapering precision floats) is ongoing. If a new format proves useful, ONNX will need to adapt to represent it. &amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Quantization Noise and Model Robustness:&#039;&#039;&#039; Reducing precision can sometimes expose models to issues like adversarial vulnerability or fragility – e.g., a slight perturbation might flip a decision if the margin was small and precision loss nudged it. Ensuring model robustness under quantization is an open problem. It might require re-training the model with quantization in the loop (quantization-aware training) or designing models that have large &amp;quot;margin&amp;quot; between classes to tolerate quantization noise.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Dynamic range during training vs inference:&#039;&#039;&#039; In training, activations can have wider distributions (especially early in training). Models often learn to constrain their ranges (via activation functions saturating or normalization), which makes inference more stable in lower precision. But if one tries to train in FP8 from scratch, it&#039;s very hard. This gap between what&#039;s possible in a post-trained inference model vs training is an open area – how to train directly in low precision without significant accuracy loss. Solving that could simplify workflows (no need for high-precision training then quantize; just train low-prec end to end). Techniques like loss scaling, gradient clipping, etc., are partial solutions. The development of optimizers that are resilient to low precision is ongoing.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Hardware variability:&#039;&#039;&#039; As new hardware emerges (with new numeric capabilities or quirks), ensuring that ONNX models run consistently is an ongoing task. For instance, one device might implement a fused op that reduces rounding error, another does it separately. Even something like convolution might use a different algorithm (FFT-based vs direct) which has different numeric properties. Open problem: could ONNX allow specifying numeric tolerances per op or preferred algorithms? That may be overkill for most, but for some HPC ML use cases, maybe desired.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Testing and validation of numerics:&#039;&#039;&#039; There isn&#039;t a standardized set of &amp;quot;numeric compliance&amp;quot; tests for ONNX runtimes beyond simple correctness. Perhaps in the future, test suites might include challenging numeric scenarios (large sums, nearly singular matrices) to ensure a runtime handles them gracefully (maybe by switching precision). Right now, it&#039;s mostly on the user to test their model under expected conditions.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Combining quantization with floating point:&#039;&#039;&#039; Many production models use a mix (e.g., int8 for some layers, FP16 for others). ONNX can represent this (with QuantizeLinear ops or MixedPrecision through casting), but tooling to automatically find the optimal mix is still developing. It&#039;s somewhat trial-and-error or using heuristics. An open question is how to automate precision assignment: given a model and a target hardware, how to automatically choose which layers can be int8, which should be FP16, etc., to meet an accuracy target. This crosses into NAS (neural architecture search) territory or automated quantization. &lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Numerical debugging tools:&#039;&#039;&#039; When a model goes wrong due to precision, it&#039;s often hard to pinpoint where. Better tooling to analyze numerical error layer by layer could be useful. For instance, one could run a model in double and float and diff the intermediate results to see where the largest divergence happens. Doing this systematically for big nets is an open tooling challenge. Some frameworks have hooks for this, but not standardized.&lt;br /&gt;
&lt;br /&gt;
In conclusion, while we&#039;ve come far with using lower precision in AI without sacrificing much accuracy, the landscape is always evolving. The key open problem is how to push precision lower (for efficiency gains) without crossing the threshold where accuracy falls off a cliff. Each new generation of hardware and techniques (like FP8, novel quantization schemes) extends that boundary, but also brings new questions on how to manage the numerical errors. ONNX as an interchange will need to keep pace, possibly by adding more expressive power for these numeric considerations.&lt;br /&gt;
&lt;br /&gt;
== References ==&lt;br /&gt;
&lt;br /&gt;
&amp;lt;references&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref1&amp;quot;&amp;gt;Accelerate and simplify Scikit-learn model inference with ONNX Runtime - Microsoft Open Source Blog — opensource.microsoft.com — https://opensource.microsoft.com/blog/2020/12/17/accelerate-simplify-scikit-learn-model-inference-onnx-runtime&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref2&amp;quot;&amp;gt;Floating Point Precision: Understanding FP64, FP32, and FP16 in Large Language Models - DEV Community — dev.to — https://dev.to/lukehinds/floating-point-precision-understanding-fp64-fp32-and-fp16-in-large-language-models-3gk6&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref3&amp;quot;&amp;gt;Floating Point Precision and Its Limitations | by Umair Akbar | Medium — akbu.medium.com — https://akbu.medium.com/floating-point-precision-and-its-limitations-cfb7247d7789&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref4&amp;quot;&amp;gt;Kahan summation algorithm - Wikipedia — en.wikipedia.org — https://en.wikipedia.org/wiki/Kahan_summation_algorithm&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref5&amp;quot;&amp;gt;Pairwise summation - Wikipedia — en.wikipedia.org — https://en.wikipedia.org/wiki/Pairwise_summation&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref6&amp;quot;&amp;gt;Numerical behavior of NVIDIA tensor cores - PMC — pmc.ncbi.nlm.nih.gov — https://pmc.ncbi.nlm.nih.gov/articles/PMC7959640/&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref7&amp;quot;&amp;gt;Mixed precision and performance-accuracy tuning (neuron-cc) — AWS Neuron Documentation — awsdocs-neuron.readthedocs-hosted.com — https://awsdocs-neuron.readthedocs-hosted.com/en/latest/about-neuron/appnotes/neuron-cc/mixed-precision.html&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref8&amp;quot;&amp;gt;Create Float16 and Mixed Precision Models - ONNX Runtime — onnxruntime.ai — https://onnxruntime.ai/docs/performance/model-optimizations/float16.html&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref9&amp;quot;&amp;gt;[Performance] ONNX FP16 model is having performance bottle neck when compared to FP32 variant · Issue #25824 · microsoft/onnxruntime · GitHub — github.com — https://github.com/microsoft/onnxruntime/issues/25824&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref10&amp;quot;&amp;gt;Float stored in 8 bits - ONNX 1.21.0 documentation — onnx.ai — https://onnx.ai/onnx/technical/float8.html&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref11&amp;quot;&amp;gt;[2209.05433] FP8 Formats for Deep Learning — arxiv.org — https://arxiv.org/abs/2209.05433&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref12&amp;quot;&amp;gt;Should MatMul op allow specification of accumulation precision? · Issue #7072 · onnx/onnx · GitHub — github.com — https://github.com/onnx/onnx/issues/7072&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref13&amp;quot;&amp;gt;Quantize ONNX models | onnxruntime — onnxruntime.ai — https://onnxruntime.ai/docs/performance/model-optimizations/quantization.html&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref14&amp;quot;&amp;gt;Keep Attention Softmax FP32 during FP16/ZeRO Training · Issue #1485 · hpcaitech/ColossalAI · GitHub — github.com — https://github.com/hpcaitech/ColossalAI/issues/1485&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;/references&amp;gt;&lt;/div&gt;</summary>
		<author><name>Timo.stripf</name></author>
	</entry>
	<entry>
		<id>https://www.emmtrix.com/w139/index.php?title=Automatic_C_to_Rust_Translation&amp;diff=2908</id>
		<title>Automatic C to Rust Translation</title>
		<link rel="alternate" type="text/html" href="https://www.emmtrix.com/w139/index.php?title=Automatic_C_to_Rust_Translation&amp;diff=2908"/>
		<updated>2026-02-03T08:00:59Z</updated>

		<summary type="html">&lt;p&gt;Timo.stripf: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;The systems programming community has long grappled with security flaws from memory-unsafe languages like C. Rust offers memory and thread safety guarantees by enforcing strict ownership and lifetime rules at compile time. Migrating legacy C codebases to Rust could eliminate entire classes of vulnerabilities. However, manual rewrites of large C codebases in Rust are labor-intensive and error-prone. This has spurred development of &#039;&#039;&#039;automatic C-to-Rust translation&#039;&#039;&#039; tools. The goal is to automate conversion of C into Rust code that preserves the original program’s behavior while leveraging Rust’s safety features. In fact, the importance of this challenge is highlighted by DARPA’s &#039;&#039;Translating All C to Rust (TRACTOR)&#039;&#039; program, which explicitly aims to fully automate conversion of C into high-quality, idiomatic Rust code with memory safety guarantees. Achieving this is difficult due to fundamental differences between C and Rust (e.g. manual memory management vs. Rust’s ownership model, unrestricted pointers vs. Rust’s borrow checker). Nonetheless, several tools and research projects are actively tackling automatic C-to-Rust translation using a mix of compiler techniques and, more recently, AI. This article surveys the state-of-the-art tools – focusing on those actively maintained – and compares their approaches, features, fidelity, safety guarantees, and limitations. &amp;lt;ref name=&amp;quot;ref1&amp;quot;/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Challenges in Translating C to Rust ==&lt;br /&gt;
Automatically translating C into Rust is non-trivial because the languages have different paradigms and safety models. &#039;&#039;&#039;Pointers and memory management&#039;&#039;&#039; are central: C allows arbitrary pointer arithmetic, unchecked array accesses, and manual &amp;lt;code&amp;gt;malloc/free&amp;lt;/code&amp;gt; control, whereas Rust requires structured borrowing, prohibits data races, and enforces bounds-checking for safe references. A direct transliteration of C pointers into Rust will typically use Rust’s &#039;&#039;raw pointers&#039;&#039; and &amp;lt;code&amp;gt;unsafe&amp;lt;/code&amp;gt; blocks, which forgo Rust’s safety checks. This preserves semantics but yields Rust code that is as unsafe as the original C. The challenge is to infer higher-level safe abstractions (like Rust slices, references, or smart pointers) from low-level C code. Other tricky features include &#039;&#039;&#039;C’s unions&#039;&#039;&#039;, which have no direct safe equivalent in Rust (Rust’s unions are unsafe and lack tagging) and &#039;&#039;&#039;setjmp/longjmp&#039;&#039;&#039; or goto-based control flow, which do not map cleanly to Rust’s structured control constructs. Because C allows patterns that violate Rust’s safety rules (like aliasing a mutable buffer in multiple places), any automated tool must either (a) leave such code in &amp;lt;code&amp;gt;unsafe&amp;lt;/code&amp;gt; blocks, or (b) apply complex analyses or transformations to restructure the code for safety.&lt;br /&gt;
&lt;br /&gt;
Another concern is handling &#039;&#039;&#039;undefined behavior (UB)&#039;&#039;&#039; in C. C code that invokes UB (like overflowing a signed integer, dereferencing an invalid pointer, or reading uninitialized memory) has no well-defined semantics. A straightforward translation may “inherit” these issues. For example, translating a potentially overflowing C arithmetic operation to Rust might use Rust’s normal arithmetic operator, which will panic on overflow in debug builds (a detectable failure) instead of exhibiting arbitrary behavior. In general, tools either assume the input C is well-defined (no UB), or they insert checks/conversions for certain UB cases, accepting that the Rust output may safely abort or diverge from C’s unpredictable behavior. These challenges require a combination of parsing technology, AST analysis, dataflow/alias analysis, and sometimes dynamic or heuristic methods to produce correct and safe Rust code. &amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Semantics-Preserving Transpilers ==&lt;br /&gt;
Early and foundational work on C-to-Rust translation has focused on &#039;&#039;&#039;semantic preservation&#039;&#039;&#039; – generating Rust code that is as close as possible to the original C in behavior and structure. These tools prioritize correctness and completeness of translation, emitting Rust that may not be idiomatic but can be compiled and run to mirror the C program’s output (assuming no UB). Because Rust is a safer language, such translations typically mark the unsafe portions explicitly, resulting in a Rust program that compiles and runs, but largely bypasses Rust’s safety checks by using &amp;lt;code&amp;gt;unsafe&amp;lt;/code&amp;gt; constructs.&lt;br /&gt;
&lt;br /&gt;
=== C2Rust ===&lt;br /&gt;
&#039;&#039;&#039;C2Rust&#039;&#039;&#039; is one of the most prominent C-to-Rust transpilers and is actively maintained (primarily by Immunant and Galois). It parses C source using &#039;&#039;&#039;Clang&#039;&#039;&#039; (supporting C99) and then programmatically generates corresponding Rust code. The design goal is to translate &#039;&#039;most C code into semantically equivalent Rust&#039;&#039; without manual intervention. C2Rust leverages Clang’s type information to produce Rust with matching types and function signatures, ensuring that the translated modules can link with code compiled from the original C (useful for incremental migration). Arbitrary control flow (including &amp;lt;code&amp;gt;goto&amp;lt;/code&amp;gt; and complex loops) is handled by using Emscripten’s &#039;&#039;&#039;Relooper&#039;&#039;&#039; algorithm to structure Rust &amp;lt;code&amp;gt;if/while&amp;lt;/code&amp;gt; constructs that emulate the original flow. For example, computed &amp;lt;code&amp;gt;goto&amp;lt;/code&amp;gt; or irreducible loops are transformed into state-machine like Rust code with explicit &amp;lt;code&amp;gt;break&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;continue&amp;lt;/code&amp;gt; logic, since Rust has no direct &amp;lt;code&amp;gt;goto&amp;lt;/code&amp;gt;. &amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
C2Rust’s output is intentionally close to the C source. Pointers in C become raw pointers in Rust (&amp;lt;code&amp;gt;&#039;&#039;mut T&amp;lt;/code&amp;gt; or &amp;lt;code&amp;gt;&#039;&#039;const T&amp;lt;/code&amp;gt;), pointer arithmetic is translated using &amp;lt;code&amp;gt;.offset()&amp;lt;/code&amp;gt; or integer address calculations, and manual memory management calls (e.g. &amp;lt;code&amp;gt;malloc&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;free&amp;lt;/code&amp;gt;) remain as calls to libc equivalents in Rust. The emphasis is on &#039;&#039;&#039;fidelity&#039;&#039;&#039;: the Rust code will behave the same as the C code, down to low-level details, provided the C program had defined behavior. As a result, the initial translated Rust is often littered with &amp;lt;code&amp;gt;unsafe&amp;lt;/code&amp;gt; blocks and can look “unidiomatic” or overly verbose. For instance, array indexing in C might be translated to pointer arithmetic in Rust (&amp;lt;code&amp;gt;*p.offset(i as isize)&amp;lt;/code&amp;gt; rather than a safe slice index) if the translator cannot prove it’s safe to use Rust slices. Indeed, early users noted that C2Rust’s output “just converts C to rather painful Rust” that mirrors pointer manipulations. This is a conscious design choice to avoid changing program semantics. C2Rust does not automatically convert C’s &#039;&#039;&#039;structs&#039;&#039;&#039; or pointer-heavy data structures into richer Rust types; they remain as struct definitions and pointer fields in Rust, preserving memory layout so that external binary interfaces (ABI) remain compatible. &amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref6&amp;quot;/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
From an &#039;&#039;&#039;implementation&#039;&#039;&#039; standpoint, C2Rust originally built Rust ASTs using &amp;lt;code&amp;gt;librustc&amp;lt;/code&amp;gt; internals, but it has since been refactored to use the stable &amp;lt;code&amp;gt;syn&amp;lt;/code&amp;gt; library for Rust code generation. This decoupling from Rust compiler internals has made C2Rust easier to maintain and install. The tool operates via a command-line that takes a &#039;&#039;&#039;compile commands database&#039;&#039;&#039; (e.g. from CMake or Bear) and processes each C file into a corresponding Rust file. It supports transpiling large projects module-by-module. Not every C construct is handled (for example, C99 variable-length arrays and certain GCC extensions may not be fully supported), but known unsupported features are explicitly warned or skipped. C2Rust assumes the input is portable C99; it runs the C preprocessor and thus the translation is platform-specific (it will embed definitions as seen on the host platform, which means the Rust output might be specific to the host OS/architecture). &amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref7&amp;quot;/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Undefined behavior handling:&#039;&#039;&#039; C2Rust largely carries over the original logic into Rust &amp;lt;code&amp;gt;unsafe&amp;lt;/code&amp;gt; code without adding safety checks, so memory errors in C would still be possible in the Rust output (now confined to &amp;lt;code&amp;gt;unsafe&amp;lt;/code&amp;gt; blocks). For certain UB cases that Rust &#039;&#039;cannot&#039;&#039; represent directly, C2Rust has to make a choice. For instance, C allows reading uninitialized values (UB), whereas Rust forbids using an uninitialized variable. C2Rust’s strategy in such cases is typically to initialize variables to a default when necessary or use constructs like &amp;lt;code&amp;gt;std::mem::MaybeUninit&amp;lt;/code&amp;gt; to represent them, to ensure the Rust compiles. For integer overflow, as noted, it doesn’t insert any special wrapping: it relies on Rust’s standard overflow semantics (two’s complement wrap in release builds, panic in debug). If the C code relies on overflow behavior, this could introduce a divergence (but since relying on signed overflow is UB in C, C2Rust chooses a reasonable Rust default rather than trying to exactly mimic the undefined). Overall, C2Rust’s priority is that the Rust program &#039;&#039;&#039;compiles and reproduces the C program’s functionality&#039;&#039;&#039; for all well-defined cases. Memory safety is not improved in this first-stage translation – the output Rust is essentially an “unsafe Rust” codebase. To help evolve this into idiomatic safe Rust, C2Rust was designed with subsequent refactoring in mind. The project provides (or in recent versions, plans to provide) additional tools to gradually refactor the transpiled code into more idiomatic and safe Rust by applying automated transformations. For example, there were &amp;lt;code&amp;gt;c2rust-refactor&amp;lt;/code&amp;gt; tools for doing things like renaming, or converting certain pointer patterns to references, though an “exciting new approach” to automate deeper unsafe-to-safe rewrites is under development. &amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref7&amp;quot;/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Corrode ===&lt;br /&gt;
&#039;&#039;&#039;Corrode&#039;&#039;&#039; is another early C-to-Rust translator, developed by Jamey Sharp in 2016-2017, written in Haskell. It uses the &#039;&#039;language-c&#039;&#039; Haskell library as a C parser and similarly attempts to emit equivalent Rust code. Corrode was an inspiration for C2Rust – in fact, C2Rust’s authors acknowledge borrowing ideas from Corrode’s design. The overarching philosophy of Corrode is to &#039;&#039;&#039;preserve the original program’s behavior, API, and structure as much as possible&#039;&#039;&#039;. Like C2Rust, it outputs Rust code intended to act as a drop-in replacement for the C, not changing data representations or logic. For example, Corrode will translate a C global array or pointer arithmetic in a way that keeps the same memory layout and computations in Rust. Its maintainers explicitly note that if the input program is free of undefined and implementation-defined behavior, the Rust output should behave &#039;&#039;exactly&#039;&#039; the same. In cases of C undefined behavior, Corrode makes a best-effort guess at a sensible Rust translation. A concrete example given in Corrode’s documentation is how it handles &#039;&#039;&#039;overflowing arithmetic&#039;&#039;&#039;: in C, signed overflow is UB, but Corrode translates it to Rust’s &amp;lt;code&amp;gt;+&amp;lt;/code&amp;gt; operator on integers, which will yield panics in debug mode (or two’s-complement wrapping in release mode). This choice means the Rust program might crash in debug builds when the C would have overflowed silently, but it avoids introducing silent wrong results—favoring a predictable failure over undefined behavior. &amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Corrode’s output, much like C2Rust’s, is initially &#039;&#039;unsafe Rust&#039;&#039;. It uses raw pointers and &amp;lt;code&amp;gt;unsafe&amp;lt;/code&amp;gt; for operations that Rust cannot verify. However, Corrode did make some attempts at slightly more idiomatic constructs in simple cases. For instance, Corrode can recognize certain C loop patterns. A common pattern is a &amp;lt;code&amp;gt;for&amp;lt;/code&amp;gt; loop incrementing an index; Corrode will translate a C &amp;lt;code&amp;gt;for(int i=0; i&amp;amp;lt;i_max; ++i)&amp;lt;/code&amp;gt; into a Rust &amp;lt;code&amp;gt;for i in 0..i_max&amp;lt;/code&amp;gt; range loop. This is possible when the loop fits a clear Rust iterator pattern. Such transformations improve readability without altering semantics. Another example is converting some uses of C’s &amp;lt;code&amp;gt;NULL&amp;lt;/code&amp;gt; to Rust’s &amp;lt;code&amp;gt;Option&amp;lt;/code&amp;gt; type when context allows (though this is limited). Despite these niceties, Corrode’s general output still contains many unsafe pointers. Like C2Rust, it doesn’t magically introduce Rust’s ownership tracking where none existed in C. Its focus is a correct translation that &#039;&#039;compiles&#039;&#039;, leaving deeper safety improvements to be done later (likely by a human). Corrode also strives to maintain C ABI compatibility, meaning one could replace one C file of a project with Corrode’s Rust output and link it with the remaining C. This constrained Corrode to avoid changing data layouts or function signatures. &amp;lt;ref name=&amp;quot;ref6&amp;quot;/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
It should be noted that as of late 2010s, Corrode is &#039;&#039;&#039;no longer actively developed&#039;&#039;&#039;. Its author announced in 2018 that he would deprecate Corrode in favor of C2Rust, acknowledging that C2Rust’s more extensive engineering effort made it a better foundation going forward. Corrode’s last updates were around 2017, and while the repository is still available, it may not be compatible with the latest Rust compilers or handle newer C features. Nonetheless, Corrode is historically important as a pioneering tool, and its design principles (preserve behavior, keep code maintainable, allow gradual adoption) have influenced subsequent projects. &amp;lt;ref name=&amp;quot;ref8&amp;quot;/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Safety-Oriented Translation Tools ==&lt;br /&gt;
While direct transpilers like C2Rust produce a Rust version of the C code, they do not immediately yield the &#039;&#039;safe&#039;&#039;, idiomatic Rust that human developers would write. Recognizing this, a number of tools and research efforts have focused on **translating C to &#039;&#039;safer&#039;&#039; Rust**, going beyond rote syntax conversion to attempt enforcement of Rust’s safety guarantees. These tools typically start from code that is equivalent to the C (often leveraging output from C2Rust or a similar baseline), and then apply analyses or transformations to refactor &amp;lt;code&amp;gt;unsafe&amp;lt;/code&amp;gt; constructs into safe ones (like replacing raw pointers with references, or introducing Rust types that eliminate unchecked operations). This process is challenging because it requires reasoning about aliasing and lifetimes in the C code. Below, we discuss some representative safety-oriented tools that are under active development or study.&lt;br /&gt;
&lt;br /&gt;
=== Laertes (Ownership Inference Post-Translation) ===&lt;br /&gt;
&#039;&#039;&#039;Laertes&#039;&#039;&#039; is a tool introduced by Emre et al. (OOPSLA 2021) that aims to &#039;&#039;&#039;lift C2Rust-generated code into safer Rust&#039;&#039;&#039; by inferring ownership and lifetimes for pointers. The input to Laertes is essentially the output of C2Rust (or an equivalent unsafe Rust translation). Laertes then tries to automatically convert as many raw pointers as possible into Rust references (&amp;lt;code&amp;gt;&amp;amp;amp;&amp;lt;/code&amp;gt; or &amp;lt;code&amp;gt;&amp;amp;amp;mut&amp;lt;/code&amp;gt;) which the Rust borrow checker can verify. The core algorithm is an &#039;&#039;&#039;iterative, compiler-guided inference&#039;&#039;&#039;: Laertes optimistically assumes all pointers can be safely transformed into references and assigns them provisional lifetime parameters. It then attempts to compile the program. Where the Rust compiler throws errors (due to violations of borrowing rules or type mismatches), Laertes analyzes the error and “backs off” that assumption for the offending pointers. For example, if two pointers were actually aliasing in a way that Rust’s rules disallow for references, the compiler would complain, and Laertes would revert those pointers to raw pointers (unsafe) in the next iteration. Under the hood, Laertes uses a combination of pointer analyses (Steensgaard’s inclusion-based analysis and Andersen’s subset-based points-to analysis) to propagate constraints and determine which pointers must remain unsafe. It effectively treats the Rust compiler’s borrow checker as an oracle: any pointer that can be converted to a reference without compiler errors is assumed to be safe to do so. &amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Laertes can automatically introduce lifetime annotations in function signatures and struct definitions so that converted references have proper lifetimes. It also distinguishes owning pointers vs. borrowing pointers as part of its inference (e.g., a pointer that is only ever freed in one place might be turned into a Rust &amp;lt;code&amp;gt;Box&amp;lt;/code&amp;gt; or an owned value). However, Laertes is &#039;&#039;&#039;conservative&#039;&#039;&#039; in what it attempts. It completely ignores any pointer involved in patterns that inherently require unsafe code, such as pointer arithmetic, union use, or casting to/from integers. Only pointers that are “unsafe solely because C lacks explicit lifetime/ownership” are candidates. In their study, this turned out to be a relatively small subset – about 11% of raw pointers on average could be handled by Laertes’s approach. The rest still needed to stay as raw pointers due to other complicating factors (like global variables, complex aliasing, etc.). In other words, Laertes could make a portion of the C2Rust output safe, but most of the code remained untouched. Subsequent research (Emre et al., OOPSLA 2023) examined the limits of this approach, finding that even if one pre-processes the code to remove other causes of unsafety, inferring correct lifetimes and ownership for all pointers is extremely difficult. Nonetheless, Laertes demonstrated that an automated tool can &#039;&#039;safely convert a non-trivial fraction of raw pointers to references&#039;&#039;, eliminating those memory-unsafe pieces from the code. The advantage of Laertes’ method is that it &#039;&#039;&#039;guarantees preservation of behavior&#039;&#039;&#039; (it only changes pointers where the Rust compiler confirms no borrowing rule is violated) – so any pointer it converts to a reference is, by construction, not involved in unsafe aliasing. This means the translated program remains functionally equivalent to the original C (assuming the original had no UB) but now enjoys some enforced safety. The limitation is simply that many pointers cannot be transformed with this approach, so manual effort or more powerful analyses are needed for those. &amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Crown (Static Ownership Analysis) ===&lt;br /&gt;
&#039;&#039;&#039;Crown&#039;&#039;&#039; (Zhang et al., 2023) is another tool that automates conversion of C pointers to Rust references, but it takes a different approach from Laertes. Instead of relying on trial-and-error with the compiler, Crown uses a more &#039;&#039;&#039;static, ownership-oriented analysis&#039;&#039;&#039; to determine which pointers can be safely replaced by references. According to its description, Crown performs a whole-program ownership inference that can handle a larger set of pointers than Laertes, which was restricted by needing a clean compile for each guess. Essentially, Crown analyzes the C code (or the unsafe Rust) to identify data that have a single owner or no concurrent aliases, allowing those to become &amp;lt;code&amp;gt;Box&amp;lt;/code&amp;gt; (owned heap allocations) or &amp;lt;code&amp;gt;&amp;amp;amp;mut&amp;lt;/code&amp;gt; (unique mutable references) in Rust. By tracking the flow of pointers through assignments and function calls, it can sometimes determine, for example, that a pointer is only ever used in one context and is not aliased elsewhere, qualifying it for safe transformation. The Crown paper notes that it “employs ownership analysis to facilitate the replacement of a larger number of pointers compared to Laertes”. In contrast to Laertes’s iterative feedback loop, Crown’s approach is more direct: it computes a model of which pointers are exclusive, which are shared read-only, etc., and then rewrites the code accordingly in one go. &amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Crown is able to convert not only simple function-local pointers but also more complex cases, thanks to its analysis of how pointers are passed and returned (ensuring the callee doesn’t stash a reference in a global or somewhere escaping the caller’s scope, for instance). Any pointer conversions Crown performs are vetted by compiling the resulting program – the output must still pass Rust’s borrow checker, meaning Crown’s static analysis must be sound. If Crown is too optimistic, the Rust code won’t compile, so presumably the implementation either avoids unsound replacements or would need to catch errors and adjust (the details in literature suggest Crown was successful on their benchmarks). In addition, Crown addresses some issues outside the scope of Laertes. For example, &#039;&#039;Concrat&#039;&#039; (Hong and Ryu, 2023) is a complementary tool cited alongside Crown that specifically targets replacing certain C library calls with safer Rust equivalents (such as replacing C’s &amp;lt;code&amp;gt;pthread_mutex&amp;lt;/code&amp;gt; and lock functions with Rust’s &amp;lt;code&amp;gt;std::sync::Mutex&amp;lt;/code&amp;gt; API). This indicates the broader effort to not only transform pointers but also other unsafe patterns. Crown and related techniques are currently research prototypes and are not as plug-and-play as C2Rust. They often require the code to be translated by C2Rust first, then a separate phase to apply the safe refactor. Nevertheless, Crown represents the direction of using deeper program analyses to push the boundary of what can be automatically made safe, moving closer to the TRACTOR program’s goal of human-quality Rust output. By using static ownership reasoning, Crown can handle cases that would stump a purely local or trial-based approach – for instance, inferring that a struct and all its pointers are encapsulated in one module and can be given lifetimes that make the whole module safe. &amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== CRustS (Semantics-Relaxed Source Rewriting) ===&lt;br /&gt;
&#039;&#039;&#039;CRustS&#039;&#039;&#039; (Ling et al., 2022) takes yet another approach: it applies a series of &#039;&#039;&#039;source-to-source transformation rules&#039;&#039;&#039; to the C2Rust output (unsafe Rust) with the aim of significantly increasing the proportion of code that is safe, even if it means slightly &#039;&#039;&#039;relaxing the requirement of full semantic preservation&#039;&#039;&#039;. The philosophy here is that &#039;&#039;if&#039;&#039; we allow minor changes to program behavior (preferably in cases that don’t affect intended functionality), we can achieve much safer Rust code automatically. CRustS is implemented using TXL (a source transformation language) and defines 220 rewrite rules that pattern-match on Rust code and transform it. Of these, 198 rules are strictly semantics-preserving (similar to what Corrode or Laertes would do – only reorganizing code without changing its meaning), and 22 are &#039;&#039;semantics-approximating&#039;&#039;. The approximating transformations deliberately make trade-offs that might alter corner-case behavior but remove &amp;lt;code&amp;gt;unsafe&amp;lt;/code&amp;gt; constructs. For example, one approximating rule might convert a raw pointer used for array access into a safe slice, filling in a default value for out-of-bounds indices rather than exactly mimicking a buffer overrun (which is UB anyway). Another might replace an unsafe union with an enum that covers common cases but might not preserve exact bit-level behavior for unsupported union variants. The idea is to eliminate as many unsafety causes as possible, then let the Rust compiler’s checks and tests ensure the program still behaves correctly for expected inputs. &amp;lt;ref name=&amp;quot;ref11&amp;quot;/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
CRustS reports impressive improvements in safety: applying these rules to C2Rust-translated code yielded a &#039;&#039;&#039;significantly higher ratio of safe code&#039;&#039;&#039;, even reaching function-level safe code ratios comparable to idiomatic Rust projects. In comparison to Laertes, which could only handle a small subset of pointers, CRustS claims a much higher conversion rate (their paper notes that on Laertes’s own benchmarks, CRustS achieved a higher safe-pointer ratio). The cost, of course, is that some transformations are not guaranteed semantics-preserving in all cases. For instance, CRustS might assume that certain global variables can be made immutable or that certain pointer casts are never meant to alias incompatible types – assumptions that a human would validate but the tool can only guess. Because of this, CRustS is presented as a “demo” or research prototype – it’s a proof that by bending the rules, one can get far more safe code automatically. In practice, one would likely use CRustS’s output as a starting point and then run test suites or formal verification to ensure nothing critical broke. The CRustS pipeline is essentially: run C2Rust to get an unsafe Rust version, then run CRustS (which is a separate tool) to refactor that code. Installation instructions show it builds on top of C2Rust’s output. This two-phase approach underscores that CRustS doesn’t parse C directly; it relies on Rust code input. This is reasonable since TXL rules are applied to the Rust abstract syntax tree or source. &amp;lt;ref name=&amp;quot;ref11&amp;quot;/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Examples of CRustS transformations&#039;&#039;&#039; (deduced from its description and related work) likely include: converting pointer arithmetic into indexed slice accesses when possible (with bounds checks), turning C-style string manipulation with &amp;lt;code&amp;gt;char&#039;&#039;&amp;lt;/code&amp;gt; into usage of Rust &amp;lt;code&amp;gt;String&amp;lt;/code&amp;gt; or &amp;lt;code&amp;gt;&amp;amp;amp;str&amp;lt;/code&amp;gt; where lengths are known, and simplifying macro-expanded code. It may also introduce safe wrappers: e.g., if a C function returns a pointer that must be freed by the caller, CRustS could change it to return a &amp;lt;code&amp;gt;Box&amp;amp;lt;T&amp;amp;gt;&amp;lt;/code&amp;gt; (owned pointer) in Rust, thus transferring ownership and making the deallocation deterministic (this might be a semantics-preserving change if done carefully, or semantics-relaxed if the C code sometimes leaked or reused that pointer in unusual ways). The end result of CRustS is a Rust codebase that has far fewer &amp;lt;code&amp;gt;unsafe&amp;lt;/code&amp;gt; blocks. Its authors measured that a majority of functions became &#039;&#039;&#039;completely safe Rust&#039;&#039;&#039; after transformation (95% of functions in their sample had no unsafe code inside, up from a much smaller percentage originally). This is a huge step towards the goal of automatic full migration. However, a caveat noted by other researchers is that CRustS’s safety might be &#039;&#039;shallow* – e.g., making each function body safe doesn’t guarantee the entire program is free of logical memory errors if some unsafe operations were hidden behind foreign calls or relaxed rules. In any case, CRustS is a valuable approach exploring the frontier between strict semantic equivalence and practical safety gains. &amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Other Notable Efforts ===&lt;br /&gt;
Beyond the tools above, there are other specialized efforts. &#039;&#039;Concrat&#039;&#039; (2023) was mentioned earlier – it focuses on replacing C concurrency primitives with Rust’s standard library equivalents using dataflow analysis. This addresses a niche (but important) aspect: translating C’s pthreads usage into Rust’s thread and mutex types safely. Another area of focus has been C’s &#039;&#039;&#039;union types&#039;&#039;&#039;. One research work titled “To Tag or Not to Tag” (2023) examines how to automatically translate C unions into Rust in a safer way by introducing tagged enums or on-demand initialization of union fields. Unions are tricky because a C union allows reinterpretation of memory in incompatible ways. Tools like C2Rust leave unions as Rust &amp;lt;code&amp;gt;union&amp;lt;/code&amp;gt; (which still requires unsafe usage when accessing), but experimental transforms can wrap union accesses in safe enums or generate accessors that maintain a tag to track the active union variant. These kinds of targeted solutions could eventually be composed into larger translation pipelines. &amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== LLM-Assisted Translation Approaches ==&lt;br /&gt;
In recent years, the emergence of powerful &#039;&#039;&#039;Large Language Models (LLMs)&#039;&#039;&#039; for code (such as OpenAI’s Codex/GPT or similar) has opened a new frontier for automatic code translation, including C to Rust. LLMs can be prompted with C code and asked to produce Rust code, and often they will generate more &#039;&#039;idiomatic&#039;&#039; Rust by drawing on high-level patterns rather than doing a one-to-one translation. For example, an AI might translate a C buffer + length pair into a Rust slice, or use high-level library functions in Rust to replace low-level C loops. This is promising in terms of producing human-like Rust code, but LLM-based translation has its own challenges. Chief among them: the LLM might produce code that &#039;&#039;&#039;looks&#039;&#039;&#039; plausible but is semantically incorrect or fails to compile. Unlike a deterministic compiler-based tool, an LLM might miss subtle aspects of the C semantics, especially for larger functions or when complex pointer manipulation is involved. Additionally, LLMs have context length limits, making it hard to directly feed large codebases. &lt;br /&gt;
&lt;br /&gt;
To harness the strengths of both worlds, some projects combine traditional analysis with LLMs – a &#039;&#039;neuro-symbolic&#039;&#039; approach. &#039;&#039;&#039;C2SaferRust&#039;&#039;&#039; (Nitin et al., 2023) is one such approach that uses C2Rust &#039;&#039;and then&#039;&#039; uses an LLM to improve the safety of the code. In C2SaferRust’s pipeline, first C2Rust generates the baseline unsafe Rust. Then the code is automatically sliced into small, independent chunks that an LLM (GPT-4 in their case) can handle. Each chunk (for example, a single function or a group of related functions) is given to the LLM with instructions to produce a safer Rust version of that code. The LLM can thereby suggest, say, using a Rust &amp;lt;code&amp;gt;Vec&amp;amp;lt;T&amp;amp;gt;&amp;lt;/code&amp;gt; instead of a raw array pointer + length, or using &amp;lt;code&amp;gt;Option&amp;lt;/code&amp;gt; for possibly-null returns, etc., effectively performing a context-aware refactoring. After getting the LLM’s suggestions for all parts, C2SaferRust reassembles the program and runs its test suite to verify correctness. This last step is crucial – it compensates for the fact that the LLM might introduce mistakes. If a test fails, the process could be iterated or the problematic section flagged for manual review. On a benchmark of real C projects, C2SaferRust was able to automatically reduce the number of raw pointer usages by up to 38% and overall &amp;lt;code&amp;gt;unsafe&amp;lt;/code&amp;gt; code by up to 28%, &#039;&#039;while all tests still passed&#039;&#039;. This demonstrates measurable progress: the resulting code is closer to idiomatic Rust (significantly fewer unsafe blocks), and it still behaves correctly on the tested functionality. C2SaferRust also outperformed some prior techniques like Laertes on those benchmarks, showing the benefit of letting an LLM propose creative refactorings that pure static tools might not attempt. &amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Other LLM-based tools include &#039;&#039;&#039;Flourine&#039;&#039;&#039; and &#039;&#039;&#039;Vert&#039;&#039;&#039; (both discussed in a 2024 study). Flourine uses a &#039;&#039;test-driven repair&#039;&#039; strategy: it generates two versions of Rust code for a given C input – one via a safe-but-idiomatic guess from an LLM, and one via a more literal translation (e.g., through an intermediate WebAssembly representation to ensure correctness) – then it fuzz-tests the outputs to find behavioral differences and tries to automatically fix any discrepancies. The idea is to combine the reliability of a baseline translation with the elegance of an LLM translation. Vert, on the other hand, emphasizes breaking the input program into &#039;&#039;vertically sliced components&#039;&#039; that an LLM can handle piecewise, attempting to ensure that dependencies are managed when merging the LLM-generated parts. Despite these sophisticated strategies, the results so far show that &#039;&#039;&#039;LLM-based translation still struggles with large or complex code&#039;&#039;&#039;. One report noted that less than 20% of C programs over 150 lines could be satisfactorily translated by an LLM-based method without manual intervention. The necessity to split programs into small chunks is a major hurdle – if a function &amp;lt;code&amp;gt;f&amp;lt;/code&amp;gt; calls &amp;lt;code&amp;gt;g&amp;lt;/code&amp;gt;, the LLM ideally needs to see both to produce consistent Rust, otherwise it might generate conflicting versions of &amp;lt;code&amp;gt;g&amp;lt;/code&amp;gt; when given different contexts. Indeed, experiments with Flourine and Vert found that when functions were translated independently, it led to inconsistencies that had to be reconciled, and translating whole programs in one prompt often exceeded LLM capacity or produced non-compiling output. &amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
That said, LLMs have shown they can handle many &#039;&#039;syntactic&#039;&#039; or even somewhat semantic conversions gracefully on smaller snippets. They excel at producing idiomatic constructs: for example, using Rust’s &amp;lt;code&amp;gt;Iterator&amp;lt;/code&amp;gt; APIs instead of C-style loops, or leveraging pattern matching and &amp;lt;code&amp;gt;Result&amp;lt;/code&amp;gt; types for error handling. These high-level refactorings are something that pure static tools won’t do unless explicitly programmed for each pattern. As LLMs improve and as techniques like guided prompting, automated verification, and iterative refinement advance, we can expect LLM-assisted translation to become more reliable. It’s likely that the future of automatic C to Rust translation will involve a hybrid: using static analysis to break down and formally understand the source, and using LLMs or other AI to suggest the more complex transformations that require semantic understanding or creative restructuring, all under the validation of tests or formal checks. &amp;lt;ref name=&amp;quot;ref1&amp;quot;/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Comparative Analysis of Tools ==&lt;br /&gt;
The landscape of C-to-Rust translators can be compared along several axes: &#039;&#039;&#039;fidelity vs. idiomaticity&#039;&#039;&#039;, &#039;&#039;&#039;safety guarantees&#039;&#039;&#039;, &#039;&#039;&#039;automation scope&#039;&#039;&#039;, and &#039;&#039;&#039;maturity&#039;&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Translation Fidelity:&#039;&#039;&#039; Tools like &#039;&#039;C2Rust&#039;&#039; and &#039;&#039;Corrode&#039;&#039; prioritize fidelity. They retain the exact behavior and even the structure of the C code in the generated Rust (making only minimal adjustments needed for Rust syntax). This means they have very high success in translating arbitrary C code (including low-level tricks) into &#039;&#039;compilable&#039;&#039; Rust. However, the Rust they produce is often verbose and non-idiomatic – essentially a C program expressed in Rust syntax. At the other end, LLM-based approaches and aggressive refactoring tools sacrifice some fidelity for readability and safety. For instance, an AI might refactor a C loop into a Rust iterator, which is more idiomatic but could conceivably alter performance characteristics or behavior in edge cases. Tools like Laertes and Crown try to walk the line: they keep the overall structure from C2Rust, but on the subset of code they can handle, they introduce Rust references (which is a semantic change – e.g., if in C two aliased pointers were written through, translating both to &amp;lt;code&amp;gt;&amp;amp;amp;mut&amp;lt;/code&amp;gt; would be incorrect and is avoided). These tools ensure fidelity by &#039;&#039;only&#039;&#039; making changes that Rust’s compiler or analyses can guarantee are safe (thus presumed semantically valid). &amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref6&amp;quot;/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Safety and Memory Guarantees:&#039;&#039;&#039; The primary benefit of translating C to Rust is potential memory safety, but not all tools achieve this equally. C2Rust’s direct output provides &#039;&#039;&#039;no additional memory safety&#039;&#039;&#039; beyond the original C – it compiles under &amp;lt;code&amp;gt;unsafe&amp;lt;/code&amp;gt; and the programmer must manually inspect or refactor to get safety. Corrode is similar in this regard. In contrast, Laertes and Crown explicitly aim to &#039;&#039;increase&#039;&#039; memory safety by converting pointers to safe references wherever possible. The result is a partially safe program: some parts are protected by Rust’s compile-time checks (no data races or use-after-frees on those portions), while other parts remain &amp;lt;code&amp;gt;unsafe&amp;lt;/code&amp;gt;. CRustS pushes further, attempting to maximize the safe portion by even altering some semantics if necessary. Its output can be predominantly safe Rust code (with runtime checks like bounds checking in place of what were raw pointer operations) – a big step in guarantees, although a developer may need to verify that the semantics deviations are acceptable. LLM-based translations often &#039;&#039;target&#039;&#039; completely safe Rust (no &amp;lt;code&amp;gt;unsafe&amp;lt;/code&amp;gt; at all), because they try to use high-level Rust features. When they succeed, the code has all of Rust’s guarantees (e.g., any out-of-bounds access would be caught as a panic, not a memory violation). However, the caveat is that if an LLM is unsure how to handle some low-level operation, it might omit necessary functionality or mis-handle it, leading to an incomplete translation that might not even compile. In a controlled setting with tests, the hope is to get the best of both: safe Rust that passes all the same tests as the C. Notably, when Rust translations enforce safety, they inherently change how &#039;&#039;&#039;formerly undefined behaviors&#039;&#039;&#039; manifest – rather than silent memory corruption, you might get a Rust panic or error for things like buffer overflow or null dereference. This is usually considered a desirable outcome (failing fast instead of continuing with corruption). &amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref11&amp;quot;/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Idiomatic Quality and Maintainability:&#039;&#039;&#039; From a maintainability standpoint, the closer the output is to how a human would have written it, the better. On this front, basic transpilers score low – they tend to produce code that, while correct, is hard for Rust programmers to work with or modify. Variables might have awkward names or extra casts, and everything is wrapped in unsafe. Tools that do even modest refactoring help a lot: e.g., Corrode’s loop translation or C2Rust’s upcoming refactoring successor can turn clunky patterns into cleaner Rust forms. The most idiomatic results currently come from &#039;&#039;&#039;AI-based translations&#039;&#039;&#039; or very high-level refactor rules. For example, an LLM might recognize a C idiom for error handling (set an integer status and jump to cleanup) and translate it into Rust using the &amp;lt;code&amp;gt;?&amp;lt;/code&amp;gt; operator and &amp;lt;code&amp;gt;Result&amp;lt;/code&amp;gt; type – a very idiomatic construct. Such transformations are beyond the current static tools’ pattern matching (which tends to be local). RustMap (2025) is a research prototype that attempts project-scale migration by analyzing the C code for patterns that can be globally replaced by Rust equivalents, such as turning C macros into Rust &amp;lt;code&amp;gt;const&amp;lt;/code&amp;gt; or functions, or identifying groups of functions that operate on a data structure and converting that whole data structure into a Rust struct with methods. This yields more idiomatic designs rather than line-by-line translation. Across the board, there is a trade-off: the more idiomatic the Rust, the more the tool must &#039;&#039;&#039;understand the code’s intent&#039;&#039;&#039;, which is hard to do automatically. Thus, the most idiomatic translations may require AI and carry a risk of misinterpretation. &amp;lt;ref name=&amp;quot;ref6&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref13&amp;quot;/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Tool Maturity and Ecosystem:&#039;&#039;&#039; C2Rust is fairly mature and has been used on real-world projects; it is open-source (BSD-licensed), with an active community (over 4k stars on GitHub) and recent releases (as of 2025). It supports integration into build systems via compile command databases, making it practical for large codebases. Corrode, conversely, is discontinued, and while one can still use it on small code, it may not handle newer C or Rust versions well. The safety-oriented tools like Laertes and CRustS are research prototypes – they have published artifacts or code (Laertes’s artifact and dataset were released for evaluation, and CRustS is available as a Cargo package for demo purposes), but they are not one-click solutions and may require expertise to apply. We can consider them as early explorations likely to be incorporated into future pipelines. LLM-based tools currently are mostly in academic or experimental stages; they are not broadly available as off-the-shelf products, partly because of the cost and complexity (LLM APIs, managing prompts, etc.). However, one can already experiment with LLMs via services (for instance, there are online converters using GPT-4 that attempt C to Rust conversion, though their reliability varies). The DARPA TRACTOR program is likely to spawn more integrated tools in the next few years that combine these techniques. &amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref11&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref14&amp;quot;/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
In summary, &#039;&#039;&#039;no single tool yet completely automates a perfect translation of C to fully safe and idiomatic Rust for large codebases&#039;&#039;&#039;. C2Rust provides a strong foundation with complete and correct (but unsafe) translations. On top of that foundation, tools like Laertes, Crown, and CRustS layer increasingly powerful analyses to convert chunks to safe code. Meanwhile, AI-driven approaches offer glimpses of near-human-like translations, excelling in small scopes but struggling to scale. The current best practice for migrating C to Rust might be to use a transpiler (like C2Rust) to do the heavy lifting of rote translation, and then incrementally apply automated refactors (where possible) and manual improvements for critical sections – essentially a human-in-the-loop approach. The ongoing research and development aim to reduce the amount of human effort needed by making the automated steps smarter and more comprehensive.&lt;br /&gt;
&lt;br /&gt;
== Handling of Undefined Behavior and Edge Cases ==&lt;br /&gt;
A critical aspect of translation is how tools deal with &#039;&#039;&#039;undefined behavior (UB)&#039;&#039;&#039; and low-level C quirks. As mentioned, most transpilers assume that the C code is &#039;&#039;reasonably well-behaved&#039;&#039;; they don’t attempt to make an incorrect C program safer or correct. If the C code has latent bugs (like buffer overflows), the translated Rust (with &amp;lt;code&amp;gt;unsafe&amp;lt;/code&amp;gt; code) will have the same bug. However, when translation tools attempt to produce &#039;&#039;safe&#039;&#039; Rust, they inherently must address UB because safe Rust cannot express certain dangerous operations without a check. One straightforward example is &#039;&#039;&#039;buffer overflows&#039;&#039;&#039;: C might let you increment a pointer past the end of an array (UB if dereferenced). If a tool converts that array and pointer into a Rust slice, any out-of-bounds access will cause a runtime panic in Rust. Thus, the &#039;&#039;behavior&#039;&#039; diverges: the C might have continued (possibly corrupting memory or misbehaving later), whereas the Rust will immediately abort on a bounds check failure. Researchers categorize this as an expected difference – the Rust translation “eliminates potentially unsafe behavior” and replaces it with a controlled failure. In a sense, the Rust version is &#039;&#039;safer by design&#039;&#039;: it won’t blindly continue after an out-of-bounds, thereby avoiding exploitation, though it might not match an ill-defined C outcome (there is no meaningful “equivalent” to a wild memory write in safe Rust except termination). &amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Similarly, for &#039;&#039;&#039;null pointers&#039;&#039;&#039;: C can have a null pointer that gets dereferenced (UB). A safe Rust translation might use &amp;lt;code&amp;gt;Option&amp;amp;lt;&amp;amp;amp;T&amp;amp;gt;&amp;lt;/code&amp;gt; and explicitly handle the null case (e.g., return an error or panic). This changes the control flow (the Rust might error out where the C would crash unpredictably). Tools like Laertes and Crown, when converting pointers to references, must ensure that those pointers were never null – otherwise the Rust code would introduce a new crash. They likely conservatively leave potentially-null pointers as raw &amp;lt;code&amp;gt;&#039;&#039;mut T&amp;lt;/code&amp;gt; (still unsafe) unless they can prove null cannot happen. CRustS’s approach of semantics relaxation might, for instance, initialize pointers that are observed to be null to a dummy object to avoid crashes – but that could mask a bug. The trade-off is complex: do we preserve a bug or guard against it? Most automatic tools lean toward &#039;&#039;guarding*, on the principle that avoiding UB is the goal (especially since the exact UB behavior can’t be preserved anyway). &lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Integer behaviors&#039;&#039;&#039; are another subtle area. C’s integer types can overflow silently (except when using compiler sanitizers). Rust’s safe arithmetic will panic on overflow in debug builds and wrap in release. Neither is “the same” as C’s UB (which could do anything, but on typical two’s complement hardware effectively wraps). Tools generally choose Rust’s native arithmetic, which means a translation is effectively giving defined semantics to what was undefined. Empirical studies using fuzzing have shown that Rust translations often exhibit different behavior from C when it comes to things like I/O or arithmetic edge cases, precisely because Rust checks things C doesn’t. For example, if a C program reads an invalid UTF-8 byte sequence into a &amp;lt;code&amp;gt;char&#039;&#039;&amp;lt;/code&amp;gt; and later prints it, in C this might just output garbled text or pass through bytes. A direct Rust translation using &amp;lt;code&amp;gt;std::string::String&amp;lt;/code&amp;gt; might refuse to encode those bytes (Rust &amp;lt;code&amp;gt;String&amp;lt;/code&amp;gt; requires UTF-8), causing a runtime error or loss of data. One study noted differences in how tools handle I/O encoding – some Rust translations assume UTF-8 and thus diverge when given non-UTF-8 input. These are not &#039;&#039;memory safety* issues per se, but they show how higher-level assumptions can creep in. &amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Setjmp/longjmp&#039;&#039;&#039; and other non-local jumps are typically not supported by Rust at all. C2Rust and others usually cannot translate a &amp;lt;code&amp;gt;longjmp&amp;lt;/code&amp;gt; in any straightforward way; they might either reject such code or transform it into a Rust &amp;lt;code&amp;gt;panic!&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;catch_unwind&amp;lt;/code&amp;gt; (which is not exactly the same, and is unsafe FFI with C). Similarly, inline assembly in C is often left as inline assembly in Rust (which is unstable and &amp;lt;code&amp;gt;unsafe&amp;lt;/code&amp;gt;), or the tool will bail out if it can’t handle it.&lt;br /&gt;
&lt;br /&gt;
In summary, automatic translators try to stay correct on defined behavior and &#039;&#039;usually&#039;&#039; either skip or embed as unsafe anything truly undefined. When aiming for safety, they effectively turn some UB into Rust runtime errors or compiler errors. This is generally acceptable since one goal is to use Rust’s safety to catch those issues. The key is that when evaluating these tools, one should not expect bit-for-bit identical behavior in scenarios that were undefined in C; rather, we expect that if the C program was intended to be correct, the Rust will be correct on the same inputs. When UB is unavoidable, the Rust will likely either refuse to compile (requiring human attention) or will contain safety abstractions that prevent the UB (possibly changing the program’s failure mode). Tools under active development are increasingly looking to integrate &#039;&#039;&#039;dynamic analysis&#039;&#039;&#039; (like running test cases or fuzzing) as part of the translation workflow to detect behavioral deviations caused by these issues and correct them. &amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Conclusion and Future Outlook ==&lt;br /&gt;
Automatic C-to-Rust translation has evolved from simple transpilers that emit line-by-line unsafe Rust into a multi-faceted field combining compiler theory, program analysis, and machine learning. Currently, if one needs to port a C project to Rust, a typical pipeline might involve using C2Rust to get an initial Rust codebase that is functionally correct but rife with &amp;lt;code&amp;gt;unsafe&amp;lt;/code&amp;gt;, then employing a mix of automated refactoring tools (like the experimental Laertes or CRustS) and manual rewrites to improve safety and idiomatic style. This incremental approach is necessary because fully automated translation &#039;&#039;at the level of a skilled human programmer&#039;&#039; is still an open problem. The active research (funded by initiatives like DARPA’s TRACTOR) indicates optimism that the gap can be closed. Future translators will likely integrate the static and dynamic techniques: for example, a tool could perform sophisticated alias analysis to convert most pointers to safe references (like Crown), use an LLM to rewrite low-level C patterns into high-level Rust (like using iterators or Rust libraries), and then validate the result with exhaustive testing or model checking. Each of those components is being prototyped today. &amp;lt;ref name=&amp;quot;ref1&amp;quot;/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
One can imagine a near-future tool that, given a C codebase, produces a Rust crate where memory safety issues are largely fixed, perhaps with annotations or reports for the developer for any remaining &amp;lt;code&amp;gt;unsafe&amp;lt;/code&amp;gt; bits that could not be resolved automatically. The developer can then focus their effort only on the truly tricky parts (for example, a complex bit-casting union or an exotic macro). In essence, the goal is to handle the “mundane 90%” of the translation automatically and safely, and leave the 10% of corner cases for human expertise – or further learning by the tool as these corner patterns repeat across projects.&lt;br /&gt;
&lt;br /&gt;
It’s also worth noting that the presence of a correct automatic translation tool could influence how people write C (knowing it will be machine-converted) or how they write Rust (perhaps in a way that is easier to generate from C). Such co-evolution has precedent (as seen with automatic refactoring tools influencing coding standards). &lt;br /&gt;
&lt;br /&gt;
In conclusion, automatic C to Rust translation has made significant strides: &#039;&#039;&#039;C2Rust&#039;&#039;&#039; provides a reliable base for transpiling C99 code to Rust, &#039;&#039;&#039;Corrode&#039;&#039;&#039; demonstrated the feasibility and influenced later work, and newer tools like &#039;&#039;&#039;Laertes&#039;&#039;&#039;, &#039;&#039;&#039;Crown&#039;&#039;&#039;, and &#039;&#039;&#039;CRustS&#039;&#039;&#039; show that a sizable portion of &amp;lt;code&amp;gt;unsafe&amp;lt;/code&amp;gt; can be eliminated through clever analysis. Meanwhile, &#039;&#039;&#039;Flourine&#039;&#039;&#039;, &#039;&#039;&#039;Vert&#039;&#039;&#039;, and &#039;&#039;&#039;C2SaferRust&#039;&#039;&#039; illustrate the power of AI and fuzzing to make translated code more idiomatic and safe. The field is moving quickly, and collaborations between academia and industry (such as the open-source efforts on C2Rust and the research from multiple universities) are bringing us closer to the day where legacy C code can be automatically “uplifted” into safe Rust. That promises not only to reduce bugs but also to extend the life of critical software by modernizing it for today’s secure development standards. Each tool today contributes a piece of the puzzle – from preserving exact semantics to inferring lifetimes or suggesting Rust-specific idioms – and combined, they are paving the way toward fully automated, trustworthy translation from C to Rust. &amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref11&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;References:&#039;&#039;&#039; The content above cites information from official tool documentation, academic papers, and reputable sources, including the C2Rust project site, the Corrode GitHub README, research papers on Laertes and related techniques, the CRustS demonstration summary, and studies on LLM-based translation effectiveness. These references provide further details for readers interested in the technical specifics of each tool and approach. &amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref11&amp;quot;/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== References ==&lt;br /&gt;
&lt;br /&gt;
&amp;lt;references&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref1&amp;quot;&amp;gt;Translating All C to Rust — darpa.mil — https://www.darpa.mil/program/translating-all-c-to-rust&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref2&amp;quot;&amp;gt;GitHub - jameysharp/corrode: C to Rust translator — github.com — https://github.com/jameysharp/corrode&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref3&amp;quot;&amp;gt;https://www.ndss-symposium.org/wp-content/uploads/2025-1407-paper.pdf — ndss-symposium.org — https://www.ndss-symposium.org/wp-content/uploads/2025-1407-paper.pdf&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref4&amp;quot;&amp;gt;C2Rust Demonstration — c2rust.com — https://c2rust.com/&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref5&amp;quot;&amp;gt;GitHub - immunant/c2rust: Migrate C code to Rust — github.com — https://github.com/immunant/c2rust&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref6&amp;quot;&amp;gt;C to Rust translator - community - The Rust Programming Language Forum — users.rust-lang.org — https://users.rust-lang.org/t/c-to-rust-translator/55381&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref7&amp;quot;&amp;gt;C2Rust is Back :: Immunant, Inc — immunant.com — https://immunant.com/blog/2022/06/back/&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref8&amp;quot;&amp;gt;c2rust vs Corrode — jamey.thesharps.us — https://jamey.thesharps.us/2018/06/30/c2rust-vs-corrode/&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref9&amp;quot;&amp;gt;Aliasing Limits on Translating C to Safe Rust — dl.acm.org — https://dl.acm.org/doi/pdf/10.1145/3586046&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref10&amp;quot;&amp;gt;To Tag, or Not to Tag: Translating C’s Unions to Rust’s Tagged Unions — arxiv.org — https://arxiv.org/html/2408.11418v2&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref11&amp;quot;&amp;gt;CRustS — Rust application // Lib.rs — lib.rs — https://lib.rs/crates/crusts&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref12&amp;quot;&amp;gt;C2SaferRust: Transforming C Projects into Safer Rust with NeuroSymbolic Techniques — arxiv.org — https://arxiv.org/html/2501.14257v1&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref13&amp;quot;&amp;gt;RustMap: Towards Project-Scale C-to-Rust Migration via Program Analysis and LLM — arxiv.org — https://arxiv.org/html/2503.17741v1&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref14&amp;quot;&amp;gt;Online C to Rust Converter - CodeConvert AI — codeconvert.ai — https://www.codeconvert.ai/c-to-rust-converter&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;/references&amp;gt;&lt;/div&gt;</summary>
		<author><name>Timo.stripf</name></author>
	</entry>
	<entry>
		<id>https://www.emmtrix.com/w139/index.php?title=Main_Page&amp;diff=2907</id>
		<title>Main Page</title>
		<link rel="alternate" type="text/html" href="https://www.emmtrix.com/w139/index.php?title=Main_Page&amp;diff=2907"/>
		<updated>2026-02-02T21:23:26Z</updated>

		<summary type="html">&lt;p&gt;Timo.stripf: /* Other Topics */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;__NOTOC__&lt;br /&gt;
&lt;br /&gt;
Welcome to the emmtrix Technologies Wiki. As a company with a deep-rooted passion for compilers, we specialize in source-to-source compilers designed to analyze, optimize and transform your code. This Wiki aims to offer detailed, technical background information that complements the tools and resources available on our official website. Here, you&#039;ll find in-depth explanations, usage guidelines, and insights into the engineering behind our specialized software solutions. Whether you&#039;re a developer or a technically-inclined enthusiast, this space is designed to deepen your understanding of what makes our tools essential for your projects.&lt;br /&gt;
&lt;br /&gt;
{| border=&amp;quot;0&amp;quot; style=&amp;quot;padding: 10px&amp;quot;&lt;br /&gt;
| style=&amp;quot;vertical-align: top; padding: 10px;&amp;quot; |&lt;br /&gt;
=== emmtrix Products ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;!--T:7--&amp;gt;&lt;br /&gt;
* [[emmtrix Dependency Analyzer]]&lt;br /&gt;
* [[emmtrix Performance Estimator]]&lt;br /&gt;
* [[emmtrix Code Vectorizer]]&lt;br /&gt;
* [[emmtrix C++ to C Compiler]]&lt;br /&gt;
** [https://online-ecpp2c.emmtrix.com emmtrix C++ to C Compiler Online]&lt;br /&gt;
* [[emmtrix C to Rust Compiler]]&lt;br /&gt;
* emmtrix Parallel Studio&lt;br /&gt;
* Generic information&lt;br /&gt;
** [[emmtrix Studio Release Notes]]&lt;br /&gt;
** [[:Category:emmtrix Studio FAQ|FAQ]]&lt;br /&gt;
* Discontinued product&lt;br /&gt;
** [[emmtrix Code Generator]]&lt;br /&gt;
** [[emmtrix Model Code Generator]]&lt;br /&gt;
* Other Solutions&lt;br /&gt;
** [[TC4x PPU Coverage Analysis]]&lt;br /&gt;
** [[emmtrix Link Stubber]]&lt;br /&gt;
| style=&amp;quot;vertical-align: top; padding: 10px;&amp;quot; |&lt;br /&gt;
=== Supported Architectures ===&lt;br /&gt;
* [[TriCore Instruction Set Architecture]]&lt;br /&gt;
** [[Infineon AURIX TC2xx]]&lt;br /&gt;
** [[Infineon AURIX TC3xx]]&lt;br /&gt;
** [[Infineon AURIX TC4x]], including [[Infineon AURIX TC4x Parallel Processing Unit (PPU)|Parallel Processing Unit (PPU)]]&lt;br /&gt;
* ... and many more&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
|&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Compiler Background ===&lt;br /&gt;
&lt;br /&gt;
* [[Loop Transformations]]&lt;br /&gt;
* [[:Category:Code Transformation]]&lt;br /&gt;
*[[Demystifying C++]]&lt;br /&gt;
*[[The alias Attribute|The alias attribute]]&lt;br /&gt;
*[[Clang Diagnostics Overview]]&lt;br /&gt;
*[[:Category:Clang Diagnostics]]&lt;br /&gt;
&lt;br /&gt;
=== Other Topics ===&lt;br /&gt;
* [[C to Z3 Cheat Sheet]]&lt;br /&gt;
* [[Eclipse Xcore Cheat Sheet]]&lt;br /&gt;
* [[Logical Execution Time (LET)]]&lt;br /&gt;
* [[ULP Difference of Float Numbers]]&lt;br /&gt;
* [[Numerical Precision in ONNX and AI Inference]]&lt;br /&gt;
* [[Automatic C to Rust Translation]]&lt;/div&gt;</summary>
		<author><name>Timo.stripf</name></author>
	</entry>
	<entry>
		<id>https://www.emmtrix.com/w139/index.php?title=Numerical_Precision_in_ONNX_and_AI_Inference&amp;diff=2906</id>
		<title>Numerical Precision in ONNX and AI Inference</title>
		<link rel="alternate" type="text/html" href="https://www.emmtrix.com/w139/index.php?title=Numerical_Precision_in_ONNX_and_AI_Inference&amp;diff=2906"/>
		<updated>2026-02-01T22:59:38Z</updated>

		<summary type="html">&lt;p&gt;Timo.stripf: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&#039;&#039;&#039;Open Neural Network Exchange (ONNX)&#039;&#039;&#039; is an open standard format for representing machine learning models and neural network computations across different frameworks and hardware&amp;lt;ref name=&amp;quot;ref1&amp;quot;/&amp;gt;. As models are exported and deployed via ONNX, the &#039;&#039;&#039;numerical precision&#039;&#039;&#039; of computations becomes critical. Deep learning inference involves a variety of floating-point operations, and small numerical discrepancies can accumulate and affect model accuracy or reproducibility. This article provides an in-depth overview of floating-point precision issues in numerical computing, with a focus on how they manifest in AI inference and the ONNX ecosystem. We cover floating-point formats and rounding behavior, accumulation strategies for summations, the use of low-precision formats (FP16, BFloat16, FP8) in modern AI, and how ONNX defines (or leaves unspecified) the precision of accumulators in operations. Strategies for maintaining numerical stability in deep neural networks are discussed, along with the trade-offs between computational performance and arithmetic accuracy. The goal is to give engineers and researchers a comprehensive, encyclopedic reference on numerical precision in ONNX-based AI inference systems.&lt;br /&gt;
&lt;br /&gt;
== Floating-Point Precision in Numerical Computing ==&lt;br /&gt;
&lt;br /&gt;
=== 2.1. IEEE 754 Floating-Point Formats ===&lt;br /&gt;
&lt;br /&gt;
Modern computers represent real numbers using the IEEE 754 floating-point standard, which defines formats like 32-bit &#039;&#039;single precision&#039;&#039; (FP32) and 64-bit &#039;&#039;double precision&#039;&#039; (FP64). In IEEE 754 binary formats, a floating-point number is composed of three fields: 1 sign bit, several exponent bits (with a bias), and several fraction (mantissa) bits. For example, FP32 consists of 1 sign bit, 8 exponent bits, and 23 fraction bits (24 bits of significand precision including the implicit leading 1)&amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;. This provides roughly 7 decimal digits of precision and an exponent range allowing values on the order of &#039;&#039;10&amp;lt;sup&amp;gt;±38&amp;lt;/sup&amp;gt;&#039;&#039;. FP64 allocates 1 sign bit, 11 exponent bits, and 52 fraction bits (53-bit precision), yielding about 15–16 decimal digits of precision and a vastly larger range (~&#039;&#039;10&amp;lt;sup&amp;gt;±308&amp;lt;/sup&amp;gt;&#039;&#039;)&amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;. These larger formats (especially FP64) are the &amp;quot;gold standard&amp;quot; for numerical accuracy in scientific computing, as they reduce round-off error in complex calculations&amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;. However, the increased precision comes at the cost of doubled storage and typically reduced computational throughput (e.g. many GPUs execute FP64 instructions at a much lower rate than FP32&amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
In addition to single and double precision, the IEEE 754-2008 standard introduced a 16-bit &#039;&#039;half precision&#039;&#039; format (FP16, or binary16). FP16 has 1 sign bit, 5 exponent bits, and 10 fraction bits (11-bit significand including the implicit 1)&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. This format covers a much smaller dynamic range (approximately &#039;&#039;6.1 × 10&amp;lt;sup&amp;gt;-5&amp;lt;/sup&amp;gt;&#039;&#039; to &#039;&#039;6.5 × 10&amp;lt;sup&amp;gt;4&amp;lt;/sup&amp;gt;&#039;&#039; for normalized values&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;) and offers only around 3 decimal digits of precision. Half precision was originally used in graphics, but it has gained popularity in machine learning for its speed and memory advantages (discussed later in Section 4). Another format relevant to AI is &#039;&#039;&#039;BFloat16&#039;&#039;&#039; (Brain Float 16), a 16-bit float with &#039;&#039;&#039;8 exponent bits and 7 fraction bits&#039;&#039;&#039;&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. BFloat16 sacrifices precision (only ~7 fraction bits ≈ 2–3 decimal digits) in order to have the &#039;&#039;&#039;same exponent range as FP32&#039;&#039;&#039;&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. This means BFloat16 can represent very large or very small numbers (∼&#039;&#039;10&amp;lt;sup&amp;gt;±38&amp;lt;/sup&amp;gt;&#039;&#039;, similar range as FP32) but with much less precision between representable values&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. BFloat16 was introduced for deep learning by Google (for TPUs) to allow reduced precision &#039;&#039;training&#039;&#039; without frequent overflow/underflow, relying on the wide range and accepting the coarse precision.&lt;br /&gt;
&lt;br /&gt;
Each floating-point format has a &#039;&#039;&#039;unit roundoff&#039;&#039;&#039; or machine precision, denoted &#039;&#039;ε&#039;&#039;. This is effectively the spacing between 1.0 and the next representable number. For FP32, &#039;&#039;ε ≈ 2&amp;lt;sup&amp;gt;-23&amp;lt;/sup&amp;gt; ≈ 1.19 × 10&amp;lt;sup&amp;gt;-7&amp;lt;/sup&amp;gt;&#039;&#039;, corresponding to about 7–8 decimal digits of precision&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. In FP64, &#039;&#039;ε = 2&amp;lt;sup&amp;gt;-52&amp;lt;/sup&amp;gt; ≈ 2.22 × 10&amp;lt;sup&amp;gt;-16&amp;lt;/sup&amp;gt;&#039;&#039; (15–16 decimal digits). Half precision is much less precise: FP16 has &#039;&#039;ε = 2&amp;lt;sup&amp;gt;-10&amp;lt;/sup&amp;gt; ≈ 9.76 × 10&amp;lt;sup&amp;gt;-4&amp;lt;/sup&amp;gt;&#039;&#039;&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;, and BFloat16 has &#039;&#039;ε = 2&amp;lt;sup&amp;gt;-7&amp;lt;/sup&amp;gt; = 7.8125 × 10&amp;lt;sup&amp;gt;-3&amp;lt;/sup&amp;gt;&#039;&#039;&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. These values quantify how finely the continuum of real numbers is quantized in each format.&lt;br /&gt;
&lt;br /&gt;
=== 2.2. Machine Epsilon and Rounding Error ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Machine epsilon (&#039;&#039;ε&#039;&#039;)&#039;&#039;&#039; is formally defined as the smallest positive number such that &#039;&#039;1.0 + ε &amp;gt; 1.0&#039;&#039; in the floating-point format&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. It effectively measures the &#039;&#039;&#039;precision&#039;&#039;&#039; of the representation. In FP32, &#039;&#039;ε = 2&amp;lt;sup&amp;gt;-23&amp;lt;/sup&amp;gt; ≈ 1.19 × 10&amp;lt;sup&amp;gt;-7&amp;lt;/sup&amp;gt;&#039;&#039;, meaning any result is rounded to about 7 decimal digits. FP16’s &#039;&#039;ε = 2&amp;lt;sup&amp;gt;-10&amp;lt;/sup&amp;gt; ≈ 9.77 × 10&amp;lt;sup&amp;gt;-4&amp;lt;/sup&amp;gt;&#039;&#039;, over 1000× larger (so only ~3 decimal digits of precision), and BFloat16’s &#039;&#039;ε = 2&amp;lt;sup&amp;gt;-7&amp;lt;/sup&amp;gt; ≈ 7.8 × 10&amp;lt;sup&amp;gt;-3&amp;lt;/sup&amp;gt;&#039;&#039;, reflecting its mere ~8-bit precision&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. Rounding in IEEE 754 by default uses &#039;&#039;&#039;round-to-nearest (ties to even)&#039;&#039;&#039;, which ensures the rounding error for any single operation is at most half a unit in the last place (0.5 ULP). We can model a floating-point addition as: &lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;\text{fl}(a+b) = (a+b)\,(1 + \delta),&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
where &#039;&#039;|δ| ≤ ε&#039;&#039; for that format&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. Here &#039;&#039;ε/2&#039;&#039; is the maximum &#039;&#039;&#039;relative error&#039;&#039;&#039; in one rounding (also called unit roundoff). For example, when adding two FP32 numbers, the result is accurate to within about &#039;&#039;10&amp;lt;sup&amp;gt;-7&amp;lt;/sup&amp;gt;&#039;&#039; of the true sum (relative), whereas in BF16, the result can deviate by up to &#039;&#039;∼7.8 × 10&amp;lt;sup&amp;gt;-3&amp;lt;/sup&amp;gt;&#039;&#039; (nearly 0.78% relative error per operation)&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. Thus, &#039;&#039;&#039;each arithmetic operation incurs a small rounding error&#039;&#039;&#039; (on the order of machine epsilon) that can propagate or accumulate through a sequence of calculations.&lt;br /&gt;
&lt;br /&gt;
Because floating-point representations have finite exponent range, we must also consider &#039;&#039;&#039;overflow&#039;&#039;&#039; and &#039;&#039;&#039;underflow&#039;&#039;&#039;. If a result’s magnitude exceeds the maximum representable value, it overflows to infinity; if it falls below the minimum normal (or minimum subnormal) value, it underflows to zero (or a denormalized tiny value). FP32 and BF16 share an exponent size (8 bits), so their range for normal numbers is roughly &#039;&#039;[10&amp;lt;sup&amp;gt;-38&amp;lt;/sup&amp;gt;, 10&amp;lt;sup&amp;gt;38&amp;lt;/sup&amp;gt;]&#039;&#039; (approx &amp;lt;math&amp;gt;~\!3.4\times10^{38}&amp;lt;/math&amp;gt; max)&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. FP16’s 5-bit exponent allows max &#039;&#039;∼ 6.55 × 10&amp;lt;sup&amp;gt;4&amp;lt;/sup&amp;gt;&#039;&#039; and min normal &#039;&#039;∼ 6.10 × 10&amp;lt;sup&amp;gt;-5&amp;lt;/sup&amp;gt;&#039;&#039;&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. In practice, &#039;&#039;&#039;underflow/overflow&#039;&#039;&#039; can occur in low-precision arithmetic if values are not appropriately scaled. For instance, summing a large number of moderately sized terms in FP16 could overflow where FP32 would not. As an example, summing on the order of &#039;&#039;10&amp;lt;sup&amp;gt;4&amp;lt;/sup&amp;gt;&#039;&#039; values of magnitude ~&#039;&#039;10&amp;lt;sup&amp;gt;1&amp;lt;/sup&amp;gt;&#039;&#039; (say, adding 10,000 terms around 10 each) would produce a total &#039;&#039;∼10&amp;lt;sup&amp;gt;5&amp;lt;/sup&amp;gt;&#039;&#039;, which exceeds FP16’s max finite value ~&#039;&#039;6.5 × 10&amp;lt;sup&amp;gt;4&amp;lt;/sup&amp;gt;&#039;&#039; and would overflow to &#039;&#039;+∞&#039;&#039; in half precision&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. Underflow is usually a concern when subtracting nearly equal numbers (catastrophic cancellation yielding a tiny difference that might flush to zero in low precision).&lt;br /&gt;
&lt;br /&gt;
=== 2.3. Accumulation Error in Reductions ===&lt;br /&gt;
&lt;br /&gt;
Floating-point addition is &#039;&#039;&#039;not associative&#039;&#039;&#039;: &#039;&#039;(a+b)+c&#039;&#039; can differ from &#039;&#039;a+(b+c)&#039;&#039; due to rounding, and thus the order in which we sum a list of numbers can change the result slightly. When summing a large sequence of &#039;&#039;N&#039;&#039; numbers &#039;&#039;x&amp;lt;sub&amp;gt;1&amp;lt;/sub&amp;gt;, x&amp;lt;sub&amp;gt;2&amp;lt;/sub&amp;gt;, ..., x&amp;lt;sub&amp;gt;N&amp;lt;/sub&amp;gt;&#039;&#039;, the &#039;&#039;&#039;accumulated rounding error&#039;&#039;&#039; can be significant. In the &#039;&#039;worst case&#039;&#039; (e.g., adding many positive and negative terms that cause maximal cancellation or always rounding one way), the error can grow on the order of &#039;&#039;O(N ε)&#039;&#039; relative to the exact sum&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;. Simply summing &#039;&#039;N&#039;&#039; numbers in sequence (the &#039;&#039;&#039;naive linear summation&#039;&#039;&#039;) has a worst-case error bound proportional to &amp;lt;math&amp;gt;N\,\varepsilon&amp;lt;/math&amp;gt;&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. This worst-case occurs for adversarial arrangements of values; in more typical scenarios with rounding errors of random signs, one can model the error as a random walk, yielding an expected root-mean-square (RMS) error growth on the order of &amp;lt;math&amp;gt;O(\sqrt{N}\,\varepsilon)&amp;lt;/math&amp;gt;&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;. Still, with very large &#039;&#039;N&#039;&#039;, even &amp;lt;math&amp;gt;\sqrt{N}&amp;lt;/math&amp;gt; times &#039;&#039;ε&#039;&#039; can become large enough to matter.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Cancellation&#039;&#039;&#039; is a particular problem: if we add two nearly equal numbers of opposite sign, most leading digits cancel and the result loses significant bits of precision. For example, in single precision, subtracting two numbers that agree in the first 6 decimal digits will yield a result with only ~1 digit of accuracy left. Catastrophic cancellation can dramatically increase relative error. Summation is especially sensitive to cancellation if large positive and negative terms are summed naively. The &#039;&#039;&#039;condition number&#039;&#039;&#039; of a summation (the ratio of the sum of absolute values &amp;lt;math&amp;gt;\sum|x_i|&amp;lt;/math&amp;gt; to the absolute value of the true sum &amp;lt;math&amp;gt;|\sum x_i|&amp;lt;/math&amp;gt;) indicates how sensitive the result is to perturbations&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;. If the numbers have mixed signs and nearly cancel out (making the condition number large), even an optimal summation order will have a large relative error. For instance, summing [1.0, &#039;&#039;10&amp;lt;sup&amp;gt;100&amp;lt;/sup&amp;gt;&#039;&#039;, 1.0, &#039;&#039;-10&amp;lt;sup&amp;gt;100&amp;lt;/sup&amp;gt;&#039;&#039;] in double precision has an exact sum of 2.0, but naive summation may yield 0.0 due to cancellation; an improved summation algorithm yields the correct 2.0&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
In practical terms, when summing a long list of floating-point numbers (such as accumulating a dot product or reducing a tensor), the &#039;&#039;&#039;accumulation error&#039;&#039;&#039; can become noticeable if no precautions are taken. Techniques to mitigate this error include carefully choosing the summation order or using higher precision for intermediate sums. We will explore these strategies in the next section.&lt;br /&gt;
&lt;br /&gt;
== Accumulators and Reduction Strategies ==&lt;br /&gt;
&lt;br /&gt;
When performing reductions (sums, dot products, etc.), the method of accumulation can greatly affect the accuracy of the result. Below, we discuss various strategies:&lt;br /&gt;
&lt;br /&gt;
=== 3.1. Naive Linear Accumulation ===&lt;br /&gt;
&lt;br /&gt;
The simplest accumulation strategy is to use a single accumulator and add each element sequentially. In C-like pseudocode:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;c&amp;quot;&amp;gt;&lt;br /&gt;
float sum = 0.0f;&lt;br /&gt;
for (int i = 0; i &amp;lt; N; ++i) {&lt;br /&gt;
    sum += array[i];&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This &#039;&#039;&#039;naive linear summation&#039;&#039;&#039; adds values one by one to the running total. While straightforward and fast, it is prone to the rounding error accumulation described earlier. The partial sum &amp;lt;code&amp;gt;sum&amp;lt;/code&amp;gt; continually grows (in absolute value) as terms are added, so new small terms may fall outside its precision. For example, if &amp;lt;code&amp;gt;sum&amp;lt;/code&amp;gt; is large and positive, adding a comparatively tiny positive number may produce no change because the small term&#039;s significant digits vanish in the presence of the large sum (the increment is below the rounding threshold of &amp;lt;code&amp;gt;sum&amp;lt;/code&amp;gt;). Conversely, adding a small number of opposite sign can suffer from cancellation. The order of addition matters: summing from smallest magnitude to largest (ascending order) tends to yield a more accurate result than summing in descending order, because adding small terms first preserves their contributions before the sum becomes very large. The naive loop above implicitly sums in the given index order; if the data is unsorted, this order is essentially arbitrary.&lt;br /&gt;
&lt;br /&gt;
The performance of linear accumulation is excellent (minimal overhead), and for many applications the resulting error is within acceptable limits. However, for &#039;&#039;&#039;very large N&#039;&#039;&#039; or ill-conditioned sums, the inaccuracy can become a problem. In a reduction involving millions of elements, a single-precision sum could lose several digits of accuracy. In deep learning, this could translate to small discrepancies in layer outputs or aggregated gradients if done naively in low precision.&lt;br /&gt;
&lt;br /&gt;
=== 3.2. Pairwise and Tree-Based Reductions ===&lt;br /&gt;
&lt;br /&gt;
A better approach to summation is &#039;&#039;&#039;pairwise summation&#039;&#039;&#039;, which sums numbers in a binary tree structure rather than linearly. The idea is to recursively split the array into halves, sum each half, then add the two partial sums. This can be implemented as:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;text&amp;quot;&amp;gt;&lt;br /&gt;
function pairwise_sum(x[1..n]):&lt;br /&gt;
    if n == 0:&lt;br /&gt;
        return 0&lt;br /&gt;
    if n == 1:&lt;br /&gt;
        return x[1]&lt;br /&gt;
    m = floor(n/2)&lt;br /&gt;
    left_sum  = pairwise_sum(x[1..m])&lt;br /&gt;
    right_sum = pairwise_sum(x[m+1..n])&lt;br /&gt;
    return left_sum + right_sum&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
By always adding numbers of similar magnitudes first (small subsets are reduced to subtotals, then those are added, etc.), pairwise summation avoids the situation of a very large running total absorbing small additions until the final steps. The error of pairwise summation grows logarithmically with &#039;&#039;N&#039;&#039; in the worst case (roughly &#039;&#039;O(ε log&amp;lt;sub&amp;gt;2&amp;lt;/sub&amp;gt; N)&#039;&#039;)&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;, instead of linearly. Intuitively, at each level of the tree, rounding error is constrained, and there are about &#039;&#039;log&amp;lt;sub&amp;gt;2&amp;lt;/sub&amp;gt; N&#039;&#039; levels. In fact, the worst-case relative error bound for pairwise summation (base case of 1 element) is:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;|E_N| \le \frac{\varepsilon \log_2 N}{1 - \varepsilon \log_2 N} \sum_{i=1}^N |x_i|,&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
which for practical &#039;&#039;N&#039;&#039; simplifies to on the order of &#039;&#039;ε log&amp;lt;sub&amp;gt;2&amp;lt;/sub&amp;gt; N&#039;&#039; times the condition number of the sum&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;. In typical random-error scenarios, the error behaves even better (on the order of &amp;lt;math&amp;gt;\sqrt{\log N}&amp;lt;/math&amp;gt;)&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;. The key is that &#039;&#039;&#039;pairwise (tree) reduction balances the accumulations&#039;&#039;&#039;, preventing one sum from overwhelming the others too early.&lt;br /&gt;
&lt;br /&gt;
From a performance standpoint, pairwise summation has the same &#039;&#039;O(N)&#039;&#039; total additions as linear summation, just a different order. It is also highly amenable to parallelization: independent partial sums can be computed in parallel at each level of the tree. Many numerical libraries and BLAS implementations use a form of blocked or pairwise summation in dot products for improved accuracy at negligible performance cost. For example, NumPy and Julia perform partial pairwise summation by default for better numerical behavior&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=== 3.3. Compensated Summation (Kahan, Neumaier) ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Compensated summation&#039;&#039;&#039; is an alternative technique to improve summation accuracy by tracking the small “lost” quantities. The most famous algorithm is the &#039;&#039;&#039;Kahan summation algorithm&#039;&#039;&#039; (also called Kahan–Babuška)&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. Kahan summation uses an extra variable to accumulate the round-off error. In pseudocode:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;c&amp;quot;&amp;gt;&lt;br /&gt;
double sum = 0.0;&lt;br /&gt;
double c = 0.0;    // compensation for lost low-order bits&lt;br /&gt;
for (i = 1; i &amp;lt;= N; ++i) {&lt;br /&gt;
    double y = x[i] - c;    // recover low-order bits by subtracting compensation&lt;br /&gt;
    double t = sum + y;     // perform the addition&lt;br /&gt;
    c = (t - sum) - y;      // compute new compensation (the error in t)&lt;br /&gt;
    sum = t;&lt;br /&gt;
}&lt;br /&gt;
return sum;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Here &amp;lt;code&amp;gt;c&amp;lt;/code&amp;gt; accumulates the tiny leftovers that would otherwise be dropped&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. Each iteration essentially subtracts the last error from the new addend (&amp;lt;code&amp;gt;y = x[i] - c&amp;lt;/code&amp;gt;) so that &amp;lt;code&amp;gt;t = sum + y&amp;lt;/code&amp;gt; includes the small bits from the previous operation. The new error is then stored in &amp;lt;code&amp;gt;c&amp;lt;/code&amp;gt; for the next iteration. The result is that the final &amp;lt;code&amp;gt;sum&amp;lt;/code&amp;gt; is as if computed with higher precision than the machine’s: Kahan’s algorithm can &#039;&#039;&#039;greatly reduce the error&#039;&#039;&#039; in summing a sequence of floats. In fact, with exact arithmetic for the compensation, Kahan summation yields a result accurate to within 1 or 2 ULP of the true sum, regardless of N (assuming the sequence of partial sums doesn’t overflow)&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. More precisely, Kahan summation achieves a &#039;&#039;&#039;worst-case error independent of N&#039;&#039;&#039; (error bound depends only on machine precision, not the number of terms)&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. In practice, using a double accumulator for summing single-precision numbers via Kahan can give near double-precision accuracy.&lt;br /&gt;
&lt;br /&gt;
An important subtlety is that the compensation variable &amp;lt;code&amp;gt;c&amp;lt;/code&amp;gt; should be kept in &#039;&#039;full precision&#039;&#039;. In the implementation above, we used &amp;lt;code&amp;gt;double&amp;lt;/code&amp;gt; for both sum and c. If summing FP32 values, one might use &amp;lt;code&amp;gt;float sum&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;float c&amp;lt;/code&amp;gt; (both 32-bit), which still improves accuracy, but using a higher precision for the accumulator and compensation (i.e. double) yields even better results. Many implementations of Kahan summation use the same type for sum and c, but it&#039;s assumed to be higher precision than the inputs when possible.&lt;br /&gt;
&lt;br /&gt;
Kahan’s algorithm carries a performance cost: each iteration does five arithmetic operations (two adds, one subtract, etc.) and has a data dependency, which can hinder parallelism or pipelining&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. Thus, it runs slower than naive summation (roughly 2–3× slower or more, depending on CPU pipeline). In loops where summation is a bottleneck, this may be significant. Nonetheless, for modest N or where maximum accuracy is required, compensated summation is extremely useful. In fact, Python’s &amp;lt;code&amp;gt;math.fsum&amp;lt;/code&amp;gt; function uses a variant of Kahan’s algorithm (actually a more advanced multi-part compensation) to sum iterables with high precision.&lt;br /&gt;
&lt;br /&gt;
There are variations and improvements on basic Kahan summation. &#039;&#039;&#039;Neumaier’s algorithm&#039;&#039;&#039; (sometimes called &#039;&#039;Kahan-Babuška-Neumaier&#039;&#039;) improves Kahan’s method by also handling cases where the next input is larger in magnitude than the running sum&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. In such cases, the original Kahan formula could suffer error; Neumaier’s version effectively swaps roles when a huge value comes in. Its pseudocode is:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;text&amp;quot;&amp;gt;&lt;br /&gt;
function NeumaierSum(array):&lt;br /&gt;
    sum = 0.0&lt;br /&gt;
    compensation = 0.0&lt;br /&gt;
    for each x in array:&lt;br /&gt;
        t = sum + x&lt;br /&gt;
        if (fabs(sum) &amp;gt;= fabs(x))&lt;br /&gt;
            compensation += (sum - t) + x;&lt;br /&gt;
        else&lt;br /&gt;
            compensation += (x - t) + sum;&lt;br /&gt;
        sum = t;&lt;br /&gt;
    return sum + compensation;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This algorithm ensures the compensation covers both scenarios: when the sum is the bigger addend or when the new term is bigger&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. In the end, it adds the final &amp;lt;code&amp;gt;compensation&amp;lt;/code&amp;gt; once to the result. Neumaier’s method can produce the correct result in cases where basic Kahan fails. For instance, summing [1.0, &#039;&#039;+10&amp;lt;sup&amp;gt;100&amp;lt;/sup&amp;gt;&#039;&#039;, 1.0, &#039;&#039;-10&amp;lt;sup&amp;gt;100&amp;lt;/sup&amp;gt;&#039;&#039;] in double precision: regular Kahan summation might give 0.0 (losing the small terms), whereas Neumaier’s algorithm yields 2.0 exactly&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. There are also higher-order compensation methods (carrying multiple compensation terms, e.g., the &#039;&#039;&#039;Klein summation&#039;&#039;&#039; variant&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt; or using arbitrary precision for intermediate sums) that further reduce error at increased computational cost&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
In summary, compensated summation algorithms trade extra computation for greatly improved numerical accuracy. They are especially valuable when summing large arrays of numbers with varying magnitudes, where naive summation could lose most of the low-order bits. Many scientific computing environments provide compensated sum routines for this reason. In deep learning, however, a different approach to improved precision is often taken: &#039;&#039;&#039;using a wider accumulator type&#039;&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
=== 3.4. Mixed-Precision Accumulation ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Mixed-precision accumulation&#039;&#039;&#039; refers to performing arithmetic in a higher precision than the input data. A common scenario is accumulating sums in 32-bit floats when the operands are 16-bit floats. Rather than implementing a compensation algorithm, one can simply use a wider accumulator so that intermediate rounding error is smaller. This is effectively what modern GPUs do for tensor operations: for example, NVIDIA &#039;&#039;&#039;Tensor Cores operate on FP16 input data with an FP32 accumulator&#039;&#039;&#039; (the 16-bit products are accumulated in 32-bit)&amp;lt;ref name=&amp;quot;ref6&amp;quot;/&amp;gt;. In this way, summing 2048 products in a dot product has far less precision loss than it would with a 16-bit accumulator. Hardware that supports mixed precision can give the best of both worlds: fast low-precision multiplies and high-precision accumulation.&lt;br /&gt;
&lt;br /&gt;
Software can also leverage mixed precision. For instance, one can sum an array of &amp;lt;code&amp;gt;float&amp;lt;/code&amp;gt; values into a &amp;lt;code&amp;gt;double&amp;lt;/code&amp;gt; variable, accumulating the result in double precision and then converting back to float at the end:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;c&amp;quot;&amp;gt;&lt;br /&gt;
double sum = 0.0;&lt;br /&gt;
for (int i=0; i&amp;lt;N; ++i) {&lt;br /&gt;
    sum += (double) arr_float[i];&lt;br /&gt;
}&lt;br /&gt;
float result = (float) sum;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This approach often yields a more accurate &amp;lt;code&amp;gt;result&amp;lt;/code&amp;gt; than summing in single precision directly, because the intermediate sum carries 52-bit precision. The final rounding to float happens only once at the end (minimizing overall rounding error). In fact, the above is one way to implement &amp;lt;code&amp;gt;math.fsum&amp;lt;/code&amp;gt; in Python (it internally does something similar with multiple partial sums in double precision).&lt;br /&gt;
&lt;br /&gt;
In machine learning training and inference, &#039;&#039;&#039;mixed precision&#039;&#039;&#039; is widely used. When using FP16 for speed, it&#039;s common to accumulate with FP32. For example, &#039;&#039;&#039;Google&#039;s TPUs use BFloat16 for multiplication but accumulate in FP32&#039;&#039;&#039; for matrix ops&amp;lt;ref name=&amp;quot;ref6&amp;quot;/&amp;gt;. AWS’s Inferentia (Neuron) hardware similarly supports FP16/BF16 matrix multiplication with FP32 accumulation&amp;lt;ref name=&amp;quot;ref7&amp;quot;/&amp;gt;. The motivation is that summing hundreds or thousands of half-precision values would otherwise introduce intolerable error or risk overflow. With an FP32 accumulator, the reduction error is drastically reduced (by about &#039;&#039;2&amp;lt;sup&amp;gt;13&amp;lt;/sup&amp;gt;&#039;&#039; times, since FP32 has 13 more precision bits than FP16). Mixed-precision accumulation thus provides a simple and effective form of compensation: &#039;&#039;&#039;use a larger bucket to collect many small contributions&#039;&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
There is a spectrum of options: summing int8 or int16 integers might use 32-bit integers as accumulators for the same reason (to avoid overflow). Some BLAS libraries even offer &#039;&#039;extended precision accumulators&#039;&#039; for single precision dot products (accumulating in double). The downside of mixed precision is the extra memory and maybe throughput cost of the wider type. But on many platforms, the add latency is dominated by hardware design rather than bit-width, so using a 32-bit adder vs 16-bit has negligible speed impact if supported natively. The key is whether data movement or register pressure increases.&lt;br /&gt;
&lt;br /&gt;
In summary, &#039;&#039;&#039;using a higher precision accumulator is often the easiest way to improve numerical accuracy&#039;&#039;&#039; for reductions. It is a primary strategy in deep learning hardware to maintain stability while exploiting low precision for storage and initial computations.&lt;br /&gt;
&lt;br /&gt;
== Precision Formats in AI and Machine Learning ==&lt;br /&gt;
&lt;br /&gt;
Modern AI systems use a variety of numeric formats to balance speed, memory, and accuracy. We discuss common precisions and their roles:&lt;br /&gt;
&lt;br /&gt;
=== 4.1. FP32 and FP64 in Training and Inference ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;FP32 (single precision)&#039;&#039;&#039; has been the workhorse of deep learning for years. Most training scripts in frameworks like TensorFlow and PyTorch used FP32 by default, as it offers sufficient precision to propagate gradients and accumulate weight updates over millions of iterations without excessive error. With ~7 decimal digits of precision and a wide exponent range, FP32 can handle the dynamic range of activations and gradients in typical neural networks. In inference, FP32 is often more precision than strictly necessary for final predictions, but it has remained common because of its ease and the fact that many GPU operations were optimized for FP32.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;FP64 (double precision)&#039;&#039;&#039; is rarely used in deep learning model training or inference, except in specific scenarios requiring extreme precision. FP64 is common in scientific computing and certain types of numerical simulations. In neural networks, however, the extra precision is usually not needed for model accuracy—using doubles tends to yield negligible improvements in model metrics but at a large performance cost (since on GPUs FP64 throughput can be 1/2, 1/8, or even 1/32 of FP32 throughput depending on hardware). One exception might be when integrating neural nets into larger physics simulations or when solving ill-conditioned problems as part of the model. Some researchers use FP64 to diagnose numerical issues or ensure reproducibility. Generally though, deep learning has favored lower precision for efficiency, and FP64 is considered overkill for most networks&amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
That said, certain classical machine learning algorithms benefit from double precision. For example, computing the inverse of a matrix (as in a Gaussian Process regression or Kalman filter embedded in a model) can incur large rounding errors in single precision, so using FP64 can significantly improve the result&amp;lt;ref name=&amp;quot;ref1&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref1&amp;quot;/&amp;gt;. The ONNX standard supports a float64 tensor type for such needs. In ONNX Runtime, however, emphasis has been on FP32 performance, and some pipelines default to float even if the original model was double&amp;lt;ref name=&amp;quot;ref1&amp;quot;/&amp;gt;. If a model’s prediction computation involves operations that are numerically sensitive (like matrix inversion, Cholesky decomposition, or summing extremely large counts), then FP64 may be required to avoid noticeable discrepancies&amp;lt;ref name=&amp;quot;ref1&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref1&amp;quot;/&amp;gt;. A good practice is to test a model in both float and double if you suspect precision issues; significant differences indicate an ill-conditioned calculation that might need higher precision.&lt;br /&gt;
&lt;br /&gt;
In summary, FP32 remains the &#039;&#039;&#039;default&#039;&#039;&#039; format for most training and inference due to its balance of accuracy and efficiency&amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;, while FP64 is reserved for niche cases or precision-critical subroutines. FP32 typically achieves the same accuracy as FP64 for end-to-end deep learning tasks, because network training can often compensate for small noise, and other sources of error (like generalization error) dominate. Therefore, the community trend has been moving to &#039;&#039;&#039;lower&#039;&#039;&#039; precisions, not higher, to gain speed — provided the models still converge and perform well.&lt;br /&gt;
&lt;br /&gt;
=== 4.2. FP16 and BFloat16 in Deep Learning ===&lt;br /&gt;
&lt;br /&gt;
To further speed up neural network training and inference, &#039;&#039;&#039;half-precision (FP16)&#039;&#039;&#039; was adopted, especially with the advent of GPUs supporting FP16 math at twice the rate of FP32. Using FP16 reduces memory storage and bandwidth by 50% compared to FP32, which is significant for large models. However, FP16’s small exponent range and precision posed challenges: training a large network purely in FP16 can fail due to gradient underflow/overflow or weight updates not registering (because changes are below the 0.001 quantization step). To address this, frameworks introduced &#039;&#039;&#039;mixed-precision training&#039;&#039;&#039;, where compute-intensive parts (matrix multiplies, convolutions) run in FP16, but certain accumulations and model parameters remain in FP32. For instance, NVIDIA’s &#039;&#039;&#039;Tensor Core&#039;&#039;&#039; units require FP16 (or BF16) inputs but accumulate results in FP32, effectively ensuring that reductions are done in higher precision&amp;lt;ref name=&amp;quot;ref6&amp;quot;/&amp;gt;. Research showed that with proper loss scaling and mixed precision, models can train to FP32-level accuracy using mostly FP16 arithmetic, achieving significant speedups.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;BFloat16 (BF16)&#039;&#039;&#039; emerged as an alternative 16-bit format primarily from Google’s TPU training systems. BF16 has a larger exponent (8 bits, same as FP32) but only 7 fraction bits&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. This means BF16 can handle the &#039;&#039;&#039;same dynamic range&#039;&#039;&#039; as FP32 (no overflows where FP32 wouldn’t) but at the cost of even lower precision than FP16 (BF16 has about 2–3 decimal digits of precision vs FP16’s ~3–4). Despite its coarse precision, BF16 turns out to be effective for training deep networks because the algorithms (stochastic gradient descent, etc.) are somewhat robust to that noise, and having the wide range prevents catastrophic loss of signal (no gradient will overflow to Inf unless it was going to in FP32 anyway). Many training frameworks (PyTorch, TensorFlow) support BF16 training on hardware like &#039;&#039;&#039;TPU, NVIDIA A100+, Intel CPUs with AVX-512 BF16, etc&#039;&#039;&#039;. Typically, when using BF16, &#039;&#039;&#039;accumulations are still done in FP32&#039;&#039;&#039; (for example, a TPU v2/v3 multiplies BF16 matrices and accumulates into an FP32 result for the dot product&amp;lt;ref name=&amp;quot;ref6&amp;quot;/&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
In inference, both FP16 and BF16 are used to deploy models for faster speed or lower memory. FP16 is popular on NVIDIA GPUs (which have specialized FP16 units). BF16 is advantageous on TPUs and some CPUs that support it, and increasingly on GPUs (Ampere and newer GPUs also support BF16 arithmetic). The choice between FP16 and BF16 can depend on hardware: BF16’s advantage is easier software conversion (you can often take an FP32-trained model and cast weights to BF16 with minimal loss, since the dynamic range is intact), whereas FP16 might require adjusting the model or ensuring values fit in range (sometimes needing techniques like maintaining a master copy of weights in FP32 during training, or handling out-of-range values).&lt;br /&gt;
&lt;br /&gt;
For ONNX, both Float16 and BFloat16 types are part of the standard. ONNX models can be converted to FP16 (truncating weights and inserting cast nodes for computations) using tools like &amp;lt;code&amp;gt;float16_converter&amp;lt;/code&amp;gt; in ONNX Runtime&amp;lt;ref name=&amp;quot;ref8&amp;quot;/&amp;gt;. ONNX Runtime on GPUs will typically execute those FP16 ops with mixed precision on tensor cores (FP16 input, FP32 accumulate), whereas on CPU it may emulate FP16 or upcast to FP32 (as many CPU implementations do not natively support half-precision math)&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;. In fact, as an example, running an ONNX model in FP16 on a CPU might insert hidden Cast operations to FP32 behind the scenes, because the CPU kernel for that op only exists in FP32&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;. This ensures correctness (the computation is done in higher precision) but can reduce performance. We will revisit such backend-dependent behavior in Section 5.3.&lt;br /&gt;
&lt;br /&gt;
In summary, &#039;&#039;&#039;FP16 and BF16&#039;&#039;&#039; are now established as important low-precision formats in deep learning. FP16 offers more precision bits, which can matter for e.g. small differences, but has a limited range (care needed to avoid overflow). BF16 offers safety in range but less precision (which can slightly affect convergence or final accuracy in some cases). Both typically rely on FP32 for accumulation to achieve acceptable accuracy. When deploying models, one should choose a format supported efficiently by the target hardware and verify that the accuracy is not significantly degraded. Often, CNNs and transformers can run in FP16/BF16 with virtually no drop in score, but certain operations (like softmax, see Section 6.3) might need special handling.&lt;br /&gt;
&lt;br /&gt;
=== 4.3. FP8 Formats (E4M3, E5M2) and Scaling ===&lt;br /&gt;
&lt;br /&gt;
The push for ever lower precision has led to experimental &#039;&#039;&#039;8-bit floating point (FP8)&#039;&#039;&#039; formats for deep learning. In 2022, researchers from NVIDIA, Intel, and Arm proposed an FP8 standard with two variants: &#039;&#039;&#039;E4M3&#039;&#039;&#039; (4 exponent bits, 3 fraction bits) and &#039;&#039;&#039;E5M2&#039;&#039;&#039; (5 exponent bits, 2 fraction bits)&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;. Both have 1 sign bit, for a total of 8 bits. E5M2 has a wider exponent range (5 bits gives bias 15) but one fewer mantissa bit than E4M3. The idea is to use E4M3 for numbers that need more precision (like weights or activations around 1.0) and E5M2 for numbers that need more dynamic range (like gradient updates which can be very small or large). In fact, the study found that using E4M3 for forward activations/weights and E5M2 for backward gradients can allow training of large networks with FP8 to accuracy on par with FP16 training&amp;lt;ref name=&amp;quot;ref11&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
FP8 formats are &#039;&#039;not&#039;&#039; IEEE 754 standard (though IEEE is considering lower precision in its 2019 revision). Notably, the E4M3 format as used by NVIDIA does not allocate any bit-pattern for &#039;&#039;+∞&#039;&#039; or &#039;&#039;-∞&#039;&#039; – it has only NaN for invalid results, using that freed bit pattern to extend the representable range by one exponent value&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;. Essentially, E4M3 can represent numbers up to ~&#039;&#039;2&amp;lt;sup&amp;gt;15&amp;lt;/sup&amp;gt;&#039;&#039; (around 3e4) instead of capping at infinity at &#039;&#039;2&amp;lt;sup&amp;gt;16&amp;lt;/sup&amp;gt;&#039;&#039;, by foregoing infinities&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;. Variations also exist regarding negative zero and how NaNs are handled&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;. For instance, GraphCore’s IPU uses FP8 formats that disallow negative zero and infinities, to maximize useable range&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Using 8-bit floats for neural networks requires additional techniques, chiefly &#039;&#039;&#039;scaling&#039;&#039;&#039;. Because 8-bit has very limited precision and range, one typically applies a power-of-two scale factor to groups of values to normalize them into an FP8-friendly range. This is akin to block floating-point or “microscaling” per layer or per tensor. For example, one might keep a FP32 scale for each channel or tensor, and represent values as &amp;lt;math&amp;gt;(FP8\_value) \times 2^{scale}&amp;lt;/math&amp;gt;. The ONNX standard has introduced support for FP8 in version 1.15, including types for E4M3 and E5M2 (with variations FNUZ: finite-only and no negative zero)&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;, and even a type called &#039;&#039;&#039;E8M0&#039;&#039;&#039; which is an 8-bit exponent-only number used purely as a scale factor&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;. The E8M0 type has 8 exponent bits, no mantissa, effectively representing powers of two from &#039;&#039;2&amp;lt;sup&amp;gt;-127&amp;lt;/sup&amp;gt;&#039;&#039; to &#039;&#039;2&amp;lt;sup&amp;gt;128&amp;lt;/sup&amp;gt;&#039;&#039; for scaling purposes&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;. This indicates an approach where an FP8 tensor would be accompanied by an E8M0 scale (or scales) to interpret its value correctly.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Rounding&#039;&#039;&#039; becomes critical when casting values to FP8. The quantization error is much larger relative to FP16 or FP32. Studies have shown that choosing the right rounding mode (e.g., round-to-nearest, stochastic rounding, etc.) and clamping behavior (saturating to max instead of inf) can affect model accuracy&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;. ONNX added a &amp;lt;code&amp;gt;round_mode&amp;lt;/code&amp;gt; attribute to its Cast operator to allow controlling this for FP8&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;. In particular, one paper found that &#039;&#039;&#039;rounding up with saturation&#039;&#039;&#039; (round-to-nearest ties away from 0, and clamp overflows to max finite rather than inf) gave better accuracy in large language model training than other rounding modes&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
At present, FP8 is on the cutting edge. NVIDIA’s H100 GPU supports FP8 tensor cores (for both E4M3 and E5M2), and early results show minimal accuracy loss on some vision and NLP tasks when properly tuned. However, using FP8 requires fine-tuning hyperparameters like scaling factors for each layer and perhaps tweaking training procedures. ONNX’s inclusion of FP8 types means you can represent and transport models using FP8 weights or activations, but actual hardware support is limited and typically paired with calibration or dynamic scaling logic outside of ONNX graph (or using custom ONNX functions for scaling). It remains an &#039;&#039;&#039;open research area&#039;&#039;&#039; how to fully automate FP8 quantization for arbitrary models without accuracy loss.&lt;br /&gt;
&lt;br /&gt;
In summary, FP8 formats promise further compression of neural network computation and data. They are an order of magnitude more error-prone than FP16, but when combined with careful scaling, mixed usage of E4M3/E5M2, and maybe modified training techniques, they have shown surprising success. ONNX is actively evolving to support these ultra-low precision types, signaling their potential importance in future AI inference deployments.&lt;br /&gt;
&lt;br /&gt;
== Accumulation Semantics in ONNX ==&lt;br /&gt;
&lt;br /&gt;
One of the roles of the ONNX specification is to define the mathematical behavior of operators (Conv, MatMul, Sum, etc.) in a framework-agnostic way. However, ONNX mostly defines &#039;&#039;what&#039;&#039; an operator computes in exact arithmetic, not &#039;&#039;how&#039;&#039; the computation is carried out in finite precision. This leaves certain details – like accumulator precision or summation order – up to the implementation (runtime or hardware). We&#039;ll examine how ONNX treats accumulation and what guarantees (or lack thereof) are provided.&lt;br /&gt;
&lt;br /&gt;
=== 5.1. Operator Definitions and Numerical Guarantees ===&lt;br /&gt;
&lt;br /&gt;
Each ONNX operator is described as performing an ideal mathematical function on its inputs. For example, &amp;lt;code&amp;gt;MatMul&amp;lt;/code&amp;gt; is defined as the standard matrix product of two tensors, and &amp;lt;code&amp;gt;ReduceSum&amp;lt;/code&amp;gt; computes a summation along given axes. The ONNX spec, as of current versions, &#039;&#039;&#039;does not mandate the precision of intermediate calculations&#039;&#039;&#039; beyond the types of the inputs and outputs. If an operator’s inputs and outputs are float16, the spec implies the computation is done in float16 in a sense, but it does not forbid an implementation from using higher precision internally – it simply doesn&#039;t mention it. There is &#039;&#039;&#039;no explicit guarantee of identical bit-for-bit results&#039;&#039;&#039; across different ONNX backends for floating-point ops. Instead, results are expected to be numerically close, within tolerances one would normally expect from floating-point differences (due to different hardware or algorithms). The ONNX Backend Test suite typically uses a relative or absolute error tolerance when validating outputs for this reason.&lt;br /&gt;
&lt;br /&gt;
Because of this, important details like the use of extended precision or accumulation in higher precision are left ambiguous. This has led to discussions in the ONNX community about making such behavior more explicit. For example, a proposal was raised to allow specification of accumulation precision for certain ops like MatMul&amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;. The reasoning is that as lower-precision types (float16, bfloat16, float8) become more common, knowing or controlling whether an op internally accumulates in a higher precision is important for both accuracy and reproducibility&amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;. In June 2025, an issue on the ONNX GitHub suggested adding an attribute to MatMul (and similarly Gemm, Softmax, etc.) to specify the accumulator type&amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;. By default it would use input precision (status quo), but an attribute could request (for instance) 32-bit accumulation even if inputs are 16-bit&amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;. As of this writing, however, no such attribute exists in the official spec – it&#039;s an open issue. Thus, the numerical semantics are effectively: &#039;&#039;“compute the result as if in the same type as inputs, but actual implementations may use more precision internally as an optimization.”&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
From a guarantee standpoint, ONNX operators promise the mathematically correct result assuming real-number arithmetic, but they &#039;&#039;&#039;do not guarantee bit-exact results&#039;&#039;&#039; due to floating-point effects. There is an understanding that differences can arise from different summation orders, fused operations (e.g., an implementation might use an FMA (fused multiply-add) which has one rounding instead of two), or use of extended precision registers. ONNX does not formally define rounding modes either – it relies on IEEE 754 defaults, but if a backend used something like deterministic summation or alternative rounding, ONNX has no mechanism to express that.&lt;br /&gt;
&lt;br /&gt;
In summary, ONNX&#039;s current stance is largely &#039;&#039;&#039;implementation-dependent&#039;&#039;&#039; for numerical precision. It provides minimal explicit numerical guarantees beyond type constraints. This is why an ONNX model run on two different hardware or runtime libraries can have slightly different results, especially in reduced precision. The burden is on backends to produce results &amp;quot;close enough&amp;quot; to each other that they don&#039;t affect the model&#039;s decisions in any significant way.&lt;br /&gt;
&lt;br /&gt;
=== 5.2. MatMul, Conv, and Reduction Operators ===&lt;br /&gt;
&lt;br /&gt;
Consider ONNX &amp;lt;code&amp;gt;MatMul&amp;lt;/code&amp;gt;: it takes two tensors (often two matrices) and produces their matrix product. In pure math, &amp;lt;math&amp;gt;C_{ij} = \sum_k A_{ik} &#039;&#039; B_{kj}&amp;lt;/math&amp;gt;. If A and B are float16, should that sum be computed in float16 or float32? As discussed, ONNX doesn’t specify – it simply says the output is a float16 matrix containing the product. In practice, many backends will &#039;&#039;promote&#039;&#039; the computation: for example, NVIDIA GPUs will do the multiplication and addition in a mixed FP16/FP32 mode by default (because of tensor cores), then convert the final result back to float16. ONNX Runtime on CPU might not support float16 multiplication natively, so it will insert cast ops to float32, perform the MatMul in float32, then cast back to float16&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;. From ONNX’s perspective, as long as the final result is within acceptable error of the “ideal” float16 computation, it’s fine. But note, an “ideal” float16 computation (where every multiply and add is rounded to FP16) might actually be &#039;&#039;less accurate* than what the backend did with FP32. This means that a backend using higher precision can produce a result that is not bit-equal to a pure FP16 implementation – it&#039;s actually closer to the true real number result. This is usually considered a good thing (improved accuracy), but it does highlight a lack of consistency: another backend might do everything in pure FP16 and get a slightly different answer.&lt;br /&gt;
&lt;br /&gt;
The same situation occurs for &#039;&#039;&#039;convolution&#039;&#039;&#039; (&amp;lt;code&amp;gt;Conv&amp;lt;/code&amp;gt; op) which is essentially a batched sum of products (like MatMul in sliding window), and for explicit reduction ops like &amp;lt;code&amp;gt;ReduceSum&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;ReduceMean&amp;lt;/code&amp;gt;, etc. If you have an ONNX &amp;lt;code&amp;gt;ReduceSum&amp;lt;/code&amp;gt; that sums 1000 float16 values, the spec doesn’t say how to sum them. A GPU kernel might sum in a tree using FP32 accumulators; a naive implementation might sum in order with FP16 and possibly overflow or underflow intermediate steps. Both are allowed as long as they output a float16 result that is “reasonable.” In practice, ONNX Runtime and others take care to avoid obviously bad behavior (like overflow) by using higher precision or by splitting the sum. But again, it’s not mandated by the ONNX standard explicitly.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;\mathrm{Softmax}(x)_i = \frac{\exp(x_i)}{\sum_j \exp(x_j)},&amp;lt;/math&amp;gt;&lt;br /&gt;
typically applied along a specified axis. Directly computing this in float16 can be problematic for large vectors, as discussed in Section 6.3. Indeed, the issue of accumulation precision arises: should the sum &amp;lt;math&amp;gt;\sum_j \exp(x_j)&amp;lt;/math&amp;gt; be done in FP16 or can it be in FP32? Many implementations will do it in FP32 even if &#039;&#039;x&#039;&#039; is FP16, because the dynamic range of &amp;lt;math&amp;gt;\exp(x)&amp;lt;/math&amp;gt; is large. The ONNX Softmax spec doesn’t detail this, but it expects the result to be a proper probability distribution (summing to 1.0 within normal floating error). A pure FP16 softmax on, say, 1000 elements could sum to something like 0.996 or 1.004 due to rounding, whereas FP32 accumulation would be much closer to 1.000. ONNX likely allows both; the small difference is usually inconsequential, but if it were large (say the FP16 sum overflowed to inf), that would be a correctness issue. A robust backend will prevent that (by using the common max-subtraction trick and higher precision accumulation). The ONNX issue 7072 indeed calls out Softmax as another op where explicit control of accumulation precision would be useful&amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
In summary, &#039;&#039;&#039;MatMul, Conv, and reduction ops&#039;&#039;&#039; in ONNX are defined mathematically but with &#039;&#039;implicit&#039;&#039; numerical behavior. Implementations commonly use higher precision for intermediate sums especially when input is low precision, but this is not standardized. Consequently, results can differ slightly between implementations. For critical applications, one must be aware of these differences. A model exported to ONNX with float16 weights might perform slightly differently on one runtime vs another if one does all float16 math and the other uses float32 accumulations. Both conform to ONNX. As a user, if you require a certain precision for accumulation, currently you have to ensure it by the way you export or run the model (for example, insert manual cast nodes in the ONNX graph to force certain ops to FP32). We may see future ONNX versions address this with attributes or metadata as mentioned, but for now these ops are effectively “compute with best effort accuracy”.&lt;br /&gt;
&lt;br /&gt;
=== 5.3. Backend and Hardware-Dependent Behavior ===&lt;br /&gt;
&lt;br /&gt;
Because ONNX is implemented by many backends (ONNX Runtime, TensorRT, CoreML, PyTorch JIT, etc.), and these in turn run on varied hardware (CPU, GPU, accelerators), the numeric behavior can be backend-dependent. Some examples:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;CPU backends:&#039;&#039;&#039; Many CPUs do not have native float16 vector arithmetic (with a few exceptions in recent CPUs). ONNX Runtime’s default CPU execution provider will often upcast float16 to float32, compute, then downcast&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;. This ensures accuracy but means that if you thought you were getting a full float16 pipeline, you’re not – the CPU is essentially ignoring the low precision in compute. On the other hand, x86 CPUs have extended 80-bit precision in x87 registers (though less used now) and 64-bit SIMD registers for doubles; some compiler optimizations might carry intermediate results in higher precision. This could make CPU results slightly different from a GPU’s if, say, a sum is done in 80-bit on CPU versus 32-bit on GPU. Generally, modern compilers avoid such extended precision unless explicitly asked, to improve reproducibility.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;GPU backends:&#039;&#039;&#039; GPUs often perform summations in parallel using tree reductions. The order of operations can change depending on how threads are scheduled. This means even on the same GPU, summing an array in one launch versus another might not give bit-identical results if the internal parallel reduction algorithm differs (though it should be deterministically the same given the same library version and input, but if you change GPU model or BLAS library, it might differ). Also, GPUs have fused multiply-add (FMA) instructions that do &#039;&#039;(a*b + c)&#039;&#039; in one step with one rounding, which can produce slightly different results than doing the multiply and add separately (which would round twice). If an ONNX backend uses a fused kernel (like a GEMM library) it might yield different low-order bits than a simple loop.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Vendor libraries:&#039;&#039;&#039; ONNX Runtime can use vendor BLAS or DNN libraries (MKL, cuDNN, TensorRT, etc.). These libraries often implement their own strategies for numerical stability. For example, NVIDIA’s cuDNN might use FP32 accumulation for FP16 inputs by default. TensorRT (NVIDIA’s inference engine) will sometimes automatically promote precisions or insert scaling to maintain stability&amp;lt;ref name=&amp;quot;ref13&amp;quot;/&amp;gt;. On the other hand, if you explicitly request FP16 in TensorRT with no FP32 fallback, it might do the entire op in FP16 and you’d see more error. Some libraries offer knobs: TensorRT allows choosing FP32, FP16, or INT8 precision for each layer, and will use FP32 internally for safety in some cases unless forced otherwise.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Hardware-specific quirks:&#039;&#039;&#039; As another example, NVIDIA’s &#039;&#039;&#039;TensorFloat-32 (TF32)&#039;&#039;&#039; on Ampere GPUs is a precision mode where matrix multiplications use 10-bit mantissa (so about FP16 precision for the multiply) but keep 8-bit exponent (range like FP32). By default, CUDA libraries use TF32 for FP32 matrix multiplies on Ampere to speed them up, unless you disable it. This means if you run an ONNX model with FP32 on an A100 GPU, your matmuls might actually be lower precision than FP32 (though higher than FP16)&amp;lt;ref name=&amp;quot;ref6&amp;quot;/&amp;gt;. It’s an example of hardware choosing an intermediate precision for performance. From ONNX’s perspective, the output is still FP32 and (hopefully) within error tolerances, but a user might be surprised that it’s not a “true” FP32 computation internally. If exactness is needed, one has to turn off TF32.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Non-determinism:&#039;&#039;&#039; Some backends (especially on GPUs) allow non-deterministic execution for performance. For instance, atomic adds in parallel reductions can produce results that vary run-to-run because the order of accumulation is race-condition-dependent. ONNX’s spec doesn’t cover this explicitly, but it’s something to consider. In ONNX Runtime, there are settings for deterministic computing, but by default it may use all available performance optimizations, even if results vary at the 1e-7 level.&lt;br /&gt;
&lt;br /&gt;
In short, the numerical behavior under ONNX can depend on the combination of backend software and hardware capabilities. The lack of a strict spec for accumulation means implementations have freedom to use clever strategies (higher precision, reordering) to improve accuracy or speed. Usually this is positive (e.g. getting better accuracy than a naive approach). But it could also lead to slight inconsistencies. For example, you might find that exporting a PyTorch model to ONNX and running it with one engine yields outputs slightly different from PyTorch’s – not because ONNX is wrong, but because the summation or convolution was done differently (maybe PyTorch summed in FP32 as well, but maybe not in exactly the same way).&lt;br /&gt;
&lt;br /&gt;
The key takeaway is that ONNX &#039;&#039;by design&#039;&#039; abstracts away these details, which is good for portability but means you need to &#039;&#039;&#039;trust the backend&#039;&#039;&#039; or verify its precision behavior for sensitive models. If necessary, you can enforce certain behavior by inserting ops (for example, if you want to ensure FP32 accumulation, you could insert a cast to FP32 for inputs of a MatMul and cast the result back to FP16 – this way the ONNX graph itself mandates FP32 compute in between). This of course sacrifices some performance or device-specific magic.&lt;br /&gt;
&lt;br /&gt;
Looking forward, as mentioned, there are proposals to enrich ONNX’s specification regarding precision. This might include explicit flags for “exact summation” or “accumulate in int32” etc., which would make models more self-descriptive but also potentially less portable (if a backend doesn’t support that, what then?). For now, users should be aware of these backend differences. Most of the time, well-engineered ONNX runtimes yield results close enough that they don&#039;t affect the application (e.g., classification top-1 remains the same even if low-level sums differ by 1e-5). But for high-stakes numerical computations, testing and maybe adjusting the model or backend settings is prudent.&lt;br /&gt;
&lt;br /&gt;
== Numerical Stability in Deep Learning Models ==&lt;br /&gt;
&lt;br /&gt;
Deep learning computations involve many operations that, while theoretically well-defined, can be sensitive to numerical issues when implemented in finite precision. We&#039;ll discuss a few common scenarios in neural networks where numerical stability is a concern, and how precision and accumulation play a role.&lt;br /&gt;
&lt;br /&gt;
=== 6.1. Dot Products and Large-Scale Reductions ===&lt;br /&gt;
&lt;br /&gt;
Neural networks are full of dot products: every fully-connected layer computes &amp;lt;math&amp;gt;\mathbf{y} = W\mathbf{x}&amp;lt;/math&amp;gt;, which is a series of inner products between weight vectors and the input. Convolutional layers compute weighted sums of local regions. Attention mechanisms compute large matrix products of query/key/value matrices. All these boil down to summing a lot of terms: &amp;lt;math&amp;gt;\sum_{k=1}^K a_k b_k&amp;lt;/math&amp;gt;. When &#039;&#039;K&#039;&#039; is large (it could be in the thousands or more for a single neuron in a fully connected layer of a large model), the potential for floating-point error in that sum increases.&lt;br /&gt;
&lt;br /&gt;
If using FP32, the error may still be negligible in many cases, but in lower precision like FP16, it can become significant. For example, consider a dot product of length 2048 in FP16. Each product is in FP16 (with ~3 decimal digits precision) and if accumulated in FP16, the worst-case error could be on the order of &#039;&#039;2048  × ε_FP16 ≈ 2048  × 0.0009766 ≈ 2&#039;&#039; (i.e., you could lose a couple units in the third decimal place relative accuracy in worst case). Even in random error accumulation, you might see error on the order of &amp;lt;math&amp;gt;\sqrt{2048} \times 0.0009766 \approx 0.044&amp;lt;/math&amp;gt; or 4.4% (though this is a rough estimate). That’s quite significant. Fortunately, as discussed, most hardware will accumulate such sums in a higher precision like FP32, dramatically reducing that error (~7 orders of magnitude smaller &#039;&#039;ε&#039;&#039;). But if it didn’t, the network’s outputs could get noisy. A fully-connected layer with many inputs might output slightly different results depending on summation order if done in low precision.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Large-scale reductions&#039;&#039;&#039; also occur in operations like global average pooling (summing all elements of a feature map) or computing the total loss by summing loss over batch samples. If one sums 100,000 values in FP32, the worst-case error could be on the order of 100,000 &#039;&#039; 1e-7 ≈ 1e-2 (1% uncertainty), though typical error would be much less. In FP16, summing 100k values is likely impossible without overflow: as noted, adding ~10^4 moderate numbers can overflow FP16&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. So large reductions &#039;&#039;must* be handled carefully (either in chunks or in higher precision). Frameworks generally do: for example, when performing a sum reduction on GPU, libraries break it into parallel blocks that accumulate in registers (usually 32-bit registers, even if inputs are 16-bit). &lt;br /&gt;
&lt;br /&gt;
Another aspect is that many dot products in neural nets involve values of varying magnitude. If the distribution of terms is such that some are much larger than others, the smaller terms might not contribute much if summed last. Ideally, one might sort by magnitude or use pairwise summation to improve that. Most BLAS libraries effectively do a pairwise strategy due to their parallel structure. So, while worst-case analyses are grim, practical implementations mitigate a lot of it.&lt;br /&gt;
&lt;br /&gt;
However, certain layers can suffer if not handled properly. One famous example is in RNNs or LSTMs: if one uses FP16 naive summation for the gating mechanism, errors could accumulate over many time steps and cause drift or instability. In modern practice, recurrent layers are often run in FP16 with FP32 accumulators to avoid this.&lt;br /&gt;
&lt;br /&gt;
In summary, &#039;&#039;&#039;dot products and large reductions need sufficient precision&#039;&#039;&#039; or robust algorithms. If you are implementing a custom kernel (say a custom ONNX op) and you have to sum a huge array, you should consider using a technique from Section 3 (pairwise summation, Kahan, or at least double accumulator if possible). Many deep learning computations, by virtue of hardware and library design, already do something like this under the hood. But it&#039;s always good to be aware, especially if pushing into lower precision territories.&lt;br /&gt;
&lt;br /&gt;
=== 6.2. Cancellation, Dynamic Range, and Conditioning ===&lt;br /&gt;
&lt;br /&gt;
We introduced &#039;&#039;cancellation&#039;&#039; earlier in the context of summation. In deep learning, cancellation can appear in a few places. One is in the computation of certain norms or variances. For instance, Batch Normalization computes mean and variance of activations. The variance formula &amp;lt;math&amp;gt;\mathrm{Var}(x) = E[x^2] - (E[x])^2&amp;lt;/math&amp;gt; is subject to catastrophic cancellation if &#039;&#039;E[x]&#039;&#039; and &amp;lt;math&amp;gt;\sqrt{E[x^2]}&amp;lt;/math&amp;gt; are close in magnitude (which can happen if the variance is small compared to the mean). If implemented naively in single precision, you might get a negative variance due to numerical error (which would then be clamped to zero or sqrt would yield NaN). Robust implementations compute variance in a single pass or increase precision. Indeed, in many frameworks, batchnorm accumulation is done in FP32 even if the activations are FP16, precisely to avoid numerical issues in computing mean and variance. ONNX itself doesn’t specify this, but an ONNX backend is likely to do batchnorm stats in higher precision for safety. The &#039;&#039;conditioning&#039;&#039; of the variance calculation is poor when variance is tiny relative to mean, so it&#039;s a spot where higher precision is needed.&lt;br /&gt;
&lt;br /&gt;
Another example is in certain loss functions or regularizations. If you subtract two large numbers to get a small difference, you lose precision. Say you have two nearly identical predictions and you subtract them as part of some operation – if done in float16, you might get zero whereas float32 would have a slight difference. This could affect, say, a gradient if that difference was supposed to propagate information. Fortunately, neural nets often avoid directly subtracting large nearly equal numbers; but it can happen. In attention mechanisms, the &#039;&#039;score normalization&#039;&#039; (softmax) subtracts a max to avoid huge exponentials, which is good for range but if multiple scores are close to the max, you subtract numbers that are close. However, those subtractions are not catastrophic in the same way; they&#039;re intentional to reduce range and are typically fine in FP32 or even FP16.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Dynamic range&#039;&#039;&#039; issues are everywhere in deep learning. Activation functions like ReLU and exponential can produce very large or very small values. If subsequent computations don’t account for that, you get infinities or zeros. For example, consider a network output that produces a logit of 100 in FP16. &amp;lt;math&amp;gt;\exp(100)&amp;lt;/math&amp;gt; in FP16 overflows to infinity (since FP16 max ~ 6e4 and &#039;&#039;e&amp;lt;sup&amp;gt;100&amp;lt;/sup&amp;gt; ∼ 3.7e43&#039;&#039;). If you then do softmax, that one infinity will dominate the sum and you might get a probability of 1 for that class (or NaNs if not handled). The standard softmax implementation subtracts the max logit, so in this case it would subtract 100 from all logits, making the largest exponent &amp;lt;math&amp;gt;\exp(0)=1&amp;lt;/math&amp;gt; and others tiny – this avoids overflow. But what about underflow? If you had a very negative logit, &amp;lt;math&amp;gt;\exp(-100)&amp;lt;/math&amp;gt; in FP16 would underflow to 0. In softmax, that just contributes ~0 probability, which is fine (it effectively is zero anyway). Underflow tends to be less dangerous than overflow in inference — underflow usually just means “this probability/neuron is effectively 0,” whereas overflow can poison the results with NaNs or Infs. &lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Conditioning&#039;&#039;&#039; refers to how sensitive a function is to input perturbations. Neural networks themselves as functions can be poorly conditioned (e.g., tiny changes in weights cause big changes in output for some architectures), but typically they are designed to be reasonably well-behaved. Certain internal computations like matrix inversion (if one does a pseudoinverse or something for a layer) can be extremely ill-conditioned if the matrix is near singular. That again would need double precision perhaps. In ML, one common ill-conditioned operation is the &#039;&#039;&#039;softmax denominator&#039;&#039;&#039;: &amp;lt;math&amp;gt;\sum_j e^{x_j}&amp;lt;/math&amp;gt; can be very large and also very sensitive to changes in the largest &#039;&#039;x&amp;lt;sub&amp;gt;j&amp;lt;/sub&amp;gt;&#039;&#039;. However, the softmax is more stable if computed carefully (max trick). Another is &#039;&#039;&#039;normalization&#039;&#039;&#039;: computing &amp;lt;math&amp;gt;\frac{x - \mu}{\sigma}&amp;lt;/math&amp;gt; for layer norm or batch norm – if &amp;lt;math&amp;gt;\sigma&amp;lt;/math&amp;gt; is very small, then the division blows up noise. In those cases, a small &#039;&#039;ε&#039;&#039; is usually added to &amp;lt;math&amp;gt;\sigma^2&amp;lt;/math&amp;gt; to prevent instability (this is a model-level fix, not a numeric one per se, but it acknowledges numeric limits).&lt;br /&gt;
&lt;br /&gt;
In training, &#039;&#039;&#039;optimizers&#039;&#039;&#039; can suffer from precision issues. For example, the Adam optimizer keeps a moving average of squared gradients; in half precision, these small updates might flush to zero or not change at some point. Some research found that certain adaptive optimizers break in FP16 because the small differences are lost (&amp;quot;Why FP16 training breaks NAdam&amp;quot; etc.). The solution is often to keep the optimizer states in FP32 while doing the bulk of compute in FP16. This again is a form of mixed precision.&lt;br /&gt;
&lt;br /&gt;
From an inference perspective, once the model is trained, the main concerns are avoiding overflow in activations and ensuring summations (like in softmax or reductions) are done with enough precision. The model’s weights are fixed, so we don&#039;t have to worry about cumulative error over iterations as in training, just the forward pass.&lt;br /&gt;
&lt;br /&gt;
=== 6.3. Normalization Layers and Softmax ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Normalization layers&#039;&#039;&#039; (BatchNorm, LayerNorm, etc.) and &#039;&#039;&#039;Softmax&#039;&#039;&#039; are specifically worth examining for numerical stability:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Batch Normalization:&#039;&#039;&#039; During inference, batchnorm applies an affine transform using precomputed mean and variance (from training). It’s basically: &amp;lt;math&amp;gt;y = \frac{x - \mu}{\sqrt{\sigma^2 + \epsilon}} \gamma + \beta&amp;lt;/math&amp;gt;. This is a stable operation as long as &#039;&#039;ε&#039;&#039; is provided (to avoid division by zero). Since &amp;lt;math&amp;gt;\mu, \sigma^2&amp;lt;/math&amp;gt; are constants in inference, it reduces to linear ops which are fine. The main numerical work (computing mean/variance) happened during training or model calibration, and frameworks usually did that in FP32. So batchnorm at inference time is not a source of instability (it’s just a scale and shift of the data).&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Layer Normalization / Group Normalization:&#039;&#039;&#039; These compute mean/var on the fly for each sample (and possibly channel group). In inference, they still need to compute these statistics for each forward pass. If implemented in low precision, the variance calculation can suffer cancellation as mentioned. However, it&#039;s likely that inference engines compute the mean and variance in FP32 even if inputs are FP16 (especially since these are per-sample operations, not huge batch reductions – it&#039;s feasible to do in higher precision). If someone implemented layer norm purely in FP16, they might see issues when the variance is very small. Usually there’s an &#039;&#039;ε&#039;&#039; added which might dominate a tiny variance and thus mitigate catastrophic cancellation (the result might just output something mostly from &#039;&#039;ε&#039;&#039;). In summary, normalization layers are usually fine as long as the implementation uses enough precision for the summation of squares.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Softmax:&#039;&#039;&#039; Softmax is notorious if done improperly. The safe algorithm: subtract the maximum input from all inputs (to bring the largest exponent to 1.0), exponentiate, sum them up, divide each exponent by the sum. The max subtraction handles overflow in &amp;lt;math&amp;gt;\exp()&amp;lt;/math&amp;gt;. But what about the sum? As discussed, if there are &#039;&#039;N&#039;&#039; elements in the softmax, the sum is at most &#039;&#039;N&#039;&#039; (when all inputs equal the max). In classification, &#039;&#039;N&#039;&#039; might be the number of classes. For ImageNet, &#039;&#039;N=1000&#039;&#039;, so worst sum ~1000 (which is fine in FP16, as 1000 &amp;lt;&amp;lt; 65504). For an extreme case like a language model, &#039;&#039;N&#039;&#039; could be 50,000 (vocab size). Sum could be 50,000 in worst case (everyone equal probability). 50,000 is still below 65504, so actually an FP16 sum would &#039;&#039;not&#039;&#039; overflow in that scenario, and the rounding error on a sum of 50k identical terms would be maybe a few ULPs (which at 50k magnitude might be ~0.5 difference worst-case). So maybe softmax summation in FP16 is not catastrophic for output probabilities – you’d get essentially 1.0 after normalization instead of 0.99999 in some case. Usually fine.&lt;br /&gt;
&lt;br /&gt;
:The bigger issue is if one or a few terms dominate the softmax. If one term is much larger, the others underflow to 0 after the max subtraction. That’s fine: you get a one-hot distribution essentially. If the model expected a softer distribution, that could be a problem. But if the logits were really that far apart, even FP32 would give nearly one-hot. So not really a precision issue, more of a model property.&lt;br /&gt;
&lt;br /&gt;
:However, the &#039;&#039;&#039;stability during training&#039;&#039;&#039; is a big issue. The GitHub thread we saw (ColossalAI) mentioned training a 130B model where FP16 softmax in attention caused training to occasionally blow up&amp;lt;ref name=&amp;quot;ref14&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref14&amp;quot;/&amp;gt;. They observed attention scores in some heads ranging up to 1e4 or as low as 1e-3&amp;lt;ref name=&amp;quot;ref14&amp;quot;/&amp;gt;. 1e4 exponent in FP16 is definitely Inf (because &amp;lt;math&amp;gt;\exp(10^4)&amp;lt;/math&amp;gt; is astronomically huge, but even exp(100) is inf in half). The method CogView introduced (Precision Bottleneck Relaxation) involved subtracting the max &#039;&#039;per head&#039;&#039; to reduce range&amp;lt;ref name=&amp;quot;ref14&amp;quot;/&amp;gt;—which is basically the softmax max trick at a finer granularity or something similar. In the end, they found &#039;&#039;&#039;the simplest fix was to compute softmax in FP32&#039;&#039;&#039; for those layers&amp;lt;ref name=&amp;quot;ref14&amp;quot;/&amp;gt;. They reported that it had negligible speed impact but significantly improved stability&amp;lt;ref name=&amp;quot;ref14&amp;quot;/&amp;gt;. This highlights a general principle: certain operations like softmax (and sometimes matrix multiplications in attention) benefit from higher precision, and using FP32 in a few critical spots can save a mixed-precision training from crashing. In inference, using FP32 for softmax is also a safeguard if one is uncertain. It costs little because softmax is not a heavy compute compared to matrix multiplies.&lt;br /&gt;
&lt;br /&gt;
:ONNX does have a Softmax operator; if the input is float16, a backend might do exactly what they described: internally cast to float32 for the softmax computation, then cast output back to float16. Some inference engines (like DeepSpeed’s inference or others) keep attention softmax in FP32 even if rest is FP16, for robustness&amp;lt;ref name=&amp;quot;ref14&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Apart from softmax, other places where exponentials occur are in loss functions like cross-entropy (which uses log-softmax internally), or in activation functions like Sigmoid and Tanh (which are basically scaled logistic functions). Sigmoid(&#039;&#039;x)=1/(1+e&amp;lt;sup&amp;gt;-x&amp;lt;/sup&amp;gt;)&#039;&#039;. If &#039;&#039;x&#039;&#039; is moderately large positive, &#039;&#039;e&amp;lt;sup&amp;gt;-x&amp;lt;/sup&amp;gt;&#039;&#039; underflows to 0 in FP16 if &amp;lt;math&amp;gt;x&amp;gt;~11&amp;lt;/math&amp;gt;. That gives a sigmoid output of 1.0 exactly. In FP32, &#039;&#039;e&amp;lt;sup&amp;gt;-x&amp;lt;/sup&amp;gt;&#039;&#039; for &#039;&#039;x=11&#039;&#039; is about &#039;&#039;5e-5&#039;&#039;, not underflow, so sigmoid ~0.99995. So FP16 will saturate some outputs to exactly 0 or 1 sooner than FP32 would. This can slightly change behaviors (e.g., a neuron might go to full saturation a bit early). Usually, this isn’t a huge deal, but it could make gradients zero earlier. In inference, we don’t care about gradients, just the output value. Getting exactly 1.0 vs 0.99995 probably has no practical difference for a classification output. But one should be aware that &#039;&#039;&#039;activation functions can saturate faster&#039;&#039;&#039; in lower precision due to limited range or precision. ReLU has no issue (it’s linear or 0), but exponential-based activations do. In practice, again, frameworks often simply compute those in the given precision and accept the slight difference. If it were a problem, one could do those in FP32 as well.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Summary:&#039;&#039;&#039; Numerical stability in inference is maintained by using robust computation methods: subtracting maxima in softmax/LogSumExp, adding epsilon in denominators for normalization, and using higher precision for summations or critical steps. Most of these are built into the algorithms. ONNX models assume these standard practices (they don&#039;t need to explicitly do them in the graph because the backend inherently will for ops like Softmax). It&#039;s wise when building or exporting models to ONNX to test them in the target precision and ensure there are no surprises (like an output becomes NaN or Inf due to precision). Usually if training was done in mixed precision, the model will have been vetted in that environment already.&lt;br /&gt;
&lt;br /&gt;
== Performance vs. Accuracy Trade-offs ==&lt;br /&gt;
&lt;br /&gt;
There is an inherent trade-off between numerical precision and computational performance (speed, memory usage) in AI systems. Choosing lower precision can dramatically increase throughput and reduce memory, but it may introduce arithmetic errors. Engineers must balance these considerations:&lt;br /&gt;
&lt;br /&gt;
=== 7.1. Throughput, Memory Bandwidth, and Precision ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Throughput (speed)&#039;&#039;&#039; is often higher for lower-precision operations. Modern hardware can execute more low-precision ops in parallel than high-precision ops. For example, an NVIDIA V100 GPU can perform 2× more half-precision FLOPs than single-precision per clock, and newer architectures with Tensor Cores can do 8× more (since a Tensor Core warp operation might perform 64 FP16 FMAs per cycle vs 16 FP32). Similarly, int8 operations can be even faster and more energy efficient. Lower precision numbers also use less memory bandwidth – fetching 16-bit data is twice as fast as 32-bit data from memory (assuming the same bus width), which can relieve memory bottlenecks. Many deep learning workloads are memory-bound, so halving the data size can nearly double throughput if compute is not the limiting factor. This is a huge incentive to use lower precision.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Memory and storage&#039;&#039;&#039;: Using FP16 or int8 weights reduces model size, which is important for deploying large models to memory-limited environments (like edge devices or GPUs with limited VRAM). It also means you can fit larger batch sizes in memory, or more models on a chip, etc. There is also a cascade effect: smaller data means better cache utilization, which further improves performance.&lt;br /&gt;
&lt;br /&gt;
However, &#039;&#039;&#039;accuracy&#039;&#039;&#039; suffers if precision is too low. We have discussed how rounding error and limited range can cause deviations in outputs. The question is: do those deviations matter for the application? Often for neural networks, a small amount of noise or error in intermediate calculations does not change the final prediction. Neural nets are somewhat error-tolerant due to their redundant distributed representations. This is why quantization and FP16 techniques can work at all. The final model accuracy (e.g. top-1 accuracy on ImageNet) might drop only 0.5% or less when using FP16 instead of FP32, or int8 instead of FP16, if done properly. For many use-cases, this loss is acceptable given the speed gains.&lt;br /&gt;
&lt;br /&gt;
If one goes too low (like naive FP8 or int4 without special care), accuracy might drop severely – perhaps making the model unusable. Thus the trade-off: as precision decreases, performance increases but at some point accuracy degrades beyond an acceptable threshold. Engineers aim to find the lowest precision that still preserves accuracy.&lt;br /&gt;
&lt;br /&gt;
There is also the consideration of &#039;&#039;&#039;accumulation strategy&#039;&#039;&#039; affecting performance: for instance, Kahan summation (compensated) is more accurate but ~4× slower than naive summation. If you need that accuracy, you pay a performance cost. In many AI inference scenarios, the slight extra accuracy from something like Kahan isn&#039;t needed, so it’s not used. But in a scenario where you sum millions of elements for a final result that is thresholded, maybe you do need it.&lt;br /&gt;
&lt;br /&gt;
Another dimension is &#039;&#039;&#039;determinism vs speed&#039;&#039;&#039;. Enforcing a deterministic reduction order (for reproducibility) might require disabling some parallelism, which could slow things down. If exact repeatability is critical (like certain scientific or financial applications of neural nets), one might sacrifice some performance to ensure the summation happens the same way every time. ONNX allows both modes depending on the runtime.&lt;br /&gt;
&lt;br /&gt;
From a memory perspective, if precision is higher than needed, you waste memory. For example, storing intermediate activations in FP32 when they could be FP16 uses 2× memory, which might mean lower batch size or more memory traffic. So there&#039;s strong motivation to keep things as low-precision as possible &#039;&#039;except&#039;&#039; where accuracy truly demands.&lt;br /&gt;
&lt;br /&gt;
In summary, the trade-off often manifests as &#039;&#039;diminishing returns&#039;&#039;: FP64 vs FP32 – huge slowdown for usually negligible accuracy gain in ML; FP32 vs FP16 – moderate speedup for usually minor accuracy hit; FP16 vs int8 – another big speedup but maybe more accuracy loss unless calibration is done; int8 vs int4 – speed gain but often big accuracy drop that might not be worth it for current models.&lt;br /&gt;
&lt;br /&gt;
Engineers use techniques like &#039;&#039;&#039;mixed precision&#039;&#039;&#039; to target high precision only where needed. For example, run most of the model in FP16, but keep a few sensitive layers in FP32. This yields most of the performance benefits while ensuring critical parts are accurate. We already saw softmax as one example. Another might be final output layer: sometimes using FP32 for the logits and softmax can be safer.&lt;br /&gt;
&lt;br /&gt;
Hardware is also trending to support mixed modes – like FP8 Tensor Cores accumulate in FP32, etc. So hardware tries to give accuracy cheaply.&lt;br /&gt;
&lt;br /&gt;
=== 7.2. Hardware Accelerators and Tensor Cores ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Hardware accelerators&#039;&#039;&#039; (GPUs, TPUs, FPGAs, ASICs like GraphCore IPU, etc.) have specific support for certain precisions. Leveraging these can provide massive speedups:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;NVIDIA Tensor Cores:&#039;&#039;&#039; These started in Volta (V100) with support for 4x4 matrix multiply-adds on FP16 inputs with FP32 accumulation&amp;lt;ref name=&amp;quot;ref6&amp;quot;/&amp;gt;. Using Tensor Cores can give up to 8x throughput compared to using regular FP32 cores. But to use them, the data needs to be FP16 (or now BF16 or FP8 on newer GPUs) and aligned to certain dimensions (multiples of 8 etc. in matrix sizes, which frameworks handle by padding if needed). If an ONNX model is all FP32, by default it would not use Tensor Cores unless the runtime automatically converts some ops to mixed precision (which some do via a flag or environment variable enabling TF32 or FP16). The introduction of &#039;&#039;&#039;TensorFloat-32 (TF32)&#039;&#039;&#039; in Ampere GPUs is essentially a compromise: treat FP32 inputs as two 16-bit significand halves (10-bit mantissa actually) so that it can run on Tensor Cores, giving ~8x speed, while still outputting FP32. TF32 has about the precision of FP16 but range of FP32, and it was chosen because it typically doesn’t hurt convnet accuracy. ONNX Runtime or others on Ampere will use TF32 by default for conv and matmul unless disabled, giving a free speed boost at minimal accuracy cost. If exact FP32 is needed, users have to turn it off (sacrificing speed).&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;TPU (Google):&#039;&#039;&#039; TPUs have native support for BF16 matrix ops with FP32 accumulators. They were designed specifically for the mixed precision approach in training. For inference, TPUs also support int8 and other quantized ops (e.g., via Edge TPU for lower-power). If exporting ONNX to run on a TPU (via some bridge), one might get automatic BF16 usage. BF16 has the advantage of no loss in range, so TPUs can more straightforwardly run models in BF16 without modifications.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;FPGAs and ASICs:&#039;&#039;&#039; Some custom accelerators might use fixed-point or block floating-point arithmetic for efficiency. They often treat the network in quantized terms. The ONNX quantization specification (QuantizeLinear/DequantizeLinear ops) exists for representing quantized models (INT8 etc.), which is another domain beyond floating-point, but it overlaps as a precision issue. In those systems, accumulators for INT8 × INT8 multiplies are usually 32-bit (to avoid overflow for dot products up to certain lengths). The trade-off there is between integer vs float representation, which can give big speedups on CPUs/DSAs via SIMD instructions (many CPUs have vector int8 multiply-add instructions now for inference).&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Memory bandwidth on hardware:&#039;&#039;&#039; Some accelerators include special high-bandwidth memory or cache specifically for lower precision. For example, an accelerator might fetch two FP16 values in one 32-bit memory transaction – effectively doubling memory bandwidth for half precision. On some hardware, using FP16 can even reduce memory &#039;&#039;latency&#039;&#039; if multiple values can be packed.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Parallelism and Vectorization:&#039;&#039;&#039; Many hardware units process data in fixed vector widths (e.g., 256-bit registers). That could be 8 floats at once or 16 half-floats at once in the same 256 bits. If your code is in FP16, you might use the full vector width (16 values) whereas FP32 might only use half (8 values). This means FP16 can make better use of vector units. Similarly, matrix units often have fixed tile sizes, and smaller data types allow larger tiles or more tiles concurrently.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Energy efficiency:&#039;&#039;&#039; Lower precision often means less energy per operation, which on large deployments (data centers or battery-powered devices) is a significant advantage. This isn&#039;t directly performance, but it&#039;s an important trade: you might choose a slightly lower precision to save power even if accuracy drops a tiny bit.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Software overhead:&#039;&#039;&#039; Using mixed precision on hardware involves some overhead of casting and moving data between precisions. For instance, if you have to frequently cast FP16 to FP32 and back, that adds instructions and memory copies. A well-designed accelerator (like GPUs with unified memory for FP16/32 or special hardware casting) minimize this. Still, the runtime might insert extra operations (as we saw, ONNX Runtime CPU inserted many Cast nodes converting FP16 to FP32 and back&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;, which actually &#039;&#039;slowed down&#039;&#039; that FP16 model by 4x vs FP32 on a CPU, because the CPU had no native FP16). So ironically, on some hardware, using lower precision in ONNX might degrade performance if not supported (the CPU case). Always ensure the target hardware actually supports the low precision you plan to use; otherwise, you&#039;ll pay overhead for emulation.&lt;br /&gt;
&lt;br /&gt;
To illustrate trade-off: Suppose you have a model that achieves 99% accuracy in FP32 and drops to 98.5% in FP16. If FP16 runs 2x faster, many would accept the 0.5% accuracy drop for double throughput. If int8 runs 4x faster but accuracy drops to 95%, that might be too much drop for some applications, but acceptable for others that value speed. It&#039;s application-dependent.&lt;br /&gt;
&lt;br /&gt;
In critical applications, one might maintain higher precision accumulators or run certain validation steps in high precision to ensure nothing drifted. In research, sometimes a model is trained in mixed precision but validated in double to ensure any small numeric differences don’t influence the reported result (rare, but for scientific computing integration maybe).&lt;br /&gt;
&lt;br /&gt;
In the ONNX world, performance-vs-accuracy is often managed via &#039;&#039;&#039;Execution Providers&#039;&#039;&#039; (EPs) that target specific hardware. For example, the TensorRT EP in ONNX Runtime can take an ONNX model and run it using NVIDIA&#039;s high-performance inference library, which will use FP16 or INT8 if allowed, to maximize speed. The user can configure the EP with precision flags. If the accuracy drop is too high, one might restrict it to FP32. If it&#039;s fine, allow FP16. Tools exist to measure the accuracy difference (like calibrating int8 quantization).&lt;br /&gt;
&lt;br /&gt;
To conclude, the trade-offs revolve around picking the lowest precision that yields acceptable accuracy, and utilizing hardware features fully. ONNX being a high-level spec abstracts this, but it&#039;s ultimately up to the deployment to decide and sometimes up to the exporter to incorporate quantization. ONNX’s role is to be able to &#039;&#039;represent&#039;&#039; these choices (they have added quantized types, FP8 types, etc., to allow the graph to include those when needed).&lt;br /&gt;
&lt;br /&gt;
== Best Practices and Design Guidelines ==&lt;br /&gt;
&lt;br /&gt;
Given the above insights, here are some best practices and guidelines for dealing with numerical precision and accumulators in AI inference (and training) design:&lt;br /&gt;
&lt;br /&gt;
=== 8.1. Choosing an Accumulator Precision ===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Match precision to data length and range:&#039;&#039;&#039; If an operation accumulates a large number of terms, consider using a higher precision accumulator than the input data. For example, for summing &amp;gt;1000 values of type FP16, an FP32 accumulator is recommended to avoid large rounding error or overflow. If summing &amp;gt; million values of FP32, consider double (FP64) accumulation if high accuracy is needed (though this is rare in DL). &#039;&#039;Rule of thumb:&#039;&#039; if &amp;lt;math&amp;gt;N \cdot \varepsilon&amp;lt;/math&amp;gt; is not tiny (say &amp;gt;1e-3), you may benefit from higher precision. For FP16 (&#039;&#039;ε ≈ 1e-3&#039;&#039;), that threshold N is a few hundred. For FP32 (&#039;&#039;ε ≈ 1e-7&#039;&#039;), N would be in the millions before 1e-3 error.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Use higher precision in critical layers:&#039;&#039;&#039; Identify if certain layers of the model are more sensitive to precision. For instance, the output layer or a layer that feeds into a very sharp decision boundary might require more precision. If a small change there could flip the prediction, you don&#039;t want excessive numerical error. Using FP32 for logits and softmax is a common practice to maintain classification confidence stability.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Follow hardware defaults, but override if needed:&#039;&#039;&#039; Many hardware accelerators have sensible defaults (like accumulate in FP32 for tensor ops). It&#039;s usually best to use those defaults as they were chosen to balance accuracy and speed. Only override if you have evidence of a problem. For example, if using ONNX Runtime on CPU, you discovered it upcasts to float for you – great. But if using some custom accelerator that does pure FP16, you might manually insert casts in the ONNX graph around certain ops to force higher precision on those ops.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Maintain precision for gradient accumulation (training):&#039;&#039;&#039; This is more training-oriented, but worth noting: when summing gradients over batches or updating weights, use higher precision (FP32 or even 64) for those accumulations. This ensures training doesn&#039;t diverge due to numeric error. Many frameworks keep a &amp;quot;master copy&amp;quot; of weights in FP32 when training in FP16&amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;. For inference, this translates to: any running statistics or counters in the model (like if you had a running average inside the model) should probably be in higher precision to avoid drift.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Be mindful of intermediate cast costs:&#039;&#039;&#039; If you do decide to cast to higher precision for an operation in ONNX, remember it incurs memory copies and compute overhead. Make sure it&#039;s worth it. For example, if you cast a huge tensor to double, that could slow things drastically. Usually FP16-&amp;gt;FP32 is fine (hardware handles it well), but FP32-&amp;gt;FP64 might be expensive on GPUs.&lt;br /&gt;
&lt;br /&gt;
=== 8.2. Safe Reduction Lengths and Heuristics ===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Chunk long reductions:&#039;&#039;&#039; If you must do a very long sum in low precision (due to memory constraints or hardware), chunk the sum into parts, sum each to a higher precision local accumulator, then sum the partial sums. This is essentially pairwise summation. Many libraries use a fixed chunk size heuristic (e.g., sum in blocks of 256 in registers, then add those up). This bounds the error growth. If implementing manually, you can choose a chunk size such that chunk_length &#039;&#039; ε is small (like 256 &#039;&#039; 1e-3 = 0.256 for FP16, hmm maybe choose 100).&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Sort by magnitude if possible:&#039;&#039;&#039; In some cases (not typical in DL, but possible in post-processing), if you have numbers varying by orders of magnitude, summing from smallest to largest improves accuracy. It prevents small ones from being swallowed by big ones. This is more relevant if you output a sum of mixed-scale values (like some algorithm mixing big and small contributions). It’s not always feasible in real-time inference to sort, but keep in mind.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Use compensation for critical sums:&#039;&#039;&#039; If you find a particular sum or difference in your inference computation is crucial (like computing a small residual from two close numbers), consider using Kahan summation or another compensated scheme just for that part. For example, if you had an ONNX custom function that needs to sum a lot of almost cancelling terms, you might implement it with compensation internally.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Heuristics for FP8/low-bit:&#039;&#039;&#039; When using extremely low precision like FP8, adopt the recommended practices: use scaling (don&#039;t feed raw data into FP8, normalize it first), and perhaps only use FP8 on portions of the network that are empirically robust. The industry is still establishing heuristics, but one might be: use FP8 for matrix multiplications, but accumulate in FP16 or FP32; keep normalization in FP16; use FP16 for first and last layer maybe. This is evolving, so keep an eye on literature.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Keep an eye on sum length in softmax and normalization:&#039;&#039;&#039; If you have extremely large class counts or group sizes (e.g., a softmax over 1e6 items, or layer norm over tens of thousands of features), realize this is a giant reduction. You may want to ensure that reduction is done in higher precision. ONNX Softmax by default will likely not handle a million-class case in FP16 well (the sum would overflow since 1e6 &amp;gt; 65504). So either do that on CPU in FP32 or break it down (could do hierarchical softmax, etc.). This is a corner case, but relevant for huge vocab language models. In practice, vocab ~50k is fine as we said.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Test numeric differences:&#039;&#039;&#039; A practical tip: after quantizing or reducing precision, run some test inputs through both the high-precision model and low-precision model. Compare outputs (and maybe some intermediate signals if possible). If differences are consistently small relative to the application tolerance, you&#039;re good. If you see occasional large differences or instabilities (like outputs become NaN or Inf in low precision), that&#039;s a red flag. Use that testing to guide where you might need to increase precision or change algorithms.&lt;br /&gt;
&lt;br /&gt;
=== 8.3. When Higher Precision Is Required ===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Ill-conditioned calculations:&#039;&#039;&#039; If your model or pipeline performs any operation known to be ill-conditioned (matrix inversion, subtraction of nearly equal quantities, computing a tiny difference of large numbers), you likely need higher precision for that part. For example, if your ONNX model includes a custom op to solve a linear system, use double precision in that op if possible (or at least float with iterative refinement). In ML, this might occur in something like a physics-informed neural network or a differentiable simulation within the model.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Strict accuracy requirements:&#039;&#039;&#039; Some applications (medical, aerospace, finance) might have strict tolerances. If an inference model’s output needs to be correct to 5 decimal places, you probably cannot use FP16, which has only 3-4 decimal digits precision. FP32 might be barely enough (~7 digits). FP64 might be warranted to guarantee that level of accuracy in worst case. Most deep learning applications (like image classification) don&#039;t demand that level of numeric precision in outputs – as long as the top class is correct, it doesn&#039;t matter if its probability was 0.901 vs 0.899. But if you&#039;re doing something like summing probabilities to make a risk estimate or doing a cumulative sum over many predictions, be careful to accumulate precisely.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Large sums or products in post-processing:&#039;&#039;&#039; Sometimes the neural net is fine, but the &#039;&#039;post-processing&#039;&#039; of results might need precision. E.g., summing up probabilities of many events, or computing confidence intervals, etc. Do those calculations in double if needed outside the model.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Preventing drift in long sequences:&#039;&#039;&#039; For sequential models (RNNs processing thousands of steps), rounding error can accumulate over steps. If each step has a small error, after 10000 steps it could add up. In such cases, using a higher precision for the state might reduce drift. Some practitioners use FP64 for the hidden state accumulation in very long sequence processing to maintain numerical stability (though this is not common due to speed loss). Alternatively, periodically renormalize or reset small errors.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Simulations or differential equations:&#039;&#039;&#039; If your ONNX model involves simulating something (like a differential equation solver inside a model, or an ODE/RNN hybrid), those often need double precision to be stable for long durations. Recognize those situations and allocate precision accordingly.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Verification and debugging:&#039;&#039;&#039; Use higher precision as a tool to verify correctness. For example, run your model in FP64 (if possible) and compare to FP32 to see if there are differences. If not, FP32 is fine. If yes, you might find a particular calculation losing too much info in FP32, implying maybe you should keep it in FP64 in production as well. This is rare in inference but can be part of rigorous validation.&lt;br /&gt;
&lt;br /&gt;
In ONNX, using higher precision may involve casting to &amp;lt;code&amp;gt;tensor(double)&amp;lt;/code&amp;gt; for certain ops. Not all runtimes support double on GPU though (some do via emulation or just slower path). ONNX CPU does support double for general ops. So be mindful of the target.&lt;br /&gt;
&lt;br /&gt;
Finally, &#039;&#039;&#039;document your precision choices&#039;&#039;&#039;. If you deliver an ONNX model that uses mixed types (float16 inputs, float32 accumulations, int8 weights, etc.), ensure that is clear to whoever maintains or uses it. Precision issues can be sneaky, so clarity helps.&lt;br /&gt;
&lt;br /&gt;
== Limitations and Open Problems ==&lt;br /&gt;
&lt;br /&gt;
Despite advances in handling numerical precision, there remain limitations and open challenges in this domain:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Lack of Precision Specification in ONNX:&#039;&#039;&#039; As noted, ONNX currently has no way to formally specify that an op &#039;&#039;should&#039;&#039; use a higher precision internally. This can lead to nondeterministic differences across platforms&amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;. While proposals exist to add attributes for this&amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;, it is not yet standard. Thus an ONNX model might behave slightly differently on two compliant backends, and there&#039;s no built-in mechanism to ensure identical results. This is a limitation for applications requiring cross-platform consistency. Solving it may involve extending ONNX (e.g., an attribute like &amp;lt;code&amp;gt;accumulatePrecision=&amp;quot;FLOAT32&amp;quot;&amp;lt;/code&amp;gt; for MatMul) or tightening the spec, but those come with backward compatibility and implementation burden concerns.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Reproducibility and Determinism:&#039;&#039;&#039; Even with the same precision, parallel computation can introduce nondeterminism (due to different summation orders). Ensuring bit-identical results across runs (important for some sensitive domains) is non-trivial. Frameworks often provide &amp;quot;deterministic mode&amp;quot; which might use fixed ordering at cost of speed. In ONNX, there&#039;s no global flag for this; it&#039;s up to the runtime. More research could be done on algorithms that produce bit-wise identical results independent of parallel execution order (perhaps using techniques like pairwise summation or consistent partitioning). Stochastic rounding is another area: sometimes using probabilistic rounding can reduce bias in accumulated error, but it introduces randomness – reconciling that with reproducibility is tricky.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Ultra-low Precision (FP8 and below):&#039;&#039;&#039; While FP8 is being explored, going to 4-bit floats or mixed schemes is an open challenge. The OpenAI community is investigating 4-bit (quantization-wise) for LLMs with some success, but a true 4-bit float (like E3M1) would be extremely limited. Perhaps hybrid schemes (like block floating point, log scales, etc.) will come. ONNX already has some support (they mention 4-bit in documentation placeholders&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;), but it&#039;s nascent. Research into training techniques, calibration, and new number formats (like &#039;&#039;&#039;posit numbers&#039;&#039;&#039; or tapering precision floats) is ongoing. If a new format proves useful, ONNX will need to adapt to represent it.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Quantization Noise and Model Robustness:&#039;&#039;&#039; Reducing precision can sometimes expose models to issues like adversarial vulnerability or fragility – e.g., a slight perturbation might flip a decision if the margin was small and precision loss nudged it. Ensuring model robustness under quantization is an open problem. It might require re-training the model with quantization in the loop (quantization-aware training) or designing models that have large &amp;quot;margin&amp;quot; between classes to tolerate quantization noise.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Dynamic range during training vs inference:&#039;&#039;&#039; In training, activations can have wider distributions (especially early in training). Models often learn to constrain their ranges (via activation functions saturating or normalization), which makes inference more stable in lower precision. But if one tries to train in FP8 from scratch, it&#039;s very hard. This gap between what&#039;s possible in a post-trained inference model vs training is an open area – how to train directly in low precision without significant accuracy loss. Solving that could simplify workflows (no need for high-precision training then quantize; just train low-prec end to end). Techniques like loss scaling, gradient clipping, etc., are partial solutions. The development of optimizers that are resilient to low precision is ongoing.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Hardware variability:&#039;&#039;&#039; As new hardware emerges (with new numeric capabilities or quirks), ensuring that ONNX models run consistently is an ongoing task. For instance, one device might implement a fused op that reduces rounding error, another does it separately. Even something like convolution might use a different algorithm (FFT-based vs direct) which has different numeric properties. Open problem: could ONNX allow specifying numeric tolerances per op or preferred algorithms? That may be overkill for most, but for some HPC ML use cases, maybe desired.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Testing and validation of numerics:&#039;&#039;&#039; There isn&#039;t a standardized set of &amp;quot;numeric compliance&amp;quot; tests for ONNX runtimes beyond simple correctness. Perhaps in the future, test suites might include challenging numeric scenarios (large sums, nearly singular matrices) to ensure a runtime handles them gracefully (maybe by switching precision). Right now, it&#039;s mostly on the user to test their model under expected conditions.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Combining quantization with floating point:&#039;&#039;&#039; Many production models use a mix (e.g., int8 for some layers, FP16 for others). ONNX can represent this (with QuantizeLinear ops or MixedPrecision through casting), but tooling to automatically find the optimal mix is still developing. It&#039;s somewhat trial-and-error or using heuristics. An open question is how to automate precision assignment: given a model and a target hardware, how to automatically choose which layers can be int8, which should be FP16, etc., to meet an accuracy target. This crosses into NAS (neural architecture search) territory or automated quantization. &lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Numerical debugging tools:&#039;&#039;&#039; When a model goes wrong due to precision, it&#039;s often hard to pinpoint where. Better tooling to analyze numerical error layer by layer could be useful. For instance, one could run a model in double and float and diff the intermediate results to see where the largest divergence happens. Doing this systematically for big nets is an open tooling challenge. Some frameworks have hooks for this, but not standardized.&lt;br /&gt;
&lt;br /&gt;
In conclusion, while we&#039;ve come far with using lower precision in AI without sacrificing much accuracy, the landscape is always evolving. The key open problem is how to push precision lower (for efficiency gains) without crossing the threshold where accuracy falls off a cliff. Each new generation of hardware and techniques (like FP8, novel quantization schemes) extends that boundary, but also brings new questions on how to manage the numerical errors. ONNX as an interchange will need to keep pace, possibly by adding more expressive power for these numeric considerations.&lt;br /&gt;
&lt;br /&gt;
== See Also ==&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;IEEE 754 Floating-Point&#039;&#039;&#039; – The standard defining binary16, binary32, binary64 formats and rounding behavior.&lt;br /&gt;
* &#039;&#039;&#039;Floating-Point Arithmetic&#039;&#039;&#039; – General topic on how floating-point math works, common issues (overflow, underflow, cancellation).&lt;br /&gt;
* &#039;&#039;&#039;Kahan Summation Algorithm&#039;&#039;&#039; – Algorithm for compensated summation to reduce numerical error&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;.&lt;br /&gt;
* &#039;&#039;&#039;BFloat16&#039;&#039;&#039; – A short-format float with 8-bit exponent, used in TPUs and some GPUs for training networks.&lt;br /&gt;
* &#039;&#039;&#039;Mixed Precision Training&#039;&#039;&#039; – Technique of using FP16 (or BF16) along with FP32 accumulators to accelerate training&amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;.&lt;br /&gt;
* &#039;&#039;&#039;Quantization in Deep Learning&#039;&#039;&#039; – Converting networks to lower bit integer arithmetic for inference (INT8 quantization and calibration techniques).&lt;br /&gt;
* &#039;&#039;&#039;ONNX Operator Specification&#039;&#039;&#039; – Official docs for ONNX ops, which describe their mathematical function but not the required numeric precision (a point of potential extension in future).&lt;br /&gt;
* &#039;&#039;&#039;CUDA Tensor Cores &amp;amp; TF32&#039;&#039;&#039; – NVIDIA documentation on how tensor cores perform mixed precision and the TF32 format for accelerating FP32.&lt;br /&gt;
* &#039;&#039;&#039;Pairwise Summation&#039;&#039;&#039; – Summation algorithm with logN error growth&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;, used in many numerical libraries to improve accuracy of reductions.&lt;br /&gt;
* &#039;&#039;&#039;Softmax Stability&#039;&#039;&#039; – Articles on numerically stable softmax and log-sum-exp implementations to avoid overflow.&lt;br /&gt;
* &#039;&#039;&#039;Performance Tuning for Low Precision&#039;&#039;&#039; – Guides (e.g., NVIDIA or Intel whitepapers) on using low precision and the trade-offs involved in various hardware.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;references&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref1&amp;quot;&amp;gt;Accelerate and simplify Scikit-learn model inference with ONNX Runtime - Microsoft Open Source Blog — opensource.microsoft.com — https://opensource.microsoft.com/blog/2020/12/17/accelerate-simplify-scikit-learn-model-inference-onnx-runtime&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref2&amp;quot;&amp;gt;Floating Point Precision: Understanding FP64, FP32, and FP16 in Large Language Models - DEV Community — dev.to — https://dev.to/lukehinds/floating-point-precision-understanding-fp64-fp32-and-fp16-in-large-language-models-3gk6&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref3&amp;quot;&amp;gt;Floating Point Precision and Its Limitations | by Umair Akbar | Medium — akbu.medium.com — https://akbu.medium.com/floating-point-precision-and-its-limitations-cfb7247d7789&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref4&amp;quot;&amp;gt;Kahan summation algorithm - Wikipedia — en.wikipedia.org — https://en.wikipedia.org/wiki/Kahan_summation_algorithm&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref5&amp;quot;&amp;gt;Pairwise summation - Wikipedia — en.wikipedia.org — https://en.wikipedia.org/wiki/Pairwise_summation&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref6&amp;quot;&amp;gt;Numerical behavior of NVIDIA tensor cores - PMC — pmc.ncbi.nlm.nih.gov — https://pmc.ncbi.nlm.nih.gov/articles/PMC7959640/&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref7&amp;quot;&amp;gt;Mixed precision and performance-accuracy tuning (neuron-cc) — AWS Neuron Documentation — awsdocs-neuron.readthedocs-hosted.com — https://awsdocs-neuron.readthedocs-hosted.com/en/latest/about-neuron/appnotes/neuron-cc/mixed-precision.html&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref8&amp;quot;&amp;gt;Create Float16 and Mixed Precision Models - ONNX Runtime — onnxruntime.ai — https://onnxruntime.ai/docs/performance/model-optimizations/float16.html&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref9&amp;quot;&amp;gt;[Performance] ONNX FP16 model is having performance bottle neck when compared to FP32 variant · Issue #25824 · microsoft/onnxruntime · GitHub — github.com — https://github.com/microsoft/onnxruntime/issues/25824&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref10&amp;quot;&amp;gt;Float stored in 8 bits - ONNX 1.21.0 documentation — onnx.ai — https://onnx.ai/onnx/technical/float8.html&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref11&amp;quot;&amp;gt;[2209.05433] FP8 Formats for Deep Learning — arxiv.org — https://arxiv.org/abs/2209.05433&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref12&amp;quot;&amp;gt;Should MatMul op allow specification of accumulation precision? · Issue #7072 · onnx/onnx · GitHub — github.com — https://github.com/onnx/onnx/issues/7072&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref13&amp;quot;&amp;gt;Quantize ONNX models | onnxruntime — onnxruntime.ai — https://onnxruntime.ai/docs/performance/model-optimizations/quantization.html&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref14&amp;quot;&amp;gt;Keep Attention Softmax FP32 during FP16/ZeRO Training · Issue #1485 · hpcaitech/ColossalAI · GitHub — github.com — https://github.com/hpcaitech/ColossalAI/issues/1485&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;/references&amp;gt;&lt;/div&gt;</summary>
		<author><name>Timo.stripf</name></author>
	</entry>
	<entry>
		<id>https://www.emmtrix.com/w139/index.php?title=Numerical_Precision_in_ONNX_and_AI_Inference&amp;diff=2905</id>
		<title>Numerical Precision in ONNX and AI Inference</title>
		<link rel="alternate" type="text/html" href="https://www.emmtrix.com/w139/index.php?title=Numerical_Precision_in_ONNX_and_AI_Inference&amp;diff=2905"/>
		<updated>2026-02-01T22:58:12Z</updated>

		<summary type="html">&lt;p&gt;Timo.stripf: /* 5.2. MatMul, Conv, and Reduction Operators */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&#039;&#039;&#039;Open Neural Network Exchange (ONNX)&#039;&#039;&#039; is an open standard format for representing machine learning models and neural network computations across different frameworks and hardware&amp;lt;ref name=&amp;quot;ref1&amp;quot;/&amp;gt;. As models are exported and deployed via ONNX, the &#039;&#039;&#039;numerical precision&#039;&#039;&#039; of computations becomes critical. Deep learning inference involves a variety of floating-point operations, and small numerical discrepancies can accumulate and affect model accuracy or reproducibility. This article provides an in-depth overview of floating-point precision issues in numerical computing, with a focus on how they manifest in AI inference and the ONNX ecosystem. We cover floating-point formats and rounding behavior, accumulation strategies for summations, the use of low-precision formats (FP16, BFloat16, FP8) in modern AI, and how ONNX defines (or leaves unspecified) the precision of accumulators in operations. Strategies for maintaining numerical stability in deep neural networks are discussed, along with the trade-offs between computational performance and arithmetic accuracy. The goal is to give engineers and researchers a comprehensive, encyclopedic reference on numerical precision in ONNX-based AI inference systems.&lt;br /&gt;
&lt;br /&gt;
== Floating-Point Precision in Numerical Computing ==&lt;br /&gt;
&lt;br /&gt;
=== 2.1. IEEE 754 Floating-Point Formats ===&lt;br /&gt;
&lt;br /&gt;
Modern computers represent real numbers using the IEEE 754 floating-point standard, which defines formats like 32-bit &#039;&#039;single precision&#039;&#039; (FP32) and 64-bit &#039;&#039;double precision&#039;&#039; (FP64). In IEEE 754 binary formats, a floating-point number is composed of three fields: 1 sign bit, several exponent bits (with a bias), and several fraction (mantissa) bits. For example, FP32 consists of 1 sign bit, 8 exponent bits, and 23 fraction bits (24 bits of significand precision including the implicit leading 1)&amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;. This provides roughly 7 decimal digits of precision and an exponent range allowing values on the order of &amp;lt;math&amp;gt;10^{\pm38}&amp;lt;/math&amp;gt;. FP64 allocates 1 sign bit, 11 exponent bits, and 52 fraction bits (53-bit precision), yielding about 15–16 decimal digits of precision and a vastly larger range (~&amp;lt;math&amp;gt;10^{\pm308}&amp;lt;/math&amp;gt;)&amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;. These larger formats (especially FP64) are the &amp;quot;gold standard&amp;quot; for numerical accuracy in scientific computing, as they reduce round-off error in complex calculations&amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;. However, the increased precision comes at the cost of doubled storage and typically reduced computational throughput (e.g. many GPUs execute FP64 instructions at a much lower rate than FP32&amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
In addition to single and double precision, the IEEE 754-2008 standard introduced a 16-bit &#039;&#039;half precision&#039;&#039; format (FP16, or binary16). FP16 has 1 sign bit, 5 exponent bits, and 10 fraction bits (11-bit significand including the implicit 1)&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. This format covers a much smaller dynamic range (approximately &amp;lt;math&amp;gt;6.1\times10^{-5}&amp;lt;/math&amp;gt; to &amp;lt;math&amp;gt;6.5\times10^4&amp;lt;/math&amp;gt; for normalized values&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;) and offers only around 3 decimal digits of precision. Half precision was originally used in graphics, but it has gained popularity in machine learning for its speed and memory advantages (discussed later in Section 4). Another format relevant to AI is &#039;&#039;&#039;BFloat16&#039;&#039;&#039; (Brain Float 16), a 16-bit float with &#039;&#039;&#039;8 exponent bits and 7 fraction bits&#039;&#039;&#039;&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. BFloat16 sacrifices precision (only ~7 fraction bits ≈ 2–3 decimal digits) in order to have the &#039;&#039;&#039;same exponent range as FP32&#039;&#039;&#039;&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. This means BFloat16 can represent very large or very small numbers (∼&amp;lt;math&amp;gt;10^{\pm38}&amp;lt;/math&amp;gt;, similar range as FP32) but with much less precision between representable values&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. BFloat16 was introduced for deep learning by Google (for TPUs) to allow reduced precision &#039;&#039;training&#039;&#039; without frequent overflow/underflow, relying on the wide range and accepting the coarse precision.&lt;br /&gt;
&lt;br /&gt;
Each floating-point format has a &#039;&#039;&#039;unit roundoff&#039;&#039;&#039; or machine precision, denoted &amp;lt;math&amp;gt;\epsilon&amp;lt;/math&amp;gt;. This is effectively the spacing between 1.0 and the next representable number. For FP32, &amp;lt;math&amp;gt;\epsilon \approx 2^{-23} \approx 1.19\times10^{-7}&amp;lt;/math&amp;gt;, corresponding to about 7–8 decimal digits of precision&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. In FP64, &amp;lt;math&amp;gt;\epsilon = 2^{-52} \approx 2.22\times10^{-16}&amp;lt;/math&amp;gt; (15–16 decimal digits). Half precision is much less precise: FP16 has &amp;lt;math&amp;gt;\epsilon = 2^{-10} \approx 9.76\times10^{-4}&amp;lt;/math&amp;gt;&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;, and BFloat16 has &amp;lt;math&amp;gt;\epsilon = 2^{-7} = 7.8125\times10^{-3}&amp;lt;/math&amp;gt;&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. These values quantify how finely the continuum of real numbers is quantized in each format.&lt;br /&gt;
&lt;br /&gt;
=== 2.2. Machine Epsilon and Rounding Error ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Machine epsilon (&amp;lt;math&amp;gt;\varepsilon&amp;lt;/math&amp;gt;)&#039;&#039;&#039; is formally defined as the smallest positive number such that &amp;lt;math&amp;gt;1.0 + \varepsilon &amp;gt; 1.0&amp;lt;/math&amp;gt; in the floating-point format&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. It effectively measures the &#039;&#039;&#039;precision&#039;&#039;&#039; of the representation. In FP32, &amp;lt;math&amp;gt;\varepsilon = 2^{-23} \approx 1.19\times10^{-7}&amp;lt;/math&amp;gt;, meaning any result is rounded to about 7 decimal digits. FP16’s &amp;lt;math&amp;gt;\varepsilon = 2^{-10} \approx 9.77\times10^{-4}&amp;lt;/math&amp;gt;, over 1000× larger (so only ~3 decimal digits of precision), and BFloat16’s &amp;lt;math&amp;gt;\varepsilon = 2^{-7} \approx 7.8\times10^{-3}&amp;lt;/math&amp;gt;, reflecting its mere ~8-bit precision&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. Rounding in IEEE 754 by default uses &#039;&#039;&#039;round-to-nearest (ties to even)&#039;&#039;&#039;, which ensures the rounding error for any single operation is at most half a unit in the last place (0.5 ULP). We can model a floating-point addition as: &lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;$\text{fl}(a+b) = (a+b)\,(1 + \delta),&amp;lt;/math&amp;gt;$ &lt;br /&gt;
&lt;br /&gt;
where &amp;lt;math&amp;gt;|\delta| \le \varepsilon&amp;lt;/math&amp;gt; for that format&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. Here &amp;lt;math&amp;gt;\varepsilon/2&amp;lt;/math&amp;gt; is the maximum &#039;&#039;&#039;relative error&#039;&#039;&#039; in one rounding (also called unit roundoff). For example, when adding two FP32 numbers, the result is accurate to within about &amp;lt;math&amp;gt;10^{-7}&amp;lt;/math&amp;gt; of the true sum (relative), whereas in BF16, the result can deviate by up to &amp;lt;math&amp;gt;\sim7.8\times10^{-3}&amp;lt;/math&amp;gt; (nearly 0.78% relative error per operation)&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. Thus, &#039;&#039;&#039;each arithmetic operation incurs a small rounding error&#039;&#039;&#039; (on the order of machine epsilon) that can propagate or accumulate through a sequence of calculations.&lt;br /&gt;
&lt;br /&gt;
Because floating-point representations have finite exponent range, we must also consider &#039;&#039;&#039;overflow&#039;&#039;&#039; and &#039;&#039;&#039;underflow&#039;&#039;&#039;. If a result’s magnitude exceeds the maximum representable value, it overflows to infinity; if it falls below the minimum normal (or minimum subnormal) value, it underflows to zero (or a denormalized tiny value). FP32 and BF16 share an exponent size (8 bits), so their range for normal numbers is roughly &amp;lt;math&amp;gt;[10^{-38}, 10^{38}]&amp;lt;/math&amp;gt; (approx &amp;lt;math&amp;gt;~\!3.4\times10^{38}&amp;lt;/math&amp;gt; max)&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. FP16’s 5-bit exponent allows max &amp;lt;math&amp;gt;\sim 6.55\times10^4&amp;lt;/math&amp;gt; and min normal &amp;lt;math&amp;gt;\sim 6.10\times10^{-5}&amp;lt;/math&amp;gt;&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. In practice, &#039;&#039;&#039;underflow/overflow&#039;&#039;&#039; can occur in low-precision arithmetic if values are not appropriately scaled. For instance, summing a large number of moderately sized terms in FP16 could overflow where FP32 would not. As an example, summing on the order of &amp;lt;math&amp;gt;10^4&amp;lt;/math&amp;gt; values of magnitude ~&amp;lt;math&amp;gt;10^1&amp;lt;/math&amp;gt; (say, adding 10,000 terms around 10 each) would produce a total &amp;lt;math&amp;gt;\sim10^5&amp;lt;/math&amp;gt;, which exceeds FP16’s max finite value ~&amp;lt;math&amp;gt;6.5\times10^4&amp;lt;/math&amp;gt; and would overflow to &amp;lt;math&amp;gt;+\infty&amp;lt;/math&amp;gt; in half precision&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. Underflow is usually a concern when subtracting nearly equal numbers (catastrophic cancellation yielding a tiny difference that might flush to zero in low precision).&lt;br /&gt;
&lt;br /&gt;
=== 2.3. Accumulation Error in Reductions ===&lt;br /&gt;
&lt;br /&gt;
Floating-point addition is &#039;&#039;&#039;not associative&#039;&#039;&#039;: &amp;lt;math&amp;gt;(a+b)+c&amp;lt;/math&amp;gt; can differ from &amp;lt;math&amp;gt;a+(b+c)&amp;lt;/math&amp;gt; due to rounding, and thus the order in which we sum a list of numbers can change the result slightly. When summing a large sequence of &amp;lt;math&amp;gt;N&amp;lt;/math&amp;gt; numbers &amp;lt;math&amp;gt;x_1, x_2, ..., x_N&amp;lt;/math&amp;gt;, the &#039;&#039;&#039;accumulated rounding error&#039;&#039;&#039; can be significant. In the &#039;&#039;worst case&#039;&#039; (e.g., adding many positive and negative terms that cause maximal cancellation or always rounding one way), the error can grow on the order of &amp;lt;math&amp;gt;O(N \varepsilon)&amp;lt;/math&amp;gt; relative to the exact sum&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;. Simply summing &amp;lt;math&amp;gt;N&amp;lt;/math&amp;gt; numbers in sequence (the &#039;&#039;&#039;naive linear summation&#039;&#039;&#039;) has a worst-case error bound proportional to &amp;lt;math&amp;gt;N\,\varepsilon&amp;lt;/math&amp;gt;&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. This worst-case occurs for adversarial arrangements of values; in more typical scenarios with rounding errors of random signs, one can model the error as a random walk, yielding an expected root-mean-square (RMS) error growth on the order of &amp;lt;math&amp;gt;O(\sqrt{N}\,\varepsilon)&amp;lt;/math&amp;gt;&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;. Still, with very large &amp;lt;math&amp;gt;N&amp;lt;/math&amp;gt;, even &amp;lt;math&amp;gt;\sqrt{N}&amp;lt;/math&amp;gt; times &amp;lt;math&amp;gt;\varepsilon&amp;lt;/math&amp;gt; can become large enough to matter.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Cancellation&#039;&#039;&#039; is a particular problem: if we add two nearly equal numbers of opposite sign, most leading digits cancel and the result loses significant bits of precision. For example, in single precision, subtracting two numbers that agree in the first 6 decimal digits will yield a result with only ~1 digit of accuracy left. Catastrophic cancellation can dramatically increase relative error. Summation is especially sensitive to cancellation if large positive and negative terms are summed naively. The &#039;&#039;&#039;condition number&#039;&#039;&#039; of a summation (the ratio of the sum of absolute values &amp;lt;math&amp;gt;\sum|x_i|&amp;lt;/math&amp;gt; to the absolute value of the true sum &amp;lt;math&amp;gt;|\sum x_i|&amp;lt;/math&amp;gt;) indicates how sensitive the result is to perturbations&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;. If the numbers have mixed signs and nearly cancel out (making the condition number large), even an optimal summation order will have a large relative error. For instance, summing [1.0, &amp;lt;math&amp;gt;10^{100}&amp;lt;/math&amp;gt;, 1.0, &amp;lt;math&amp;gt;-10^{100}&amp;lt;/math&amp;gt;] in double precision has an exact sum of 2.0, but naive summation may yield 0.0 due to cancellation; an improved summation algorithm yields the correct 2.0&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
In practical terms, when summing a long list of floating-point numbers (such as accumulating a dot product or reducing a tensor), the &#039;&#039;&#039;accumulation error&#039;&#039;&#039; can become noticeable if no precautions are taken. Techniques to mitigate this error include carefully choosing the summation order or using higher precision for intermediate sums. We will explore these strategies in the next section.&lt;br /&gt;
&lt;br /&gt;
== Accumulators and Reduction Strategies ==&lt;br /&gt;
&lt;br /&gt;
When performing reductions (sums, dot products, etc.), the method of accumulation can greatly affect the accuracy of the result. Below, we discuss various strategies:&lt;br /&gt;
&lt;br /&gt;
=== 3.1. Naive Linear Accumulation ===&lt;br /&gt;
&lt;br /&gt;
The simplest accumulation strategy is to use a single accumulator and add each element sequentially. In C-like pseudocode:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;c&amp;quot;&amp;gt;&lt;br /&gt;
float sum = 0.0f;&lt;br /&gt;
for (int i = 0; i &amp;lt; N; ++i) {&lt;br /&gt;
    sum += array[i];&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This &#039;&#039;&#039;naive linear summation&#039;&#039;&#039; adds values one by one to the running total. While straightforward and fast, it is prone to the rounding error accumulation described earlier. The partial sum &amp;lt;code&amp;gt;sum&amp;lt;/code&amp;gt; continually grows (in absolute value) as terms are added, so new small terms may fall outside its precision. For example, if &amp;lt;code&amp;gt;sum&amp;lt;/code&amp;gt; is large and positive, adding a comparatively tiny positive number may produce no change because the small term&#039;s significant digits vanish in the presence of the large sum (the increment is below the rounding threshold of &amp;lt;code&amp;gt;sum&amp;lt;/code&amp;gt;). Conversely, adding a small number of opposite sign can suffer from cancellation. The order of addition matters: summing from smallest magnitude to largest (ascending order) tends to yield a more accurate result than summing in descending order, because adding small terms first preserves their contributions before the sum becomes very large. The naive loop above implicitly sums in the given index order; if the data is unsorted, this order is essentially arbitrary.&lt;br /&gt;
&lt;br /&gt;
The performance of linear accumulation is excellent (minimal overhead), and for many applications the resulting error is within acceptable limits. However, for &#039;&#039;&#039;very large N&#039;&#039;&#039; or ill-conditioned sums, the inaccuracy can become a problem. In a reduction involving millions of elements, a single-precision sum could lose several digits of accuracy. In deep learning, this could translate to small discrepancies in layer outputs or aggregated gradients if done naively in low precision.&lt;br /&gt;
&lt;br /&gt;
=== 3.2. Pairwise and Tree-Based Reductions ===&lt;br /&gt;
&lt;br /&gt;
A better approach to summation is &#039;&#039;&#039;pairwise summation&#039;&#039;&#039;, which sums numbers in a binary tree structure rather than linearly. The idea is to recursively split the array into halves, sum each half, then add the two partial sums. This can be implemented as:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;text&amp;quot;&amp;gt;&lt;br /&gt;
function pairwise_sum(x[1..n]):&lt;br /&gt;
    if n == 0:&lt;br /&gt;
        return 0&lt;br /&gt;
    if n == 1:&lt;br /&gt;
        return x[1]&lt;br /&gt;
    m = floor(n/2)&lt;br /&gt;
    left_sum  = pairwise_sum(x[1..m])&lt;br /&gt;
    right_sum = pairwise_sum(x[m+1..n])&lt;br /&gt;
    return left_sum + right_sum&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
By always adding numbers of similar magnitudes first (small subsets are reduced to subtotals, then those are added, etc.), pairwise summation avoids the situation of a very large running total absorbing small additions until the final steps. The error of pairwise summation grows logarithmically with &amp;lt;math&amp;gt;N&amp;lt;/math&amp;gt; in the worst case (roughly &amp;lt;math&amp;gt;O(\varepsilon \log_2 N)&amp;lt;/math&amp;gt;)&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;, instead of linearly. Intuitively, at each level of the tree, rounding error is constrained, and there are about &amp;lt;math&amp;gt;\log_2 N&amp;lt;/math&amp;gt; levels. In fact, the worst-case relative error bound for pairwise summation (base case of 1 element) is:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math display=&amp;quot;block&amp;quot;&amp;gt;|E_N| \le \frac{\varepsilon \log_2 N}{1 - \varepsilon \log_2 N} \sum_{i=1}^N |x_i|,&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
which for practical &amp;lt;math&amp;gt;N&amp;lt;/math&amp;gt; simplifies to on the order of &amp;lt;math&amp;gt;\varepsilon \log_2 N&amp;lt;/math&amp;gt; times the condition number of the sum&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;. In typical random-error scenarios, the error behaves even better (on the order of &amp;lt;math&amp;gt;\sqrt{\log N}&amp;lt;/math&amp;gt;)&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;. The key is that &#039;&#039;&#039;pairwise (tree) reduction balances the accumulations&#039;&#039;&#039;, preventing one sum from overwhelming the others too early.&lt;br /&gt;
&lt;br /&gt;
From a performance standpoint, pairwise summation has the same &amp;lt;math&amp;gt;O(N)&amp;lt;/math&amp;gt; total additions as linear summation, just a different order. It is also highly amenable to parallelization: independent partial sums can be computed in parallel at each level of the tree. Many numerical libraries and BLAS implementations use a form of blocked or pairwise summation in dot products for improved accuracy at negligible performance cost. For example, NumPy and Julia perform partial pairwise summation by default for better numerical behavior&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=== 3.3. Compensated Summation (Kahan, Neumaier) ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Compensated summation&#039;&#039;&#039; is an alternative technique to improve summation accuracy by tracking the small “lost” quantities. The most famous algorithm is the &#039;&#039;&#039;Kahan summation algorithm&#039;&#039;&#039; (also called Kahan–Babuška)&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. Kahan summation uses an extra variable to accumulate the round-off error. In pseudocode:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;c&amp;quot;&amp;gt;&lt;br /&gt;
double sum = 0.0;&lt;br /&gt;
double c = 0.0;    // compensation for lost low-order bits&lt;br /&gt;
for (i = 1; i &amp;lt;= N; ++i) {&lt;br /&gt;
    double y = x[i] - c;    // recover low-order bits by subtracting compensation&lt;br /&gt;
    double t = sum + y;     // perform the addition&lt;br /&gt;
    c = (t - sum) - y;      // compute new compensation (the error in t)&lt;br /&gt;
    sum = t;&lt;br /&gt;
}&lt;br /&gt;
return sum;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Here &amp;lt;code&amp;gt;c&amp;lt;/code&amp;gt; accumulates the tiny leftovers that would otherwise be dropped&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. Each iteration essentially subtracts the last error from the new addend (&amp;lt;code&amp;gt;y = x[i] - c&amp;lt;/code&amp;gt;) so that &amp;lt;code&amp;gt;t = sum + y&amp;lt;/code&amp;gt; includes the small bits from the previous operation. The new error is then stored in &amp;lt;code&amp;gt;c&amp;lt;/code&amp;gt; for the next iteration. The result is that the final &amp;lt;code&amp;gt;sum&amp;lt;/code&amp;gt; is as if computed with higher precision than the machine’s: Kahan’s algorithm can &#039;&#039;&#039;greatly reduce the error&#039;&#039;&#039; in summing a sequence of floats. In fact, with exact arithmetic for the compensation, Kahan summation yields a result accurate to within 1 or 2 ULP of the true sum, regardless of N (assuming the sequence of partial sums doesn’t overflow)&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. More precisely, Kahan summation achieves a &#039;&#039;&#039;worst-case error independent of N&#039;&#039;&#039; (error bound depends only on machine precision, not the number of terms)&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. In practice, using a double accumulator for summing single-precision numbers via Kahan can give near double-precision accuracy.&lt;br /&gt;
&lt;br /&gt;
An important subtlety is that the compensation variable &amp;lt;code&amp;gt;c&amp;lt;/code&amp;gt; should be kept in &#039;&#039;full precision&#039;&#039;. In the implementation above, we used &amp;lt;code&amp;gt;double&amp;lt;/code&amp;gt; for both sum and c. If summing FP32 values, one might use &amp;lt;code&amp;gt;float sum&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;float c&amp;lt;/code&amp;gt; (both 32-bit), which still improves accuracy, but using a higher precision for the accumulator and compensation (i.e. double) yields even better results. Many implementations of Kahan summation use the same type for sum and c, but it&#039;s assumed to be higher precision than the inputs when possible.&lt;br /&gt;
&lt;br /&gt;
Kahan’s algorithm carries a performance cost: each iteration does five arithmetic operations (two adds, one subtract, etc.) and has a data dependency, which can hinder parallelism or pipelining&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. Thus, it runs slower than naive summation (roughly 2–3× slower or more, depending on CPU pipeline). In loops where summation is a bottleneck, this may be significant. Nonetheless, for modest N or where maximum accuracy is required, compensated summation is extremely useful. In fact, Python’s &amp;lt;code&amp;gt;math.fsum&amp;lt;/code&amp;gt; function uses a variant of Kahan’s algorithm (actually a more advanced multi-part compensation) to sum iterables with high precision.&lt;br /&gt;
&lt;br /&gt;
There are variations and improvements on basic Kahan summation. &#039;&#039;&#039;Neumaier’s algorithm&#039;&#039;&#039; (sometimes called &#039;&#039;Kahan-Babuška-Neumaier&#039;&#039;) improves Kahan’s method by also handling cases where the next input is larger in magnitude than the running sum&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. In such cases, the original Kahan formula could suffer error; Neumaier’s version effectively swaps roles when a huge value comes in. Its pseudocode is:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;text&amp;quot;&amp;gt;&lt;br /&gt;
function NeumaierSum(array):&lt;br /&gt;
    sum = 0.0&lt;br /&gt;
    compensation = 0.0&lt;br /&gt;
    for each x in array:&lt;br /&gt;
        t = sum + x&lt;br /&gt;
        if (fabs(sum) &amp;gt;= fabs(x))&lt;br /&gt;
            compensation += (sum - t) + x;&lt;br /&gt;
        else&lt;br /&gt;
            compensation += (x - t) + sum;&lt;br /&gt;
        sum = t;&lt;br /&gt;
    return sum + compensation;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This algorithm ensures the compensation covers both scenarios: when the sum is the bigger addend or when the new term is bigger&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. In the end, it adds the final &amp;lt;code&amp;gt;compensation&amp;lt;/code&amp;gt; once to the result. Neumaier’s method can produce the correct result in cases where basic Kahan fails. For instance, summing [1.0, &amp;lt;math&amp;gt;+10^{100}&amp;lt;/math&amp;gt;, 1.0, &amp;lt;math&amp;gt;-10^{100}&amp;lt;/math&amp;gt;] in double precision: regular Kahan summation might give 0.0 (losing the small terms), whereas Neumaier’s algorithm yields 2.0 exactly&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. There are also higher-order compensation methods (carrying multiple compensation terms, e.g., the &#039;&#039;&#039;Klein summation&#039;&#039;&#039; variant&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt; or using arbitrary precision for intermediate sums) that further reduce error at increased computational cost&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
In summary, compensated summation algorithms trade extra computation for greatly improved numerical accuracy. They are especially valuable when summing large arrays of numbers with varying magnitudes, where naive summation could lose most of the low-order bits. Many scientific computing environments provide compensated sum routines for this reason. In deep learning, however, a different approach to improved precision is often taken: &#039;&#039;&#039;using a wider accumulator type&#039;&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
=== 3.4. Mixed-Precision Accumulation ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Mixed-precision accumulation&#039;&#039;&#039; refers to performing arithmetic in a higher precision than the input data. A common scenario is accumulating sums in 32-bit floats when the operands are 16-bit floats. Rather than implementing a compensation algorithm, one can simply use a wider accumulator so that intermediate rounding error is smaller. This is effectively what modern GPUs do for tensor operations: for example, NVIDIA &#039;&#039;&#039;Tensor Cores operate on FP16 input data with an FP32 accumulator&#039;&#039;&#039; (the 16-bit products are accumulated in 32-bit)&amp;lt;ref name=&amp;quot;ref6&amp;quot;/&amp;gt;. In this way, summing 2048 products in a dot product has far less precision loss than it would with a 16-bit accumulator. Hardware that supports mixed precision can give the best of both worlds: fast low-precision multiplies and high-precision accumulation.&lt;br /&gt;
&lt;br /&gt;
Software can also leverage mixed precision. For instance, one can sum an array of &amp;lt;code&amp;gt;float&amp;lt;/code&amp;gt; values into a &amp;lt;code&amp;gt;double&amp;lt;/code&amp;gt; variable, accumulating the result in double precision and then converting back to float at the end:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;c&amp;quot;&amp;gt;&lt;br /&gt;
double sum = 0.0;&lt;br /&gt;
for (int i=0; i&amp;lt;N; ++i) {&lt;br /&gt;
    sum += (double) arr_float[i];&lt;br /&gt;
}&lt;br /&gt;
float result = (float) sum;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This approach often yields a more accurate &amp;lt;code&amp;gt;result&amp;lt;/code&amp;gt; than summing in single precision directly, because the intermediate sum carries 52-bit precision. The final rounding to float happens only once at the end (minimizing overall rounding error). In fact, the above is one way to implement &amp;lt;code&amp;gt;math.fsum&amp;lt;/code&amp;gt; in Python (it internally does something similar with multiple partial sums in double precision).&lt;br /&gt;
&lt;br /&gt;
In machine learning training and inference, &#039;&#039;&#039;mixed precision&#039;&#039;&#039; is widely used. When using FP16 for speed, it&#039;s common to accumulate with FP32. For example, &#039;&#039;&#039;Google&#039;s TPUs use BFloat16 for multiplication but accumulate in FP32&#039;&#039;&#039; for matrix ops&amp;lt;ref name=&amp;quot;ref6&amp;quot;/&amp;gt;. AWS’s Inferentia (Neuron) hardware similarly supports FP16/BF16 matrix multiplication with FP32 accumulation&amp;lt;ref name=&amp;quot;ref7&amp;quot;/&amp;gt;. The motivation is that summing hundreds or thousands of half-precision values would otherwise introduce intolerable error or risk overflow. With an FP32 accumulator, the reduction error is drastically reduced (by about &amp;lt;math&amp;gt;2^{13}&amp;lt;/math&amp;gt; times, since FP32 has 13 more precision bits than FP16). Mixed-precision accumulation thus provides a simple and effective form of compensation: &#039;&#039;&#039;use a larger bucket to collect many small contributions&#039;&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
There is a spectrum of options: summing int8 or int16 integers might use 32-bit integers as accumulators for the same reason (to avoid overflow). Some BLAS libraries even offer &#039;&#039;extended precision accumulators&#039;&#039; for single precision dot products (accumulating in double). The downside of mixed precision is the extra memory and maybe throughput cost of the wider type. But on many platforms, the add latency is dominated by hardware design rather than bit-width, so using a 32-bit adder vs 16-bit has negligible speed impact if supported natively. The key is whether data movement or register pressure increases.&lt;br /&gt;
&lt;br /&gt;
In summary, &#039;&#039;&#039;using a higher precision accumulator is often the easiest way to improve numerical accuracy&#039;&#039;&#039; for reductions. It is a primary strategy in deep learning hardware to maintain stability while exploiting low precision for storage and initial computations.&lt;br /&gt;
&lt;br /&gt;
== Precision Formats in AI and Machine Learning ==&lt;br /&gt;
&lt;br /&gt;
Modern AI systems use a variety of numeric formats to balance speed, memory, and accuracy. We discuss common precisions and their roles:&lt;br /&gt;
&lt;br /&gt;
=== 4.1. FP32 and FP64 in Training and Inference ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;FP32 (single precision)&#039;&#039;&#039; has been the workhorse of deep learning for years. Most training scripts in frameworks like TensorFlow and PyTorch used FP32 by default, as it offers sufficient precision to propagate gradients and accumulate weight updates over millions of iterations without excessive error. With ~7 decimal digits of precision and a wide exponent range, FP32 can handle the dynamic range of activations and gradients in typical neural networks. In inference, FP32 is often more precision than strictly necessary for final predictions, but it has remained common because of its ease and the fact that many GPU operations were optimized for FP32.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;FP64 (double precision)&#039;&#039;&#039; is rarely used in deep learning model training or inference, except in specific scenarios requiring extreme precision. FP64 is common in scientific computing and certain types of numerical simulations. In neural networks, however, the extra precision is usually not needed for model accuracy—using doubles tends to yield negligible improvements in model metrics but at a large performance cost (since on GPUs FP64 throughput can be 1/2, 1/8, or even 1/32 of FP32 throughput depending on hardware). One exception might be when integrating neural nets into larger physics simulations or when solving ill-conditioned problems as part of the model. Some researchers use FP64 to diagnose numerical issues or ensure reproducibility. Generally though, deep learning has favored lower precision for efficiency, and FP64 is considered overkill for most networks&amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
That said, certain classical machine learning algorithms benefit from double precision. For example, computing the inverse of a matrix (as in a Gaussian Process regression or Kalman filter embedded in a model) can incur large rounding errors in single precision, so using FP64 can significantly improve the result&amp;lt;ref name=&amp;quot;ref1&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref1&amp;quot;/&amp;gt;. The ONNX standard supports a float64 tensor type for such needs. In ONNX Runtime, however, emphasis has been on FP32 performance, and some pipelines default to float even if the original model was double&amp;lt;ref name=&amp;quot;ref1&amp;quot;/&amp;gt;. If a model’s prediction computation involves operations that are numerically sensitive (like matrix inversion, Cholesky decomposition, or summing extremely large counts), then FP64 may be required to avoid noticeable discrepancies&amp;lt;ref name=&amp;quot;ref1&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref1&amp;quot;/&amp;gt;. A good practice is to test a model in both float and double if you suspect precision issues; significant differences indicate an ill-conditioned calculation that might need higher precision.&lt;br /&gt;
&lt;br /&gt;
In summary, FP32 remains the &#039;&#039;&#039;default&#039;&#039;&#039; format for most training and inference due to its balance of accuracy and efficiency&amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;, while FP64 is reserved for niche cases or precision-critical subroutines. FP32 typically achieves the same accuracy as FP64 for end-to-end deep learning tasks, because network training can often compensate for small noise, and other sources of error (like generalization error) dominate. Therefore, the community trend has been moving to &#039;&#039;&#039;lower&#039;&#039;&#039; precisions, not higher, to gain speed — provided the models still converge and perform well.&lt;br /&gt;
&lt;br /&gt;
=== 4.2. FP16 and BFloat16 in Deep Learning ===&lt;br /&gt;
&lt;br /&gt;
To further speed up neural network training and inference, &#039;&#039;&#039;half-precision (FP16)&#039;&#039;&#039; was adopted, especially with the advent of GPUs supporting FP16 math at twice the rate of FP32. Using FP16 reduces memory storage and bandwidth by 50% compared to FP32, which is significant for large models. However, FP16’s small exponent range and precision posed challenges: training a large network purely in FP16 can fail due to gradient underflow/overflow or weight updates not registering (because changes are below the 0.001 quantization step). To address this, frameworks introduced &#039;&#039;&#039;mixed-precision training&#039;&#039;&#039;, where compute-intensive parts (matrix multiplies, convolutions) run in FP16, but certain accumulations and model parameters remain in FP32. For instance, NVIDIA’s &#039;&#039;&#039;Tensor Core&#039;&#039;&#039; units require FP16 (or BF16) inputs but accumulate results in FP32, effectively ensuring that reductions are done in higher precision&amp;lt;ref name=&amp;quot;ref6&amp;quot;/&amp;gt;. Research showed that with proper loss scaling and mixed precision, models can train to FP32-level accuracy using mostly FP16 arithmetic, achieving significant speedups.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;BFloat16 (BF16)&#039;&#039;&#039; emerged as an alternative 16-bit format primarily from Google’s TPU training systems. BF16 has a larger exponent (8 bits, same as FP32) but only 7 fraction bits&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. This means BF16 can handle the &#039;&#039;&#039;same dynamic range&#039;&#039;&#039; as FP32 (no overflows where FP32 wouldn’t) but at the cost of even lower precision than FP16 (BF16 has about 2–3 decimal digits of precision vs FP16’s ~3–4). Despite its coarse precision, BF16 turns out to be effective for training deep networks because the algorithms (stochastic gradient descent, etc.) are somewhat robust to that noise, and having the wide range prevents catastrophic loss of signal (no gradient will overflow to Inf unless it was going to in FP32 anyway). Many training frameworks (PyTorch, TensorFlow) support BF16 training on hardware like &#039;&#039;&#039;TPU, NVIDIA A100+, Intel CPUs with AVX-512 BF16, etc&#039;&#039;&#039;. Typically, when using BF16, &#039;&#039;&#039;accumulations are still done in FP32&#039;&#039;&#039; (for example, a TPU v2/v3 multiplies BF16 matrices and accumulates into an FP32 result for the dot product&amp;lt;ref name=&amp;quot;ref6&amp;quot;/&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
In inference, both FP16 and BF16 are used to deploy models for faster speed or lower memory. FP16 is popular on NVIDIA GPUs (which have specialized FP16 units). BF16 is advantageous on TPUs and some CPUs that support it, and increasingly on GPUs (Ampere and newer GPUs also support BF16 arithmetic). The choice between FP16 and BF16 can depend on hardware: BF16’s advantage is easier software conversion (you can often take an FP32-trained model and cast weights to BF16 with minimal loss, since the dynamic range is intact), whereas FP16 might require adjusting the model or ensuring values fit in range (sometimes needing techniques like maintaining a master copy of weights in FP32 during training, or handling out-of-range values).&lt;br /&gt;
&lt;br /&gt;
For ONNX, both Float16 and BFloat16 types are part of the standard. ONNX models can be converted to FP16 (truncating weights and inserting cast nodes for computations) using tools like &amp;lt;code&amp;gt;float16_converter&amp;lt;/code&amp;gt; in ONNX Runtime&amp;lt;ref name=&amp;quot;ref8&amp;quot;/&amp;gt;. ONNX Runtime on GPUs will typically execute those FP16 ops with mixed precision on tensor cores (FP16 input, FP32 accumulate), whereas on CPU it may emulate FP16 or upcast to FP32 (as many CPU implementations do not natively support half-precision math)&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;. In fact, as an example, running an ONNX model in FP16 on a CPU might insert hidden Cast operations to FP32 behind the scenes, because the CPU kernel for that op only exists in FP32&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;. This ensures correctness (the computation is done in higher precision) but can reduce performance. We will revisit such backend-dependent behavior in Section 5.3.&lt;br /&gt;
&lt;br /&gt;
In summary, &#039;&#039;&#039;FP16 and BF16&#039;&#039;&#039; are now established as important low-precision formats in deep learning. FP16 offers more precision bits, which can matter for e.g. small differences, but has a limited range (care needed to avoid overflow). BF16 offers safety in range but less precision (which can slightly affect convergence or final accuracy in some cases). Both typically rely on FP32 for accumulation to achieve acceptable accuracy. When deploying models, one should choose a format supported efficiently by the target hardware and verify that the accuracy is not significantly degraded. Often, CNNs and transformers can run in FP16/BF16 with virtually no drop in score, but certain operations (like softmax, see Section 6.3) might need special handling.&lt;br /&gt;
&lt;br /&gt;
=== 4.3. FP8 Formats (E4M3, E5M2) and Scaling ===&lt;br /&gt;
&lt;br /&gt;
The push for ever lower precision has led to experimental &#039;&#039;&#039;8-bit floating point (FP8)&#039;&#039;&#039; formats for deep learning. In 2022, researchers from NVIDIA, Intel, and Arm proposed an FP8 standard with two variants: &#039;&#039;&#039;E4M3&#039;&#039;&#039; (4 exponent bits, 3 fraction bits) and &#039;&#039;&#039;E5M2&#039;&#039;&#039; (5 exponent bits, 2 fraction bits)&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;. Both have 1 sign bit, for a total of 8 bits. E5M2 has a wider exponent range (5 bits gives bias 15) but one fewer mantissa bit than E4M3. The idea is to use E4M3 for numbers that need more precision (like weights or activations around 1.0) and E5M2 for numbers that need more dynamic range (like gradient updates which can be very small or large). In fact, the study found that using E4M3 for forward activations/weights and E5M2 for backward gradients can allow training of large networks with FP8 to accuracy on par with FP16 training&amp;lt;ref name=&amp;quot;ref11&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
FP8 formats are &#039;&#039;not&#039;&#039; IEEE 754 standard (though IEEE is considering lower precision in its 2019 revision). Notably, the E4M3 format as used by NVIDIA does not allocate any bit-pattern for &amp;lt;math&amp;gt;+\infty&amp;lt;/math&amp;gt; or &amp;lt;math&amp;gt;-\infty&amp;lt;/math&amp;gt; – it has only NaN for invalid results, using that freed bit pattern to extend the representable range by one exponent value&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;. Essentially, E4M3 can represent numbers up to ~&amp;lt;math&amp;gt;2^{15}&amp;lt;/math&amp;gt; (around 3e4) instead of capping at infinity at &amp;lt;math&amp;gt;2^{16}&amp;lt;/math&amp;gt;, by foregoing infinities&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;. Variations also exist regarding negative zero and how NaNs are handled&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;. For instance, GraphCore’s IPU uses FP8 formats that disallow negative zero and infinities, to maximize useable range&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Using 8-bit floats for neural networks requires additional techniques, chiefly &#039;&#039;&#039;scaling&#039;&#039;&#039;. Because 8-bit has very limited precision and range, one typically applies a power-of-two scale factor to groups of values to normalize them into an FP8-friendly range. This is akin to block floating-point or “microscaling” per layer or per tensor. For example, one might keep a FP32 scale for each channel or tensor, and represent values as &amp;lt;math&amp;gt;(FP8\_value) \times 2^{scale}&amp;lt;/math&amp;gt;. The ONNX standard has introduced support for FP8 in version 1.15, including types for E4M3 and E5M2 (with variations FNUZ: finite-only and no negative zero)&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;, and even a type called &#039;&#039;&#039;E8M0&#039;&#039;&#039; which is an 8-bit exponent-only number used purely as a scale factor&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;. The E8M0 type has 8 exponent bits, no mantissa, effectively representing powers of two from &amp;lt;math&amp;gt;2^{-127}&amp;lt;/math&amp;gt; to &amp;lt;math&amp;gt;2^{128}&amp;lt;/math&amp;gt; for scaling purposes&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;. This indicates an approach where an FP8 tensor would be accompanied by an E8M0 scale (or scales) to interpret its value correctly.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Rounding&#039;&#039;&#039; becomes critical when casting values to FP8. The quantization error is much larger relative to FP16 or FP32. Studies have shown that choosing the right rounding mode (e.g., round-to-nearest, stochastic rounding, etc.) and clamping behavior (saturating to max instead of inf) can affect model accuracy&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;. ONNX added a &amp;lt;code&amp;gt;round_mode&amp;lt;/code&amp;gt; attribute to its Cast operator to allow controlling this for FP8&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;. In particular, one paper found that &#039;&#039;&#039;rounding up with saturation&#039;&#039;&#039; (round-to-nearest ties away from 0, and clamp overflows to max finite rather than inf) gave better accuracy in large language model training than other rounding modes&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
At present, FP8 is on the cutting edge. NVIDIA’s H100 GPU supports FP8 tensor cores (for both E4M3 and E5M2), and early results show minimal accuracy loss on some vision and NLP tasks when properly tuned. However, using FP8 requires fine-tuning hyperparameters like scaling factors for each layer and perhaps tweaking training procedures. ONNX’s inclusion of FP8 types means you can represent and transport models using FP8 weights or activations, but actual hardware support is limited and typically paired with calibration or dynamic scaling logic outside of ONNX graph (or using custom ONNX functions for scaling). It remains an &#039;&#039;&#039;open research area&#039;&#039;&#039; how to fully automate FP8 quantization for arbitrary models without accuracy loss.&lt;br /&gt;
&lt;br /&gt;
In summary, FP8 formats promise further compression of neural network computation and data. They are an order of magnitude more error-prone than FP16, but when combined with careful scaling, mixed usage of E4M3/E5M2, and maybe modified training techniques, they have shown surprising success. ONNX is actively evolving to support these ultra-low precision types, signaling their potential importance in future AI inference deployments.&lt;br /&gt;
&lt;br /&gt;
== Accumulation Semantics in ONNX ==&lt;br /&gt;
&lt;br /&gt;
One of the roles of the ONNX specification is to define the mathematical behavior of operators (Conv, MatMul, Sum, etc.) in a framework-agnostic way. However, ONNX mostly defines &#039;&#039;what&#039;&#039; an operator computes in exact arithmetic, not &#039;&#039;how&#039;&#039; the computation is carried out in finite precision. This leaves certain details – like accumulator precision or summation order – up to the implementation (runtime or hardware). We&#039;ll examine how ONNX treats accumulation and what guarantees (or lack thereof) are provided.&lt;br /&gt;
&lt;br /&gt;
=== 5.1. Operator Definitions and Numerical Guarantees ===&lt;br /&gt;
&lt;br /&gt;
Each ONNX operator is described as performing an ideal mathematical function on its inputs. For example, &amp;lt;code&amp;gt;MatMul&amp;lt;/code&amp;gt; is defined as the standard matrix product of two tensors, and &amp;lt;code&amp;gt;ReduceSum&amp;lt;/code&amp;gt; computes a summation along given axes. The ONNX spec, as of current versions, &#039;&#039;&#039;does not mandate the precision of intermediate calculations&#039;&#039;&#039; beyond the types of the inputs and outputs. If an operator’s inputs and outputs are float16, the spec implies the computation is done in float16 in a sense, but it does not forbid an implementation from using higher precision internally – it simply doesn&#039;t mention it. There is &#039;&#039;&#039;no explicit guarantee of identical bit-for-bit results&#039;&#039;&#039; across different ONNX backends for floating-point ops. Instead, results are expected to be numerically close, within tolerances one would normally expect from floating-point differences (due to different hardware or algorithms). The ONNX Backend Test suite typically uses a relative or absolute error tolerance when validating outputs for this reason.&lt;br /&gt;
&lt;br /&gt;
Because of this, important details like the use of extended precision or accumulation in higher precision are left ambiguous. This has led to discussions in the ONNX community about making such behavior more explicit. For example, a proposal was raised to allow specification of accumulation precision for certain ops like MatMul&amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;. The reasoning is that as lower-precision types (float16, bfloat16, float8) become more common, knowing or controlling whether an op internally accumulates in a higher precision is important for both accuracy and reproducibility&amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;. In June 2025, an issue on the ONNX GitHub suggested adding an attribute to MatMul (and similarly Gemm, Softmax, etc.) to specify the accumulator type&amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;. By default it would use input precision (status quo), but an attribute could request (for instance) 32-bit accumulation even if inputs are 16-bit&amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;. As of this writing, however, no such attribute exists in the official spec – it&#039;s an open issue. Thus, the numerical semantics are effectively: &#039;&#039;“compute the result as if in the same type as inputs, but actual implementations may use more precision internally as an optimization.”&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
From a guarantee standpoint, ONNX operators promise the mathematically correct result assuming real-number arithmetic, but they &#039;&#039;&#039;do not guarantee bit-exact results&#039;&#039;&#039; due to floating-point effects. There is an understanding that differences can arise from different summation orders, fused operations (e.g., an implementation might use an FMA (fused multiply-add) which has one rounding instead of two), or use of extended precision registers. ONNX does not formally define rounding modes either – it relies on IEEE 754 defaults, but if a backend used something like deterministic summation or alternative rounding, ONNX has no mechanism to express that.&lt;br /&gt;
&lt;br /&gt;
In summary, ONNX&#039;s current stance is largely &#039;&#039;&#039;implementation-dependent&#039;&#039;&#039; for numerical precision. It provides minimal explicit numerical guarantees beyond type constraints. This is why an ONNX model run on two different hardware or runtime libraries can have slightly different results, especially in reduced precision. The burden is on backends to produce results &amp;quot;close enough&amp;quot; to each other that they don&#039;t affect the model&#039;s decisions in any significant way.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Open Neural Network Exchange (ONNX)&#039;&#039;&#039; is an open standard format for representing machine learning models and neural network computations across different frameworks and hardware&amp;lt;ref name=&amp;quot;ref1&amp;quot;/&amp;gt;. As models are exported and deployed via ONNX, the &#039;&#039;&#039;numerical precision&#039;&#039;&#039; of computations becomes critical. Deep learning inference involves a variety of floating-point operations, and small numerical discrepancies can accumulate and affect model accuracy or reproducibility. This article provides an in-depth overview of floating-point precision issues in numerical computing, with a focus on how they manifest in AI inference and the ONNX ecosystem. We cover floating-point formats and rounding behavior, accumulation strategies for summations, the use of low-precision formats (FP16, BFloat16, FP8) in modern AI, and how ONNX defines (or leaves unspecified) the precision of accumulators in operations. Strategies for maintaining numerical stability in deep neural networks are discussed, along with the trade-offs between computational performance and arithmetic accuracy. The goal is to give engineers and researchers a comprehensive, encyclopedic reference on numerical precision in ONNX-based AI inference systems.&lt;br /&gt;
&lt;br /&gt;
== Floating-Point Precision in Numerical Computing ==&lt;br /&gt;
&lt;br /&gt;
=== 2.1. IEEE 754 Floating-Point Formats ===&lt;br /&gt;
&lt;br /&gt;
Modern computers represent real numbers using the IEEE 754 floating-point standard, which defines formats like 32-bit &#039;&#039;single precision&#039;&#039; (FP32) and 64-bit &#039;&#039;double precision&#039;&#039; (FP64). In IEEE 754 binary formats, a floating-point number is composed of three fields: 1 sign bit, several exponent bits (with a bias), and several fraction (mantissa) bits. For example, FP32 consists of 1 sign bit, 8 exponent bits, and 23 fraction bits (24 bits of significand precision including the implicit leading 1)&amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;. This provides roughly 7 decimal digits of precision and an exponent range allowing values on the order of &#039;&#039;10&amp;lt;sup&amp;gt;±38&amp;lt;/sup&amp;gt;&#039;&#039;. FP64 allocates 1 sign bit, 11 exponent bits, and 52 fraction bits (53-bit precision), yielding about 15–16 decimal digits of precision and a vastly larger range (~&#039;&#039;10&amp;lt;sup&amp;gt;±308&amp;lt;/sup&amp;gt;&#039;&#039;)&amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;. These larger formats (especially FP64) are the &amp;quot;gold standard&amp;quot; for numerical accuracy in scientific computing, as they reduce round-off error in complex calculations&amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;. However, the increased precision comes at the cost of doubled storage and typically reduced computational throughput (e.g. many GPUs execute FP64 instructions at a much lower rate than FP32&amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
In addition to single and double precision, the IEEE 754-2008 standard introduced a 16-bit &#039;&#039;half precision&#039;&#039; format (FP16, or binary16). FP16 has 1 sign bit, 5 exponent bits, and 10 fraction bits (11-bit significand including the implicit 1)&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. This format covers a much smaller dynamic range (approximately &#039;&#039;6.1 × 10&amp;lt;sup&amp;gt;-5&amp;lt;/sup&amp;gt;&#039;&#039; to &#039;&#039;6.5 × 10&amp;lt;sup&amp;gt;4&amp;lt;/sup&amp;gt;&#039;&#039; for normalized values&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;) and offers only around 3 decimal digits of precision. Half precision was originally used in graphics, but it has gained popularity in machine learning for its speed and memory advantages (discussed later in Section 4). Another format relevant to AI is &#039;&#039;&#039;BFloat16&#039;&#039;&#039; (Brain Float 16), a 16-bit float with &#039;&#039;&#039;8 exponent bits and 7 fraction bits&#039;&#039;&#039;&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. BFloat16 sacrifices precision (only ~7 fraction bits ≈ 2–3 decimal digits) in order to have the &#039;&#039;&#039;same exponent range as FP32&#039;&#039;&#039;&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. This means BFloat16 can represent very large or very small numbers (∼&#039;&#039;10&amp;lt;sup&amp;gt;±38&amp;lt;/sup&amp;gt;&#039;&#039;, similar range as FP32) but with much less precision between representable values&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. BFloat16 was introduced for deep learning by Google (for TPUs) to allow reduced precision &#039;&#039;training&#039;&#039; without frequent overflow/underflow, relying on the wide range and accepting the coarse precision.&lt;br /&gt;
&lt;br /&gt;
Each floating-point format has a &#039;&#039;&#039;unit roundoff&#039;&#039;&#039; or machine precision, denoted &#039;&#039;ε&#039;&#039;. This is effectively the spacing between 1.0 and the next representable number. For FP32, &#039;&#039;ε ≈ 2&amp;lt;sup&amp;gt;-23&amp;lt;/sup&amp;gt; ≈ 1.19 × 10&amp;lt;sup&amp;gt;-7&amp;lt;/sup&amp;gt;&#039;&#039;, corresponding to about 7–8 decimal digits of precision&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. In FP64, &#039;&#039;ε = 2&amp;lt;sup&amp;gt;-52&amp;lt;/sup&amp;gt; ≈ 2.22 × 10&amp;lt;sup&amp;gt;-16&amp;lt;/sup&amp;gt;&#039;&#039; (15–16 decimal digits). Half precision is much less precise: FP16 has &#039;&#039;ε = 2&amp;lt;sup&amp;gt;-10&amp;lt;/sup&amp;gt; ≈ 9.76 × 10&amp;lt;sup&amp;gt;-4&amp;lt;/sup&amp;gt;&#039;&#039;&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;, and BFloat16 has &#039;&#039;ε = 2&amp;lt;sup&amp;gt;-7&amp;lt;/sup&amp;gt; = 7.8125 × 10&amp;lt;sup&amp;gt;-3&amp;lt;/sup&amp;gt;&#039;&#039;&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. These values quantify how finely the continuum of real numbers is quantized in each format.&lt;br /&gt;
&lt;br /&gt;
=== 2.2. Machine Epsilon and Rounding Error ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Machine epsilon (&#039;&#039;ε&#039;&#039;)&#039;&#039;&#039; is formally defined as the smallest positive number such that &#039;&#039;1.0 + ε &amp;gt; 1.0&#039;&#039; in the floating-point format&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. It effectively measures the &#039;&#039;&#039;precision&#039;&#039;&#039; of the representation. In FP32, &#039;&#039;ε = 2&amp;lt;sup&amp;gt;-23&amp;lt;/sup&amp;gt; ≈ 1.19 × 10&amp;lt;sup&amp;gt;-7&amp;lt;/sup&amp;gt;&#039;&#039;, meaning any result is rounded to about 7 decimal digits. FP16’s &#039;&#039;ε = 2&amp;lt;sup&amp;gt;-10&amp;lt;/sup&amp;gt; ≈ 9.77 × 10&amp;lt;sup&amp;gt;-4&amp;lt;/sup&amp;gt;&#039;&#039;, over 1000× larger (so only ~3 decimal digits of precision), and BFloat16’s &#039;&#039;ε = 2&amp;lt;sup&amp;gt;-7&amp;lt;/sup&amp;gt; ≈ 7.8 × 10&amp;lt;sup&amp;gt;-3&amp;lt;/sup&amp;gt;&#039;&#039;, reflecting its mere ~8-bit precision&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. Rounding in IEEE 754 by default uses &#039;&#039;&#039;round-to-nearest (ties to even)&#039;&#039;&#039;, which ensures the rounding error for any single operation is at most half a unit in the last place (0.5 ULP). We can model a floating-point addition as: &lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;\text{fl}(a+b) = (a+b)\,(1 + \delta),&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
where &#039;&#039;|δ| ≤ ε&#039;&#039; for that format&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. Here &#039;&#039;ε/2&#039;&#039; is the maximum &#039;&#039;&#039;relative error&#039;&#039;&#039; in one rounding (also called unit roundoff). For example, when adding two FP32 numbers, the result is accurate to within about &#039;&#039;10&amp;lt;sup&amp;gt;-7&amp;lt;/sup&amp;gt;&#039;&#039; of the true sum (relative), whereas in BF16, the result can deviate by up to &#039;&#039;∼7.8 × 10&amp;lt;sup&amp;gt;-3&amp;lt;/sup&amp;gt;&#039;&#039; (nearly 0.78% relative error per operation)&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. Thus, &#039;&#039;&#039;each arithmetic operation incurs a small rounding error&#039;&#039;&#039; (on the order of machine epsilon) that can propagate or accumulate through a sequence of calculations.&lt;br /&gt;
&lt;br /&gt;
Because floating-point representations have finite exponent range, we must also consider &#039;&#039;&#039;overflow&#039;&#039;&#039; and &#039;&#039;&#039;underflow&#039;&#039;&#039;. If a result’s magnitude exceeds the maximum representable value, it overflows to infinity; if it falls below the minimum normal (or minimum subnormal) value, it underflows to zero (or a denormalized tiny value). FP32 and BF16 share an exponent size (8 bits), so their range for normal numbers is roughly &#039;&#039;[10&amp;lt;sup&amp;gt;-38&amp;lt;/sup&amp;gt;, 10&amp;lt;sup&amp;gt;38&amp;lt;/sup&amp;gt;]&#039;&#039; (approx &amp;lt;math&amp;gt;~\!3.4\times10^{38}&amp;lt;/math&amp;gt; max)&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. FP16’s 5-bit exponent allows max &#039;&#039;∼ 6.55 × 10&amp;lt;sup&amp;gt;4&amp;lt;/sup&amp;gt;&#039;&#039; and min normal &#039;&#039;∼ 6.10 × 10&amp;lt;sup&amp;gt;-5&amp;lt;/sup&amp;gt;&#039;&#039;&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. In practice, &#039;&#039;&#039;underflow/overflow&#039;&#039;&#039; can occur in low-precision arithmetic if values are not appropriately scaled. For instance, summing a large number of moderately sized terms in FP16 could overflow where FP32 would not. As an example, summing on the order of &#039;&#039;10&amp;lt;sup&amp;gt;4&amp;lt;/sup&amp;gt;&#039;&#039; values of magnitude ~&#039;&#039;10&amp;lt;sup&amp;gt;1&amp;lt;/sup&amp;gt;&#039;&#039; (say, adding 10,000 terms around 10 each) would produce a total &#039;&#039;∼10&amp;lt;sup&amp;gt;5&amp;lt;/sup&amp;gt;&#039;&#039;, which exceeds FP16’s max finite value ~&#039;&#039;6.5 × 10&amp;lt;sup&amp;gt;4&amp;lt;/sup&amp;gt;&#039;&#039; and would overflow to &#039;&#039;+∞&#039;&#039; in half precision&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. Underflow is usually a concern when subtracting nearly equal numbers (catastrophic cancellation yielding a tiny difference that might flush to zero in low precision).&lt;br /&gt;
&lt;br /&gt;
=== 2.3. Accumulation Error in Reductions ===&lt;br /&gt;
&lt;br /&gt;
Floating-point addition is &#039;&#039;&#039;not associative&#039;&#039;&#039;: &#039;&#039;(a+b)+c&#039;&#039; can differ from &#039;&#039;a+(b+c)&#039;&#039; due to rounding, and thus the order in which we sum a list of numbers can change the result slightly. When summing a large sequence of &#039;&#039;N&#039;&#039; numbers &#039;&#039;x&amp;lt;sub&amp;gt;1&amp;lt;/sub&amp;gt;, x&amp;lt;sub&amp;gt;2&amp;lt;/sub&amp;gt;, ..., x&amp;lt;sub&amp;gt;N&amp;lt;/sub&amp;gt;&#039;&#039;, the &#039;&#039;&#039;accumulated rounding error&#039;&#039;&#039; can be significant. In the &#039;&#039;worst case&#039;&#039; (e.g., adding many positive and negative terms that cause maximal cancellation or always rounding one way), the error can grow on the order of &#039;&#039;O(N ε)&#039;&#039; relative to the exact sum&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;. Simply summing &#039;&#039;N&#039;&#039; numbers in sequence (the &#039;&#039;&#039;naive linear summation&#039;&#039;&#039;) has a worst-case error bound proportional to &amp;lt;math&amp;gt;N\,\varepsilon&amp;lt;/math&amp;gt;&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. This worst-case occurs for adversarial arrangements of values; in more typical scenarios with rounding errors of random signs, one can model the error as a random walk, yielding an expected root-mean-square (RMS) error growth on the order of &amp;lt;math&amp;gt;O(\sqrt{N}\,\varepsilon)&amp;lt;/math&amp;gt;&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;. Still, with very large &#039;&#039;N&#039;&#039;, even &amp;lt;math&amp;gt;\sqrt{N}&amp;lt;/math&amp;gt; times &#039;&#039;ε&#039;&#039; can become large enough to matter.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Cancellation&#039;&#039;&#039; is a particular problem: if we add two nearly equal numbers of opposite sign, most leading digits cancel and the result loses significant bits of precision. For example, in single precision, subtracting two numbers that agree in the first 6 decimal digits will yield a result with only ~1 digit of accuracy left. Catastrophic cancellation can dramatically increase relative error. Summation is especially sensitive to cancellation if large positive and negative terms are summed naively. The &#039;&#039;&#039;condition number&#039;&#039;&#039; of a summation (the ratio of the sum of absolute values &amp;lt;math&amp;gt;\sum|x_i|&amp;lt;/math&amp;gt; to the absolute value of the true sum &amp;lt;math&amp;gt;|\sum x_i|&amp;lt;/math&amp;gt;) indicates how sensitive the result is to perturbations&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;. If the numbers have mixed signs and nearly cancel out (making the condition number large), even an optimal summation order will have a large relative error. For instance, summing [1.0, &#039;&#039;10&amp;lt;sup&amp;gt;100&amp;lt;/sup&amp;gt;&#039;&#039;, 1.0, &#039;&#039;-10&amp;lt;sup&amp;gt;100&amp;lt;/sup&amp;gt;&#039;&#039;] in double precision has an exact sum of 2.0, but naive summation may yield 0.0 due to cancellation; an improved summation algorithm yields the correct 2.0&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
In practical terms, when summing a long list of floating-point numbers (such as accumulating a dot product or reducing a tensor), the &#039;&#039;&#039;accumulation error&#039;&#039;&#039; can become noticeable if no precautions are taken. Techniques to mitigate this error include carefully choosing the summation order or using higher precision for intermediate sums. We will explore these strategies in the next section.&lt;br /&gt;
&lt;br /&gt;
== Accumulators and Reduction Strategies ==&lt;br /&gt;
&lt;br /&gt;
When performing reductions (sums, dot products, etc.), the method of accumulation can greatly affect the accuracy of the result. Below, we discuss various strategies:&lt;br /&gt;
&lt;br /&gt;
=== 3.1. Naive Linear Accumulation ===&lt;br /&gt;
&lt;br /&gt;
The simplest accumulation strategy is to use a single accumulator and add each element sequentially. In C-like pseudocode:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;c&amp;quot;&amp;gt;&lt;br /&gt;
float sum = 0.0f;&lt;br /&gt;
for (int i = 0; i &amp;lt; N; ++i) {&lt;br /&gt;
    sum += array[i];&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This &#039;&#039;&#039;naive linear summation&#039;&#039;&#039; adds values one by one to the running total. While straightforward and fast, it is prone to the rounding error accumulation described earlier. The partial sum &amp;lt;code&amp;gt;sum&amp;lt;/code&amp;gt; continually grows (in absolute value) as terms are added, so new small terms may fall outside its precision. For example, if &amp;lt;code&amp;gt;sum&amp;lt;/code&amp;gt; is large and positive, adding a comparatively tiny positive number may produce no change because the small term&#039;s significant digits vanish in the presence of the large sum (the increment is below the rounding threshold of &amp;lt;code&amp;gt;sum&amp;lt;/code&amp;gt;). Conversely, adding a small number of opposite sign can suffer from cancellation. The order of addition matters: summing from smallest magnitude to largest (ascending order) tends to yield a more accurate result than summing in descending order, because adding small terms first preserves their contributions before the sum becomes very large. The naive loop above implicitly sums in the given index order; if the data is unsorted, this order is essentially arbitrary.&lt;br /&gt;
&lt;br /&gt;
The performance of linear accumulation is excellent (minimal overhead), and for many applications the resulting error is within acceptable limits. However, for &#039;&#039;&#039;very large N&#039;&#039;&#039; or ill-conditioned sums, the inaccuracy can become a problem. In a reduction involving millions of elements, a single-precision sum could lose several digits of accuracy. In deep learning, this could translate to small discrepancies in layer outputs or aggregated gradients if done naively in low precision.&lt;br /&gt;
&lt;br /&gt;
=== 3.2. Pairwise and Tree-Based Reductions ===&lt;br /&gt;
&lt;br /&gt;
A better approach to summation is &#039;&#039;&#039;pairwise summation&#039;&#039;&#039;, which sums numbers in a binary tree structure rather than linearly. The idea is to recursively split the array into halves, sum each half, then add the two partial sums. This can be implemented as:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;text&amp;quot;&amp;gt;&lt;br /&gt;
function pairwise_sum(x[1..n]):&lt;br /&gt;
    if n == 0:&lt;br /&gt;
        return 0&lt;br /&gt;
    if n == 1:&lt;br /&gt;
        return x[1]&lt;br /&gt;
    m = floor(n/2)&lt;br /&gt;
    left_sum  = pairwise_sum(x[1..m])&lt;br /&gt;
    right_sum = pairwise_sum(x[m+1..n])&lt;br /&gt;
    return left_sum + right_sum&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
By always adding numbers of similar magnitudes first (small subsets are reduced to subtotals, then those are added, etc.), pairwise summation avoids the situation of a very large running total absorbing small additions until the final steps. The error of pairwise summation grows logarithmically with &#039;&#039;N&#039;&#039; in the worst case (roughly &#039;&#039;O(ε log&amp;lt;sub&amp;gt;2&amp;lt;/sub&amp;gt; N)&#039;&#039;)&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;, instead of linearly. Intuitively, at each level of the tree, rounding error is constrained, and there are about &#039;&#039;log&amp;lt;sub&amp;gt;2&amp;lt;/sub&amp;gt; N&#039;&#039; levels. In fact, the worst-case relative error bound for pairwise summation (base case of 1 element) is:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;|E_N| \le \frac{\varepsilon \log_2 N}{1 - \varepsilon \log_2 N} \sum_{i=1}^N |x_i|,&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
which for practical &#039;&#039;N&#039;&#039; simplifies to on the order of &#039;&#039;ε log&amp;lt;sub&amp;gt;2&amp;lt;/sub&amp;gt; N&#039;&#039; times the condition number of the sum&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;. In typical random-error scenarios, the error behaves even better (on the order of &amp;lt;math&amp;gt;\sqrt{\log N}&amp;lt;/math&amp;gt;)&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;. The key is that &#039;&#039;&#039;pairwise (tree) reduction balances the accumulations&#039;&#039;&#039;, preventing one sum from overwhelming the others too early.&lt;br /&gt;
&lt;br /&gt;
From a performance standpoint, pairwise summation has the same &#039;&#039;O(N)&#039;&#039; total additions as linear summation, just a different order. It is also highly amenable to parallelization: independent partial sums can be computed in parallel at each level of the tree. Many numerical libraries and BLAS implementations use a form of blocked or pairwise summation in dot products for improved accuracy at negligible performance cost. For example, NumPy and Julia perform partial pairwise summation by default for better numerical behavior&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=== 3.3. Compensated Summation (Kahan, Neumaier) ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Compensated summation&#039;&#039;&#039; is an alternative technique to improve summation accuracy by tracking the small “lost” quantities. The most famous algorithm is the &#039;&#039;&#039;Kahan summation algorithm&#039;&#039;&#039; (also called Kahan–Babuška)&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. Kahan summation uses an extra variable to accumulate the round-off error. In pseudocode:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;c&amp;quot;&amp;gt;&lt;br /&gt;
double sum = 0.0;&lt;br /&gt;
double c = 0.0;    // compensation for lost low-order bits&lt;br /&gt;
for (i = 1; i &amp;lt;= N; ++i) {&lt;br /&gt;
    double y = x[i] - c;    // recover low-order bits by subtracting compensation&lt;br /&gt;
    double t = sum + y;     // perform the addition&lt;br /&gt;
    c = (t - sum) - y;      // compute new compensation (the error in t)&lt;br /&gt;
    sum = t;&lt;br /&gt;
}&lt;br /&gt;
return sum;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Here &amp;lt;code&amp;gt;c&amp;lt;/code&amp;gt; accumulates the tiny leftovers that would otherwise be dropped&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. Each iteration essentially subtracts the last error from the new addend (&amp;lt;code&amp;gt;y = x[i] - c&amp;lt;/code&amp;gt;) so that &amp;lt;code&amp;gt;t = sum + y&amp;lt;/code&amp;gt; includes the small bits from the previous operation. The new error is then stored in &amp;lt;code&amp;gt;c&amp;lt;/code&amp;gt; for the next iteration. The result is that the final &amp;lt;code&amp;gt;sum&amp;lt;/code&amp;gt; is as if computed with higher precision than the machine’s: Kahan’s algorithm can &#039;&#039;&#039;greatly reduce the error&#039;&#039;&#039; in summing a sequence of floats. In fact, with exact arithmetic for the compensation, Kahan summation yields a result accurate to within 1 or 2 ULP of the true sum, regardless of N (assuming the sequence of partial sums doesn’t overflow)&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. More precisely, Kahan summation achieves a &#039;&#039;&#039;worst-case error independent of N&#039;&#039;&#039; (error bound depends only on machine precision, not the number of terms)&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. In practice, using a double accumulator for summing single-precision numbers via Kahan can give near double-precision accuracy.&lt;br /&gt;
&lt;br /&gt;
An important subtlety is that the compensation variable &amp;lt;code&amp;gt;c&amp;lt;/code&amp;gt; should be kept in &#039;&#039;full precision&#039;&#039;. In the implementation above, we used &amp;lt;code&amp;gt;double&amp;lt;/code&amp;gt; for both sum and c. If summing FP32 values, one might use &amp;lt;code&amp;gt;float sum&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;float c&amp;lt;/code&amp;gt; (both 32-bit), which still improves accuracy, but using a higher precision for the accumulator and compensation (i.e. double) yields even better results. Many implementations of Kahan summation use the same type for sum and c, but it&#039;s assumed to be higher precision than the inputs when possible.&lt;br /&gt;
&lt;br /&gt;
Kahan’s algorithm carries a performance cost: each iteration does five arithmetic operations (two adds, one subtract, etc.) and has a data dependency, which can hinder parallelism or pipelining&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. Thus, it runs slower than naive summation (roughly 2–3× slower or more, depending on CPU pipeline). In loops where summation is a bottleneck, this may be significant. Nonetheless, for modest N or where maximum accuracy is required, compensated summation is extremely useful. In fact, Python’s &amp;lt;code&amp;gt;math.fsum&amp;lt;/code&amp;gt; function uses a variant of Kahan’s algorithm (actually a more advanced multi-part compensation) to sum iterables with high precision.&lt;br /&gt;
&lt;br /&gt;
There are variations and improvements on basic Kahan summation. &#039;&#039;&#039;Neumaier’s algorithm&#039;&#039;&#039; (sometimes called &#039;&#039;Kahan-Babuška-Neumaier&#039;&#039;) improves Kahan’s method by also handling cases where the next input is larger in magnitude than the running sum&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. In such cases, the original Kahan formula could suffer error; Neumaier’s version effectively swaps roles when a huge value comes in. Its pseudocode is:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;text&amp;quot;&amp;gt;&lt;br /&gt;
function NeumaierSum(array):&lt;br /&gt;
    sum = 0.0&lt;br /&gt;
    compensation = 0.0&lt;br /&gt;
    for each x in array:&lt;br /&gt;
        t = sum + x&lt;br /&gt;
        if (fabs(sum) &amp;gt;= fabs(x))&lt;br /&gt;
            compensation += (sum - t) + x;&lt;br /&gt;
        else&lt;br /&gt;
            compensation += (x - t) + sum;&lt;br /&gt;
        sum = t;&lt;br /&gt;
    return sum + compensation;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This algorithm ensures the compensation covers both scenarios: when the sum is the bigger addend or when the new term is bigger&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. In the end, it adds the final &amp;lt;code&amp;gt;compensation&amp;lt;/code&amp;gt; once to the result. Neumaier’s method can produce the correct result in cases where basic Kahan fails. For instance, summing [1.0, &#039;&#039;+10&amp;lt;sup&amp;gt;100&amp;lt;/sup&amp;gt;&#039;&#039;, 1.0, &#039;&#039;-10&amp;lt;sup&amp;gt;100&amp;lt;/sup&amp;gt;&#039;&#039;] in double precision: regular Kahan summation might give 0.0 (losing the small terms), whereas Neumaier’s algorithm yields 2.0 exactly&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. There are also higher-order compensation methods (carrying multiple compensation terms, e.g., the &#039;&#039;&#039;Klein summation&#039;&#039;&#039; variant&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt; or using arbitrary precision for intermediate sums) that further reduce error at increased computational cost&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
In summary, compensated summation algorithms trade extra computation for greatly improved numerical accuracy. They are especially valuable when summing large arrays of numbers with varying magnitudes, where naive summation could lose most of the low-order bits. Many scientific computing environments provide compensated sum routines for this reason. In deep learning, however, a different approach to improved precision is often taken: &#039;&#039;&#039;using a wider accumulator type&#039;&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
=== 3.4. Mixed-Precision Accumulation ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Mixed-precision accumulation&#039;&#039;&#039; refers to performing arithmetic in a higher precision than the input data. A common scenario is accumulating sums in 32-bit floats when the operands are 16-bit floats. Rather than implementing a compensation algorithm, one can simply use a wider accumulator so that intermediate rounding error is smaller. This is effectively what modern GPUs do for tensor operations: for example, NVIDIA &#039;&#039;&#039;Tensor Cores operate on FP16 input data with an FP32 accumulator&#039;&#039;&#039; (the 16-bit products are accumulated in 32-bit)&amp;lt;ref name=&amp;quot;ref6&amp;quot;/&amp;gt;. In this way, summing 2048 products in a dot product has far less precision loss than it would with a 16-bit accumulator. Hardware that supports mixed precision can give the best of both worlds: fast low-precision multiplies and high-precision accumulation.&lt;br /&gt;
&lt;br /&gt;
Software can also leverage mixed precision. For instance, one can sum an array of &amp;lt;code&amp;gt;float&amp;lt;/code&amp;gt; values into a &amp;lt;code&amp;gt;double&amp;lt;/code&amp;gt; variable, accumulating the result in double precision and then converting back to float at the end:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;c&amp;quot;&amp;gt;&lt;br /&gt;
double sum = 0.0;&lt;br /&gt;
for (int i=0; i&amp;lt;N; ++i) {&lt;br /&gt;
    sum += (double) arr_float[i];&lt;br /&gt;
}&lt;br /&gt;
float result = (float) sum;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This approach often yields a more accurate &amp;lt;code&amp;gt;result&amp;lt;/code&amp;gt; than summing in single precision directly, because the intermediate sum carries 52-bit precision. The final rounding to float happens only once at the end (minimizing overall rounding error). In fact, the above is one way to implement &amp;lt;code&amp;gt;math.fsum&amp;lt;/code&amp;gt; in Python (it internally does something similar with multiple partial sums in double precision).&lt;br /&gt;
&lt;br /&gt;
In machine learning training and inference, &#039;&#039;&#039;mixed precision&#039;&#039;&#039; is widely used. When using FP16 for speed, it&#039;s common to accumulate with FP32. For example, &#039;&#039;&#039;Google&#039;s TPUs use BFloat16 for multiplication but accumulate in FP32&#039;&#039;&#039; for matrix ops&amp;lt;ref name=&amp;quot;ref6&amp;quot;/&amp;gt;. AWS’s Inferentia (Neuron) hardware similarly supports FP16/BF16 matrix multiplication with FP32 accumulation&amp;lt;ref name=&amp;quot;ref7&amp;quot;/&amp;gt;. The motivation is that summing hundreds or thousands of half-precision values would otherwise introduce intolerable error or risk overflow. With an FP32 accumulator, the reduction error is drastically reduced (by about &#039;&#039;2&amp;lt;sup&amp;gt;13&amp;lt;/sup&amp;gt;&#039;&#039; times, since FP32 has 13 more precision bits than FP16). Mixed-precision accumulation thus provides a simple and effective form of compensation: &#039;&#039;&#039;use a larger bucket to collect many small contributions&#039;&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
There is a spectrum of options: summing int8 or int16 integers might use 32-bit integers as accumulators for the same reason (to avoid overflow). Some BLAS libraries even offer &#039;&#039;extended precision accumulators&#039;&#039; for single precision dot products (accumulating in double). The downside of mixed precision is the extra memory and maybe throughput cost of the wider type. But on many platforms, the add latency is dominated by hardware design rather than bit-width, so using a 32-bit adder vs 16-bit has negligible speed impact if supported natively. The key is whether data movement or register pressure increases.&lt;br /&gt;
&lt;br /&gt;
In summary, &#039;&#039;&#039;using a higher precision accumulator is often the easiest way to improve numerical accuracy&#039;&#039;&#039; for reductions. It is a primary strategy in deep learning hardware to maintain stability while exploiting low precision for storage and initial computations.&lt;br /&gt;
&lt;br /&gt;
== Precision Formats in AI and Machine Learning ==&lt;br /&gt;
&lt;br /&gt;
Modern AI systems use a variety of numeric formats to balance speed, memory, and accuracy. We discuss common precisions and their roles:&lt;br /&gt;
&lt;br /&gt;
=== 4.1. FP32 and FP64 in Training and Inference ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;FP32 (single precision)&#039;&#039;&#039; has been the workhorse of deep learning for years. Most training scripts in frameworks like TensorFlow and PyTorch used FP32 by default, as it offers sufficient precision to propagate gradients and accumulate weight updates over millions of iterations without excessive error. With ~7 decimal digits of precision and a wide exponent range, FP32 can handle the dynamic range of activations and gradients in typical neural networks. In inference, FP32 is often more precision than strictly necessary for final predictions, but it has remained common because of its ease and the fact that many GPU operations were optimized for FP32.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;FP64 (double precision)&#039;&#039;&#039; is rarely used in deep learning model training or inference, except in specific scenarios requiring extreme precision. FP64 is common in scientific computing and certain types of numerical simulations. In neural networks, however, the extra precision is usually not needed for model accuracy—using doubles tends to yield negligible improvements in model metrics but at a large performance cost (since on GPUs FP64 throughput can be 1/2, 1/8, or even 1/32 of FP32 throughput depending on hardware). One exception might be when integrating neural nets into larger physics simulations or when solving ill-conditioned problems as part of the model. Some researchers use FP64 to diagnose numerical issues or ensure reproducibility. Generally though, deep learning has favored lower precision for efficiency, and FP64 is considered overkill for most networks&amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
That said, certain classical machine learning algorithms benefit from double precision. For example, computing the inverse of a matrix (as in a Gaussian Process regression or Kalman filter embedded in a model) can incur large rounding errors in single precision, so using FP64 can significantly improve the result&amp;lt;ref name=&amp;quot;ref1&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref1&amp;quot;/&amp;gt;. The ONNX standard supports a float64 tensor type for such needs. In ONNX Runtime, however, emphasis has been on FP32 performance, and some pipelines default to float even if the original model was double&amp;lt;ref name=&amp;quot;ref1&amp;quot;/&amp;gt;. If a model’s prediction computation involves operations that are numerically sensitive (like matrix inversion, Cholesky decomposition, or summing extremely large counts), then FP64 may be required to avoid noticeable discrepancies&amp;lt;ref name=&amp;quot;ref1&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref1&amp;quot;/&amp;gt;. A good practice is to test a model in both float and double if you suspect precision issues; significant differences indicate an ill-conditioned calculation that might need higher precision.&lt;br /&gt;
&lt;br /&gt;
In summary, FP32 remains the &#039;&#039;&#039;default&#039;&#039;&#039; format for most training and inference due to its balance of accuracy and efficiency&amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;, while FP64 is reserved for niche cases or precision-critical subroutines. FP32 typically achieves the same accuracy as FP64 for end-to-end deep learning tasks, because network training can often compensate for small noise, and other sources of error (like generalization error) dominate. Therefore, the community trend has been moving to &#039;&#039;&#039;lower&#039;&#039;&#039; precisions, not higher, to gain speed — provided the models still converge and perform well.&lt;br /&gt;
&lt;br /&gt;
=== 4.2. FP16 and BFloat16 in Deep Learning ===&lt;br /&gt;
&lt;br /&gt;
To further speed up neural network training and inference, &#039;&#039;&#039;half-precision (FP16)&#039;&#039;&#039; was adopted, especially with the advent of GPUs supporting FP16 math at twice the rate of FP32. Using FP16 reduces memory storage and bandwidth by 50% compared to FP32, which is significant for large models. However, FP16’s small exponent range and precision posed challenges: training a large network purely in FP16 can fail due to gradient underflow/overflow or weight updates not registering (because changes are below the 0.001 quantization step). To address this, frameworks introduced &#039;&#039;&#039;mixed-precision training&#039;&#039;&#039;, where compute-intensive parts (matrix multiplies, convolutions) run in FP16, but certain accumulations and model parameters remain in FP32. For instance, NVIDIA’s &#039;&#039;&#039;Tensor Core&#039;&#039;&#039; units require FP16 (or BF16) inputs but accumulate results in FP32, effectively ensuring that reductions are done in higher precision&amp;lt;ref name=&amp;quot;ref6&amp;quot;/&amp;gt;. Research showed that with proper loss scaling and mixed precision, models can train to FP32-level accuracy using mostly FP16 arithmetic, achieving significant speedups.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;BFloat16 (BF16)&#039;&#039;&#039; emerged as an alternative 16-bit format primarily from Google’s TPU training systems. BF16 has a larger exponent (8 bits, same as FP32) but only 7 fraction bits&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. This means BF16 can handle the &#039;&#039;&#039;same dynamic range&#039;&#039;&#039; as FP32 (no overflows where FP32 wouldn’t) but at the cost of even lower precision than FP16 (BF16 has about 2–3 decimal digits of precision vs FP16’s ~3–4). Despite its coarse precision, BF16 turns out to be effective for training deep networks because the algorithms (stochastic gradient descent, etc.) are somewhat robust to that noise, and having the wide range prevents catastrophic loss of signal (no gradient will overflow to Inf unless it was going to in FP32 anyway). Many training frameworks (PyTorch, TensorFlow) support BF16 training on hardware like &#039;&#039;&#039;TPU, NVIDIA A100+, Intel CPUs with AVX-512 BF16, etc&#039;&#039;&#039;. Typically, when using BF16, &#039;&#039;&#039;accumulations are still done in FP32&#039;&#039;&#039; (for example, a TPU v2/v3 multiplies BF16 matrices and accumulates into an FP32 result for the dot product&amp;lt;ref name=&amp;quot;ref6&amp;quot;/&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
In inference, both FP16 and BF16 are used to deploy models for faster speed or lower memory. FP16 is popular on NVIDIA GPUs (which have specialized FP16 units). BF16 is advantageous on TPUs and some CPUs that support it, and increasingly on GPUs (Ampere and newer GPUs also support BF16 arithmetic). The choice between FP16 and BF16 can depend on hardware: BF16’s advantage is easier software conversion (you can often take an FP32-trained model and cast weights to BF16 with minimal loss, since the dynamic range is intact), whereas FP16 might require adjusting the model or ensuring values fit in range (sometimes needing techniques like maintaining a master copy of weights in FP32 during training, or handling out-of-range values).&lt;br /&gt;
&lt;br /&gt;
For ONNX, both Float16 and BFloat16 types are part of the standard. ONNX models can be converted to FP16 (truncating weights and inserting cast nodes for computations) using tools like &amp;lt;code&amp;gt;float16_converter&amp;lt;/code&amp;gt; in ONNX Runtime&amp;lt;ref name=&amp;quot;ref8&amp;quot;/&amp;gt;. ONNX Runtime on GPUs will typically execute those FP16 ops with mixed precision on tensor cores (FP16 input, FP32 accumulate), whereas on CPU it may emulate FP16 or upcast to FP32 (as many CPU implementations do not natively support half-precision math)&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;. In fact, as an example, running an ONNX model in FP16 on a CPU might insert hidden Cast operations to FP32 behind the scenes, because the CPU kernel for that op only exists in FP32&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;. This ensures correctness (the computation is done in higher precision) but can reduce performance. We will revisit such backend-dependent behavior in Section 5.3.&lt;br /&gt;
&lt;br /&gt;
In summary, &#039;&#039;&#039;FP16 and BF16&#039;&#039;&#039; are now established as important low-precision formats in deep learning. FP16 offers more precision bits, which can matter for e.g. small differences, but has a limited range (care needed to avoid overflow). BF16 offers safety in range but less precision (which can slightly affect convergence or final accuracy in some cases). Both typically rely on FP32 for accumulation to achieve acceptable accuracy. When deploying models, one should choose a format supported efficiently by the target hardware and verify that the accuracy is not significantly degraded. Often, CNNs and transformers can run in FP16/BF16 with virtually no drop in score, but certain operations (like softmax, see Section 6.3) might need special handling.&lt;br /&gt;
&lt;br /&gt;
=== 4.3. FP8 Formats (E4M3, E5M2) and Scaling ===&lt;br /&gt;
&lt;br /&gt;
The push for ever lower precision has led to experimental &#039;&#039;&#039;8-bit floating point (FP8)&#039;&#039;&#039; formats for deep learning. In 2022, researchers from NVIDIA, Intel, and Arm proposed an FP8 standard with two variants: &#039;&#039;&#039;E4M3&#039;&#039;&#039; (4 exponent bits, 3 fraction bits) and &#039;&#039;&#039;E5M2&#039;&#039;&#039; (5 exponent bits, 2 fraction bits)&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;. Both have 1 sign bit, for a total of 8 bits. E5M2 has a wider exponent range (5 bits gives bias 15) but one fewer mantissa bit than E4M3. The idea is to use E4M3 for numbers that need more precision (like weights or activations around 1.0) and E5M2 for numbers that need more dynamic range (like gradient updates which can be very small or large). In fact, the study found that using E4M3 for forward activations/weights and E5M2 for backward gradients can allow training of large networks with FP8 to accuracy on par with FP16 training&amp;lt;ref name=&amp;quot;ref11&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
FP8 formats are &#039;&#039;not&#039;&#039; IEEE 754 standard (though IEEE is considering lower precision in its 2019 revision). Notably, the E4M3 format as used by NVIDIA does not allocate any bit-pattern for &#039;&#039;+∞&#039;&#039; or &#039;&#039;-∞&#039;&#039; – it has only NaN for invalid results, using that freed bit pattern to extend the representable range by one exponent value&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;. Essentially, E4M3 can represent numbers up to ~&#039;&#039;2&amp;lt;sup&amp;gt;15&amp;lt;/sup&amp;gt;&#039;&#039; (around 3e4) instead of capping at infinity at &#039;&#039;2&amp;lt;sup&amp;gt;16&amp;lt;/sup&amp;gt;&#039;&#039;, by foregoing infinities&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;. Variations also exist regarding negative zero and how NaNs are handled&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;. For instance, GraphCore’s IPU uses FP8 formats that disallow negative zero and infinities, to maximize useable range&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Using 8-bit floats for neural networks requires additional techniques, chiefly &#039;&#039;&#039;scaling&#039;&#039;&#039;. Because 8-bit has very limited precision and range, one typically applies a power-of-two scale factor to groups of values to normalize them into an FP8-friendly range. This is akin to block floating-point or “microscaling” per layer or per tensor. For example, one might keep a FP32 scale for each channel or tensor, and represent values as &amp;lt;math&amp;gt;(FP8\_value) \times 2^{scale}&amp;lt;/math&amp;gt;. The ONNX standard has introduced support for FP8 in version 1.15, including types for E4M3 and E5M2 (with variations FNUZ: finite-only and no negative zero)&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;, and even a type called &#039;&#039;&#039;E8M0&#039;&#039;&#039; which is an 8-bit exponent-only number used purely as a scale factor&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;. The E8M0 type has 8 exponent bits, no mantissa, effectively representing powers of two from &#039;&#039;2&amp;lt;sup&amp;gt;-127&amp;lt;/sup&amp;gt;&#039;&#039; to &#039;&#039;2&amp;lt;sup&amp;gt;128&amp;lt;/sup&amp;gt;&#039;&#039; for scaling purposes&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;. This indicates an approach where an FP8 tensor would be accompanied by an E8M0 scale (or scales) to interpret its value correctly.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Rounding&#039;&#039;&#039; becomes critical when casting values to FP8. The quantization error is much larger relative to FP16 or FP32. Studies have shown that choosing the right rounding mode (e.g., round-to-nearest, stochastic rounding, etc.) and clamping behavior (saturating to max instead of inf) can affect model accuracy&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;. ONNX added a &amp;lt;code&amp;gt;round_mode&amp;lt;/code&amp;gt; attribute to its Cast operator to allow controlling this for FP8&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;. In particular, one paper found that &#039;&#039;&#039;rounding up with saturation&#039;&#039;&#039; (round-to-nearest ties away from 0, and clamp overflows to max finite rather than inf) gave better accuracy in large language model training than other rounding modes&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
At present, FP8 is on the cutting edge. NVIDIA’s H100 GPU supports FP8 tensor cores (for both E4M3 and E5M2), and early results show minimal accuracy loss on some vision and NLP tasks when properly tuned. However, using FP8 requires fine-tuning hyperparameters like scaling factors for each layer and perhaps tweaking training procedures. ONNX’s inclusion of FP8 types means you can represent and transport models using FP8 weights or activations, but actual hardware support is limited and typically paired with calibration or dynamic scaling logic outside of ONNX graph (or using custom ONNX functions for scaling). It remains an &#039;&#039;&#039;open research area&#039;&#039;&#039; how to fully automate FP8 quantization for arbitrary models without accuracy loss.&lt;br /&gt;
&lt;br /&gt;
In summary, FP8 formats promise further compression of neural network computation and data. They are an order of magnitude more error-prone than FP16, but when combined with careful scaling, mixed usage of E4M3/E5M2, and maybe modified training techniques, they have shown surprising success. ONNX is actively evolving to support these ultra-low precision types, signaling their potential importance in future AI inference deployments.&lt;br /&gt;
&lt;br /&gt;
== Accumulation Semantics in ONNX ==&lt;br /&gt;
&lt;br /&gt;
One of the roles of the ONNX specification is to define the mathematical behavior of operators (Conv, MatMul, Sum, etc.) in a framework-agnostic way. However, ONNX mostly defines &#039;&#039;what&#039;&#039; an operator computes in exact arithmetic, not &#039;&#039;how&#039;&#039; the computation is carried out in finite precision. This leaves certain details – like accumulator precision or summation order – up to the implementation (runtime or hardware). We&#039;ll examine how ONNX treats accumulation and what guarantees (or lack thereof) are provided.&lt;br /&gt;
&lt;br /&gt;
=== 5.1. Operator Definitions and Numerical Guarantees ===&lt;br /&gt;
&lt;br /&gt;
Each ONNX operator is described as performing an ideal mathematical function on its inputs. For example, &amp;lt;code&amp;gt;MatMul&amp;lt;/code&amp;gt; is defined as the standard matrix product of two tensors, and &amp;lt;code&amp;gt;ReduceSum&amp;lt;/code&amp;gt; computes a summation along given axes. The ONNX spec, as of current versions, &#039;&#039;&#039;does not mandate the precision of intermediate calculations&#039;&#039;&#039; beyond the types of the inputs and outputs. If an operator’s inputs and outputs are float16, the spec implies the computation is done in float16 in a sense, but it does not forbid an implementation from using higher precision internally – it simply doesn&#039;t mention it. There is &#039;&#039;&#039;no explicit guarantee of identical bit-for-bit results&#039;&#039;&#039; across different ONNX backends for floating-point ops. Instead, results are expected to be numerically close, within tolerances one would normally expect from floating-point differences (due to different hardware or algorithms). The ONNX Backend Test suite typically uses a relative or absolute error tolerance when validating outputs for this reason.&lt;br /&gt;
&lt;br /&gt;
Because of this, important details like the use of extended precision or accumulation in higher precision are left ambiguous. This has led to discussions in the ONNX community about making such behavior more explicit. For example, a proposal was raised to allow specification of accumulation precision for certain ops like MatMul&amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;. The reasoning is that as lower-precision types (float16, bfloat16, float8) become more common, knowing or controlling whether an op internally accumulates in a higher precision is important for both accuracy and reproducibility&amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;. In June 2025, an issue on the ONNX GitHub suggested adding an attribute to MatMul (and similarly Gemm, Softmax, etc.) to specify the accumulator type&amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;. By default it would use input precision (status quo), but an attribute could request (for instance) 32-bit accumulation even if inputs are 16-bit&amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;. As of this writing, however, no such attribute exists in the official spec – it&#039;s an open issue. Thus, the numerical semantics are effectively: &#039;&#039;“compute the result as if in the same type as inputs, but actual implementations may use more precision internally as an optimization.”&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
From a guarantee standpoint, ONNX operators promise the mathematically correct result assuming real-number arithmetic, but they &#039;&#039;&#039;do not guarantee bit-exact results&#039;&#039;&#039; due to floating-point effects. There is an understanding that differences can arise from different summation orders, fused operations (e.g., an implementation might use an FMA (fused multiply-add) which has one rounding instead of two), or use of extended precision registers. ONNX does not formally define rounding modes either – it relies on IEEE 754 defaults, but if a backend used something like deterministic summation or alternative rounding, ONNX has no mechanism to express that.&lt;br /&gt;
&lt;br /&gt;
In summary, ONNX&#039;s current stance is largely &#039;&#039;&#039;implementation-dependent&#039;&#039;&#039; for numerical precision. It provides minimal explicit numerical guarantees beyond type constraints. This is why an ONNX model run on two different hardware or runtime libraries can have slightly different results, especially in reduced precision. The burden is on backends to produce results &amp;quot;close enough&amp;quot; to each other that they don&#039;t affect the model&#039;s decisions in any significant way.&lt;br /&gt;
&lt;br /&gt;
=== 5.2. MatMul, Conv, and Reduction Operators ===&lt;br /&gt;
&lt;br /&gt;
Consider ONNX &amp;lt;code&amp;gt;MatMul&amp;lt;/code&amp;gt;: it takes two tensors (often two matrices) and produces their matrix product. In pure math, &amp;lt;math&amp;gt;C_{ij} = \sum_k A_{ik} &#039;&#039; B_{kj}&amp;lt;/math&amp;gt;. If A and B are float16, should that sum be computed in float16 or float32? As discussed, ONNX doesn’t specify – it simply says the output is a float16 matrix containing the product. In practice, many backends will &#039;&#039;promote&#039;&#039; the computation: for example, NVIDIA GPUs will do the multiplication and addition in a mixed FP16/FP32 mode by default (because of tensor cores), then convert the final result back to float16. ONNX Runtime on CPU might not support float16 multiplication natively, so it will insert cast ops to float32, perform the MatMul in float32, then cast back to float16&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;. From ONNX’s perspective, as long as the final result is within acceptable error of the “ideal” float16 computation, it’s fine. But note, an “ideal” float16 computation (where every multiply and add is rounded to FP16) might actually be &#039;&#039;less accurate* than what the backend did with FP32. This means that a backend using higher precision can produce a result that is not bit-equal to a pure FP16 implementation – it&#039;s actually closer to the true real number result. This is usually considered a good thing (improved accuracy), but it does highlight a lack of consistency: another backend might do everything in pure FP16 and get a slightly different answer.&lt;br /&gt;
&lt;br /&gt;
The same situation occurs for &#039;&#039;&#039;convolution&#039;&#039;&#039; (&amp;lt;code&amp;gt;Conv&amp;lt;/code&amp;gt; op) which is essentially a batched sum of products (like MatMul in sliding window), and for explicit reduction ops like &amp;lt;code&amp;gt;ReduceSum&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;ReduceMean&amp;lt;/code&amp;gt;, etc. If you have an ONNX &amp;lt;code&amp;gt;ReduceSum&amp;lt;/code&amp;gt; that sums 1000 float16 values, the spec doesn’t say how to sum them. A GPU kernel might sum in a tree using FP32 accumulators; a naive implementation might sum in order with FP16 and possibly overflow or underflow intermediate steps. Both are allowed as long as they output a float16 result that is “reasonable.” In practice, ONNX Runtime and others take care to avoid obviously bad behavior (like overflow) by using higher precision or by splitting the sum. But again, it’s not mandated by the ONNX standard explicitly.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;\mathrm{Softmax}(x)_i = \frac{\exp(x_i)}{\sum_j \exp(x_j)},&amp;lt;/math&amp;gt;&lt;br /&gt;
typically applied along a specified axis. Directly computing this in float16 can be problematic for large vectors, as discussed in Section 6.3. Indeed, the issue of accumulation precision arises: should the sum &amp;lt;math&amp;gt;\sum_j \exp(x_j)&amp;lt;/math&amp;gt; be done in FP16 or can it be in FP32? Many implementations will do it in FP32 even if &#039;&#039;x&#039;&#039; is FP16, because the dynamic range of &amp;lt;math&amp;gt;\exp(x)&amp;lt;/math&amp;gt; is large. The ONNX Softmax spec doesn’t detail this, but it expects the result to be a proper probability distribution (summing to 1.0 within normal floating error). A pure FP16 softmax on, say, 1000 elements could sum to something like 0.996 or 1.004 due to rounding, whereas FP32 accumulation would be much closer to 1.000. ONNX likely allows both; the small difference is usually inconsequential, but if it were large (say the FP16 sum overflowed to inf), that would be a correctness issue. A robust backend will prevent that (by using the common max-subtraction trick and higher precision accumulation). The ONNX issue 7072 indeed calls out Softmax as another op where explicit control of accumulation precision would be useful&amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
In summary, &#039;&#039;&#039;MatMul, Conv, and reduction ops&#039;&#039;&#039; in ONNX are defined mathematically but with &#039;&#039;implicit&#039;&#039; numerical behavior. Implementations commonly use higher precision for intermediate sums especially when input is low precision, but this is not standardized. Consequently, results can differ slightly between implementations. For critical applications, one must be aware of these differences. A model exported to ONNX with float16 weights might perform slightly differently on one runtime vs another if one does all float16 math and the other uses float32 accumulations. Both conform to ONNX. As a user, if you require a certain precision for accumulation, currently you have to ensure it by the way you export or run the model (for example, insert manual cast nodes in the ONNX graph to force certain ops to FP32). We may see future ONNX versions address this with attributes or metadata as mentioned, but for now these ops are effectively “compute with best effort accuracy”.&lt;br /&gt;
&lt;br /&gt;
=== 5.3. Backend and Hardware-Dependent Behavior ===&lt;br /&gt;
&lt;br /&gt;
Because ONNX is implemented by many backends (ONNX Runtime, TensorRT, CoreML, PyTorch JIT, etc.), and these in turn run on varied hardware (CPU, GPU, accelerators), the numeric behavior can be backend-dependent. Some examples:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;CPU backends:&#039;&#039;&#039; Many CPUs do not have native float16 vector arithmetic (with a few exceptions in recent CPUs). ONNX Runtime’s default CPU execution provider will often upcast float16 to float32, compute, then downcast&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;. This ensures accuracy but means that if you thought you were getting a full float16 pipeline, you’re not – the CPU is essentially ignoring the low precision in compute. On the other hand, x86 CPUs have extended 80-bit precision in x87 registers (though less used now) and 64-bit SIMD registers for doubles; some compiler optimizations might carry intermediate results in higher precision. This could make CPU results slightly different from a GPU’s if, say, a sum is done in 80-bit on CPU versus 32-bit on GPU. Generally, modern compilers avoid such extended precision unless explicitly asked, to improve reproducibility.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;GPU backends:&#039;&#039;&#039; GPUs often perform summations in parallel using tree reductions. The order of operations can change depending on how threads are scheduled. This means even on the same GPU, summing an array in one launch versus another might not give bit-identical results if the internal parallel reduction algorithm differs (though it should be deterministically the same given the same library version and input, but if you change GPU model or BLAS library, it might differ). Also, GPUs have fused multiply-add (FMA) instructions that do &#039;&#039;(a*b + c)&#039;&#039; in one step with one rounding, which can produce slightly different results than doing the multiply and add separately (which would round twice). If an ONNX backend uses a fused kernel (like a GEMM library) it might yield different low-order bits than a simple loop.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Vendor libraries:&#039;&#039;&#039; ONNX Runtime can use vendor BLAS or DNN libraries (MKL, cuDNN, TensorRT, etc.). These libraries often implement their own strategies for numerical stability. For example, NVIDIA’s cuDNN might use FP32 accumulation for FP16 inputs by default. TensorRT (NVIDIA’s inference engine) will sometimes automatically promote precisions or insert scaling to maintain stability&amp;lt;ref name=&amp;quot;ref13&amp;quot;/&amp;gt;. On the other hand, if you explicitly request FP16 in TensorRT with no FP32 fallback, it might do the entire op in FP16 and you’d see more error. Some libraries offer knobs: TensorRT allows choosing FP32, FP16, or INT8 precision for each layer, and will use FP32 internally for safety in some cases unless forced otherwise.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Hardware-specific quirks:&#039;&#039;&#039; As another example, NVIDIA’s &#039;&#039;&#039;TensorFloat-32 (TF32)&#039;&#039;&#039; on Ampere GPUs is a precision mode where matrix multiplications use 10-bit mantissa (so about FP16 precision for the multiply) but keep 8-bit exponent (range like FP32). By default, CUDA libraries use TF32 for FP32 matrix multiplies on Ampere to speed them up, unless you disable it. This means if you run an ONNX model with FP32 on an A100 GPU, your matmuls might actually be lower precision than FP32 (though higher than FP16)&amp;lt;ref name=&amp;quot;ref6&amp;quot;/&amp;gt;. It’s an example of hardware choosing an intermediate precision for performance. From ONNX’s perspective, the output is still FP32 and (hopefully) within error tolerances, but a user might be surprised that it’s not a “true” FP32 computation internally. If exactness is needed, one has to turn off TF32.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Non-determinism:&#039;&#039;&#039; Some backends (especially on GPUs) allow non-deterministic execution for performance. For instance, atomic adds in parallel reductions can produce results that vary run-to-run because the order of accumulation is race-condition-dependent. ONNX’s spec doesn’t cover this explicitly, but it’s something to consider. In ONNX Runtime, there are settings for deterministic computing, but by default it may use all available performance optimizations, even if results vary at the 1e-7 level.&lt;br /&gt;
&lt;br /&gt;
In short, the numerical behavior under ONNX can depend on the combination of backend software and hardware capabilities. The lack of a strict spec for accumulation means implementations have freedom to use clever strategies (higher precision, reordering) to improve accuracy or speed. Usually this is positive (e.g. getting better accuracy than a naive approach). But it could also lead to slight inconsistencies. For example, you might find that exporting a PyTorch model to ONNX and running it with one engine yields outputs slightly different from PyTorch’s – not because ONNX is wrong, but because the summation or convolution was done differently (maybe PyTorch summed in FP32 as well, but maybe not in exactly the same way).&lt;br /&gt;
&lt;br /&gt;
The key takeaway is that ONNX &#039;&#039;by design&#039;&#039; abstracts away these details, which is good for portability but means you need to &#039;&#039;&#039;trust the backend&#039;&#039;&#039; or verify its precision behavior for sensitive models. If necessary, you can enforce certain behavior by inserting ops (for example, if you want to ensure FP32 accumulation, you could insert a cast to FP32 for inputs of a MatMul and cast the result back to FP16 – this way the ONNX graph itself mandates FP32 compute in between). This of course sacrifices some performance or device-specific magic.&lt;br /&gt;
&lt;br /&gt;
Looking forward, as mentioned, there are proposals to enrich ONNX’s specification regarding precision. This might include explicit flags for “exact summation” or “accumulate in int32” etc., which would make models more self-descriptive but also potentially less portable (if a backend doesn’t support that, what then?). For now, users should be aware of these backend differences. Most of the time, well-engineered ONNX runtimes yield results close enough that they don&#039;t affect the application (e.g., classification top-1 remains the same even if low-level sums differ by 1e-5). But for high-stakes numerical computations, testing and maybe adjusting the model or backend settings is prudent.&lt;br /&gt;
&lt;br /&gt;
== Numerical Stability in Deep Learning Models ==&lt;br /&gt;
&lt;br /&gt;
Deep learning computations involve many operations that, while theoretically well-defined, can be sensitive to numerical issues when implemented in finite precision. We&#039;ll discuss a few common scenarios in neural networks where numerical stability is a concern, and how precision and accumulation play a role.&lt;br /&gt;
&lt;br /&gt;
=== 6.1. Dot Products and Large-Scale Reductions ===&lt;br /&gt;
&lt;br /&gt;
Neural networks are full of dot products: every fully-connected layer computes &amp;lt;math&amp;gt;\mathbf{y} = W\mathbf{x}&amp;lt;/math&amp;gt;, which is a series of inner products between weight vectors and the input. Convolutional layers compute weighted sums of local regions. Attention mechanisms compute large matrix products of query/key/value matrices. All these boil down to summing a lot of terms: &amp;lt;math&amp;gt;\sum_{k=1}^K a_k b_k&amp;lt;/math&amp;gt;. When &#039;&#039;K&#039;&#039; is large (it could be in the thousands or more for a single neuron in a fully connected layer of a large model), the potential for floating-point error in that sum increases.&lt;br /&gt;
&lt;br /&gt;
If using FP32, the error may still be negligible in many cases, but in lower precision like FP16, it can become significant. For example, consider a dot product of length 2048 in FP16. Each product is in FP16 (with ~3 decimal digits precision) and if accumulated in FP16, the worst-case error could be on the order of &#039;&#039;2048  × ε_FP16 ≈ 2048  × 0.0009766 ≈ 2&#039;&#039; (i.e., you could lose a couple units in the third decimal place relative accuracy in worst case). Even in random error accumulation, you might see error on the order of &amp;lt;math&amp;gt;\sqrt{2048} \times 0.0009766 \approx 0.044&amp;lt;/math&amp;gt; or 4.4% (though this is a rough estimate). That’s quite significant. Fortunately, as discussed, most hardware will accumulate such sums in a higher precision like FP32, dramatically reducing that error (~7 orders of magnitude smaller &#039;&#039;ε&#039;&#039;). But if it didn’t, the network’s outputs could get noisy. A fully-connected layer with many inputs might output slightly different results depending on summation order if done in low precision.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Large-scale reductions&#039;&#039;&#039; also occur in operations like global average pooling (summing all elements of a feature map) or computing the total loss by summing loss over batch samples. If one sums 100,000 values in FP32, the worst-case error could be on the order of 100,000 &#039;&#039; 1e-7 ≈ 1e-2 (1% uncertainty), though typical error would be much less. In FP16, summing 100k values is likely impossible without overflow: as noted, adding ~10^4 moderate numbers can overflow FP16&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. So large reductions &#039;&#039;must* be handled carefully (either in chunks or in higher precision). Frameworks generally do: for example, when performing a sum reduction on GPU, libraries break it into parallel blocks that accumulate in registers (usually 32-bit registers, even if inputs are 16-bit). &lt;br /&gt;
&lt;br /&gt;
Another aspect is that many dot products in neural nets involve values of varying magnitude. If the distribution of terms is such that some are much larger than others, the smaller terms might not contribute much if summed last. Ideally, one might sort by magnitude or use pairwise summation to improve that. Most BLAS libraries effectively do a pairwise strategy due to their parallel structure. So, while worst-case analyses are grim, practical implementations mitigate a lot of it.&lt;br /&gt;
&lt;br /&gt;
However, certain layers can suffer if not handled properly. One famous example is in RNNs or LSTMs: if one uses FP16 naive summation for the gating mechanism, errors could accumulate over many time steps and cause drift or instability. In modern practice, recurrent layers are often run in FP16 with FP32 accumulators to avoid this.&lt;br /&gt;
&lt;br /&gt;
In summary, &#039;&#039;&#039;dot products and large reductions need sufficient precision&#039;&#039;&#039; or robust algorithms. If you are implementing a custom kernel (say a custom ONNX op) and you have to sum a huge array, you should consider using a technique from Section 3 (pairwise summation, Kahan, or at least double accumulator if possible). Many deep learning computations, by virtue of hardware and library design, already do something like this under the hood. But it&#039;s always good to be aware, especially if pushing into lower precision territories.&lt;br /&gt;
&lt;br /&gt;
=== 6.2. Cancellation, Dynamic Range, and Conditioning ===&lt;br /&gt;
&lt;br /&gt;
We introduced &#039;&#039;cancellation&#039;&#039; earlier in the context of summation. In deep learning, cancellation can appear in a few places. One is in the computation of certain norms or variances. For instance, Batch Normalization computes mean and variance of activations. The variance formula &amp;lt;math&amp;gt;\mathrm{Var}(x) = E[x^2] - (E[x])^2&amp;lt;/math&amp;gt; is subject to catastrophic cancellation if &#039;&#039;E[x]&#039;&#039; and &amp;lt;math&amp;gt;\sqrt{E[x^2]}&amp;lt;/math&amp;gt; are close in magnitude (which can happen if the variance is small compared to the mean). If implemented naively in single precision, you might get a negative variance due to numerical error (which would then be clamped to zero or sqrt would yield NaN). Robust implementations compute variance in a single pass or increase precision. Indeed, in many frameworks, batchnorm accumulation is done in FP32 even if the activations are FP16, precisely to avoid numerical issues in computing mean and variance. ONNX itself doesn’t specify this, but an ONNX backend is likely to do batchnorm stats in higher precision for safety. The &#039;&#039;conditioning&#039;&#039; of the variance calculation is poor when variance is tiny relative to mean, so it&#039;s a spot where higher precision is needed.&lt;br /&gt;
&lt;br /&gt;
Another example is in certain loss functions or regularizations. If you subtract two large numbers to get a small difference, you lose precision. Say you have two nearly identical predictions and you subtract them as part of some operation – if done in float16, you might get zero whereas float32 would have a slight difference. This could affect, say, a gradient if that difference was supposed to propagate information. Fortunately, neural nets often avoid directly subtracting large nearly equal numbers; but it can happen. In attention mechanisms, the &#039;&#039;score normalization&#039;&#039; (softmax) subtracts a max to avoid huge exponentials, which is good for range but if multiple scores are close to the max, you subtract numbers that are close. However, those subtractions are not catastrophic in the same way; they&#039;re intentional to reduce range and are typically fine in FP32 or even FP16.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Dynamic range&#039;&#039;&#039; issues are everywhere in deep learning. Activation functions like ReLU and exponential can produce very large or very small values. If subsequent computations don’t account for that, you get infinities or zeros. For example, consider a network output that produces a logit of 100 in FP16. &amp;lt;math&amp;gt;\exp(100)&amp;lt;/math&amp;gt; in FP16 overflows to infinity (since FP16 max ~ 6e4 and &#039;&#039;e&amp;lt;sup&amp;gt;100&amp;lt;/sup&amp;gt; ∼ 3.7e43&#039;&#039;). If you then do softmax, that one infinity will dominate the sum and you might get a probability of 1 for that class (or NaNs if not handled). The standard softmax implementation subtracts the max logit, so in this case it would subtract 100 from all logits, making the largest exponent &amp;lt;math&amp;gt;\exp(0)=1&amp;lt;/math&amp;gt; and others tiny – this avoids overflow. But what about underflow? If you had a very negative logit, &amp;lt;math&amp;gt;\exp(-100)&amp;lt;/math&amp;gt; in FP16 would underflow to 0. In softmax, that just contributes ~0 probability, which is fine (it effectively is zero anyway). Underflow tends to be less dangerous than overflow in inference — underflow usually just means “this probability/neuron is effectively 0,” whereas overflow can poison the results with NaNs or Infs. &lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Conditioning&#039;&#039;&#039; refers to how sensitive a function is to input perturbations. Neural networks themselves as functions can be poorly conditioned (e.g., tiny changes in weights cause big changes in output for some architectures), but typically they are designed to be reasonably well-behaved. Certain internal computations like matrix inversion (if one does a pseudoinverse or something for a layer) can be extremely ill-conditioned if the matrix is near singular. That again would need double precision perhaps. In ML, one common ill-conditioned operation is the &#039;&#039;&#039;softmax denominator&#039;&#039;&#039;: &amp;lt;math&amp;gt;\sum_j e^{x_j}&amp;lt;/math&amp;gt; can be very large and also very sensitive to changes in the largest &#039;&#039;x&amp;lt;sub&amp;gt;j&amp;lt;/sub&amp;gt;&#039;&#039;. However, the softmax is more stable if computed carefully (max trick). Another is &#039;&#039;&#039;normalization&#039;&#039;&#039;: computing &amp;lt;math&amp;gt;\frac{x - \mu}{\sigma}&amp;lt;/math&amp;gt; for layer norm or batch norm – if &amp;lt;math&amp;gt;\sigma&amp;lt;/math&amp;gt; is very small, then the division blows up noise. In those cases, a small &#039;&#039;ε&#039;&#039; is usually added to &amp;lt;math&amp;gt;\sigma^2&amp;lt;/math&amp;gt; to prevent instability (this is a model-level fix, not a numeric one per se, but it acknowledges numeric limits).&lt;br /&gt;
&lt;br /&gt;
In training, &#039;&#039;&#039;optimizers&#039;&#039;&#039; can suffer from precision issues. For example, the Adam optimizer keeps a moving average of squared gradients; in half precision, these small updates might flush to zero or not change at some point. Some research found that certain adaptive optimizers break in FP16 because the small differences are lost (&amp;quot;Why FP16 training breaks NAdam&amp;quot; etc.). The solution is often to keep the optimizer states in FP32 while doing the bulk of compute in FP16. This again is a form of mixed precision.&lt;br /&gt;
&lt;br /&gt;
From an inference perspective, once the model is trained, the main concerns are avoiding overflow in activations and ensuring summations (like in softmax or reductions) are done with enough precision. The model’s weights are fixed, so we don&#039;t have to worry about cumulative error over iterations as in training, just the forward pass.&lt;br /&gt;
&lt;br /&gt;
=== 6.3. Normalization Layers and Softmax ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Normalization layers&#039;&#039;&#039; (BatchNorm, LayerNorm, etc.) and &#039;&#039;&#039;Softmax&#039;&#039;&#039; are specifically worth examining for numerical stability:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Batch Normalization:&#039;&#039;&#039; During inference, batchnorm applies an affine transform using precomputed mean and variance (from training). It’s basically: &amp;lt;math&amp;gt;y = \frac{x - \mu}{\sqrt{\sigma^2 + \epsilon}} \gamma + \beta&amp;lt;/math&amp;gt;. This is a stable operation as long as &#039;&#039;ε&#039;&#039; is provided (to avoid division by zero). Since &amp;lt;math&amp;gt;\mu, \sigma^2&amp;lt;/math&amp;gt; are constants in inference, it reduces to linear ops which are fine. The main numerical work (computing mean/variance) happened during training or model calibration, and frameworks usually did that in FP32. So batchnorm at inference time is not a source of instability (it’s just a scale and shift of the data).&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Layer Normalization / Group Normalization:&#039;&#039;&#039; These compute mean/var on the fly for each sample (and possibly channel group). In inference, they still need to compute these statistics for each forward pass. If implemented in low precision, the variance calculation can suffer cancellation as mentioned. However, it&#039;s likely that inference engines compute the mean and variance in FP32 even if inputs are FP16 (especially since these are per-sample operations, not huge batch reductions – it&#039;s feasible to do in higher precision). If someone implemented layer norm purely in FP16, they might see issues when the variance is very small. Usually there’s an &#039;&#039;ε&#039;&#039; added which might dominate a tiny variance and thus mitigate catastrophic cancellation (the result might just output something mostly from &#039;&#039;ε&#039;&#039;). In summary, normalization layers are usually fine as long as the implementation uses enough precision for the summation of squares.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Softmax:&#039;&#039;&#039; Softmax is notorious if done improperly. The safe algorithm: subtract the maximum input from all inputs (to bring the largest exponent to 1.0), exponentiate, sum them up, divide each exponent by the sum. The max subtraction handles overflow in &amp;lt;math&amp;gt;\exp()&amp;lt;/math&amp;gt;. But what about the sum? As discussed, if there are &#039;&#039;N&#039;&#039; elements in the softmax, the sum is at most &#039;&#039;N&#039;&#039; (when all inputs equal the max). In classification, &#039;&#039;N&#039;&#039; might be the number of classes. For ImageNet, &#039;&#039;N=1000&#039;&#039;, so worst sum ~1000 (which is fine in FP16, as 1000 &amp;lt;&amp;lt; 65504). For an extreme case like a language model, &#039;&#039;N&#039;&#039; could be 50,000 (vocab size). Sum could be 50,000 in worst case (everyone equal probability). 50,000 is still below 65504, so actually an FP16 sum would &#039;&#039;not&#039;&#039; overflow in that scenario, and the rounding error on a sum of 50k identical terms would be maybe a few ULPs (which at 50k magnitude might be ~0.5 difference worst-case). So maybe softmax summation in FP16 is not catastrophic for output probabilities – you’d get essentially 1.0 after normalization instead of 0.99999 in some case. Usually fine.&lt;br /&gt;
&lt;br /&gt;
:The bigger issue is if one or a few terms dominate the softmax. If one term is much larger, the others underflow to 0 after the max subtraction. That’s fine: you get a one-hot distribution essentially. If the model expected a softer distribution, that could be a problem. But if the logits were really that far apart, even FP32 would give nearly one-hot. So not really a precision issue, more of a model property.&lt;br /&gt;
&lt;br /&gt;
:However, the &#039;&#039;&#039;stability during training&#039;&#039;&#039; is a big issue. The GitHub thread we saw (ColossalAI) mentioned training a 130B model where FP16 softmax in attention caused training to occasionally blow up&amp;lt;ref name=&amp;quot;ref14&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref14&amp;quot;/&amp;gt;. They observed attention scores in some heads ranging up to 1e4 or as low as 1e-3&amp;lt;ref name=&amp;quot;ref14&amp;quot;/&amp;gt;. 1e4 exponent in FP16 is definitely Inf (because &amp;lt;math&amp;gt;\exp(10^4)&amp;lt;/math&amp;gt; is astronomically huge, but even exp(100) is inf in half). The method CogView introduced (Precision Bottleneck Relaxation) involved subtracting the max &#039;&#039;per head&#039;&#039; to reduce range&amp;lt;ref name=&amp;quot;ref14&amp;quot;/&amp;gt;—which is basically the softmax max trick at a finer granularity or something similar. In the end, they found &#039;&#039;&#039;the simplest fix was to compute softmax in FP32&#039;&#039;&#039; for those layers&amp;lt;ref name=&amp;quot;ref14&amp;quot;/&amp;gt;. They reported that it had negligible speed impact but significantly improved stability&amp;lt;ref name=&amp;quot;ref14&amp;quot;/&amp;gt;. This highlights a general principle: certain operations like softmax (and sometimes matrix multiplications in attention) benefit from higher precision, and using FP32 in a few critical spots can save a mixed-precision training from crashing. In inference, using FP32 for softmax is also a safeguard if one is uncertain. It costs little because softmax is not a heavy compute compared to matrix multiplies.&lt;br /&gt;
&lt;br /&gt;
:ONNX does have a Softmax operator; if the input is float16, a backend might do exactly what they described: internally cast to float32 for the softmax computation, then cast output back to float16. Some inference engines (like DeepSpeed’s inference or others) keep attention softmax in FP32 even if rest is FP16, for robustness&amp;lt;ref name=&amp;quot;ref14&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Apart from softmax, other places where exponentials occur are in loss functions like cross-entropy (which uses log-softmax internally), or in activation functions like Sigmoid and Tanh (which are basically scaled logistic functions). Sigmoid(&#039;&#039;x)=1/(1+e&amp;lt;sup&amp;gt;-x&amp;lt;/sup&amp;gt;)&#039;&#039;. If &#039;&#039;x&#039;&#039; is moderately large positive, &#039;&#039;e&amp;lt;sup&amp;gt;-x&amp;lt;/sup&amp;gt;&#039;&#039; underflows to 0 in FP16 if &amp;lt;math&amp;gt;x&amp;gt;~11&amp;lt;/math&amp;gt;. That gives a sigmoid output of 1.0 exactly. In FP32, &#039;&#039;e&amp;lt;sup&amp;gt;-x&amp;lt;/sup&amp;gt;&#039;&#039; for &#039;&#039;x=11&#039;&#039; is about &#039;&#039;5e-5&#039;&#039;, not underflow, so sigmoid ~0.99995. So FP16 will saturate some outputs to exactly 0 or 1 sooner than FP32 would. This can slightly change behaviors (e.g., a neuron might go to full saturation a bit early). Usually, this isn’t a huge deal, but it could make gradients zero earlier. In inference, we don’t care about gradients, just the output value. Getting exactly 1.0 vs 0.99995 probably has no practical difference for a classification output. But one should be aware that &#039;&#039;&#039;activation functions can saturate faster&#039;&#039;&#039; in lower precision due to limited range or precision. ReLU has no issue (it’s linear or 0), but exponential-based activations do. In practice, again, frameworks often simply compute those in the given precision and accept the slight difference. If it were a problem, one could do those in FP32 as well.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Summary:&#039;&#039;&#039; Numerical stability in inference is maintained by using robust computation methods: subtracting maxima in softmax/LogSumExp, adding epsilon in denominators for normalization, and using higher precision for summations or critical steps. Most of these are built into the algorithms. ONNX models assume these standard practices (they don&#039;t need to explicitly do them in the graph because the backend inherently will for ops like Softmax). It&#039;s wise when building or exporting models to ONNX to test them in the target precision and ensure there are no surprises (like an output becomes NaN or Inf due to precision). Usually if training was done in mixed precision, the model will have been vetted in that environment already.&lt;br /&gt;
&lt;br /&gt;
== Performance vs. Accuracy Trade-offs ==&lt;br /&gt;
&lt;br /&gt;
There is an inherent trade-off between numerical precision and computational performance (speed, memory usage) in AI systems. Choosing lower precision can dramatically increase throughput and reduce memory, but it may introduce arithmetic errors. Engineers must balance these considerations:&lt;br /&gt;
&lt;br /&gt;
=== 7.1. Throughput, Memory Bandwidth, and Precision ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Throughput (speed)&#039;&#039;&#039; is often higher for lower-precision operations. Modern hardware can execute more low-precision ops in parallel than high-precision ops. For example, an NVIDIA V100 GPU can perform 2× more half-precision FLOPs than single-precision per clock, and newer architectures with Tensor Cores can do 8× more (since a Tensor Core warp operation might perform 64 FP16 FMAs per cycle vs 16 FP32). Similarly, int8 operations can be even faster and more energy efficient. Lower precision numbers also use less memory bandwidth – fetching 16-bit data is twice as fast as 32-bit data from memory (assuming the same bus width), which can relieve memory bottlenecks. Many deep learning workloads are memory-bound, so halving the data size can nearly double throughput if compute is not the limiting factor. This is a huge incentive to use lower precision.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Memory and storage&#039;&#039;&#039;: Using FP16 or int8 weights reduces model size, which is important for deploying large models to memory-limited environments (like edge devices or GPUs with limited VRAM). It also means you can fit larger batch sizes in memory, or more models on a chip, etc. There is also a cascade effect: smaller data means better cache utilization, which further improves performance.&lt;br /&gt;
&lt;br /&gt;
However, &#039;&#039;&#039;accuracy&#039;&#039;&#039; suffers if precision is too low. We have discussed how rounding error and limited range can cause deviations in outputs. The question is: do those deviations matter for the application? Often for neural networks, a small amount of noise or error in intermediate calculations does not change the final prediction. Neural nets are somewhat error-tolerant due to their redundant distributed representations. This is why quantization and FP16 techniques can work at all. The final model accuracy (e.g. top-1 accuracy on ImageNet) might drop only 0.5% or less when using FP16 instead of FP32, or int8 instead of FP16, if done properly. For many use-cases, this loss is acceptable given the speed gains.&lt;br /&gt;
&lt;br /&gt;
If one goes too low (like naive FP8 or int4 without special care), accuracy might drop severely – perhaps making the model unusable. Thus the trade-off: as precision decreases, performance increases but at some point accuracy degrades beyond an acceptable threshold. Engineers aim to find the lowest precision that still preserves accuracy.&lt;br /&gt;
&lt;br /&gt;
There is also the consideration of &#039;&#039;&#039;accumulation strategy&#039;&#039;&#039; affecting performance: for instance, Kahan summation (compensated) is more accurate but ~4× slower than naive summation. If you need that accuracy, you pay a performance cost. In many AI inference scenarios, the slight extra accuracy from something like Kahan isn&#039;t needed, so it’s not used. But in a scenario where you sum millions of elements for a final result that is thresholded, maybe you do need it.&lt;br /&gt;
&lt;br /&gt;
Another dimension is &#039;&#039;&#039;determinism vs speed&#039;&#039;&#039;. Enforcing a deterministic reduction order (for reproducibility) might require disabling some parallelism, which could slow things down. If exact repeatability is critical (like certain scientific or financial applications of neural nets), one might sacrifice some performance to ensure the summation happens the same way every time. ONNX allows both modes depending on the runtime.&lt;br /&gt;
&lt;br /&gt;
From a memory perspective, if precision is higher than needed, you waste memory. For example, storing intermediate activations in FP32 when they could be FP16 uses 2× memory, which might mean lower batch size or more memory traffic. So there&#039;s strong motivation to keep things as low-precision as possible &#039;&#039;except&#039;&#039; where accuracy truly demands.&lt;br /&gt;
&lt;br /&gt;
In summary, the trade-off often manifests as &#039;&#039;diminishing returns&#039;&#039;: FP64 vs FP32 – huge slowdown for usually negligible accuracy gain in ML; FP32 vs FP16 – moderate speedup for usually minor accuracy hit; FP16 vs int8 – another big speedup but maybe more accuracy loss unless calibration is done; int8 vs int4 – speed gain but often big accuracy drop that might not be worth it for current models.&lt;br /&gt;
&lt;br /&gt;
Engineers use techniques like &#039;&#039;&#039;mixed precision&#039;&#039;&#039; to target high precision only where needed. For example, run most of the model in FP16, but keep a few sensitive layers in FP32. This yields most of the performance benefits while ensuring critical parts are accurate. We already saw softmax as one example. Another might be final output layer: sometimes using FP32 for the logits and softmax can be safer.&lt;br /&gt;
&lt;br /&gt;
Hardware is also trending to support mixed modes – like FP8 Tensor Cores accumulate in FP32, etc. So hardware tries to give accuracy cheaply.&lt;br /&gt;
&lt;br /&gt;
=== 7.2. Hardware Accelerators and Tensor Cores ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Hardware accelerators&#039;&#039;&#039; (GPUs, TPUs, FPGAs, ASICs like GraphCore IPU, etc.) have specific support for certain precisions. Leveraging these can provide massive speedups:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;NVIDIA Tensor Cores:&#039;&#039;&#039; These started in Volta (V100) with support for 4x4 matrix multiply-adds on FP16 inputs with FP32 accumulation&amp;lt;ref name=&amp;quot;ref6&amp;quot;/&amp;gt;. Using Tensor Cores can give up to 8x throughput compared to using regular FP32 cores. But to use them, the data needs to be FP16 (or now BF16 or FP8 on newer GPUs) and aligned to certain dimensions (multiples of 8 etc. in matrix sizes, which frameworks handle by padding if needed). If an ONNX model is all FP32, by default it would not use Tensor Cores unless the runtime automatically converts some ops to mixed precision (which some do via a flag or environment variable enabling TF32 or FP16). The introduction of &#039;&#039;&#039;TensorFloat-32 (TF32)&#039;&#039;&#039; in Ampere GPUs is essentially a compromise: treat FP32 inputs as two 16-bit significand halves (10-bit mantissa actually) so that it can run on Tensor Cores, giving ~8x speed, while still outputting FP32. TF32 has about the precision of FP16 but range of FP32, and it was chosen because it typically doesn’t hurt convnet accuracy. ONNX Runtime or others on Ampere will use TF32 by default for conv and matmul unless disabled, giving a free speed boost at minimal accuracy cost. If exact FP32 is needed, users have to turn it off (sacrificing speed).&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;TPU (Google):&#039;&#039;&#039; TPUs have native support for BF16 matrix ops with FP32 accumulators. They were designed specifically for the mixed precision approach in training. For inference, TPUs also support int8 and other quantized ops (e.g., via Edge TPU for lower-power). If exporting ONNX to run on a TPU (via some bridge), one might get automatic BF16 usage. BF16 has the advantage of no loss in range, so TPUs can more straightforwardly run models in BF16 without modifications.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;FPGAs and ASICs:&#039;&#039;&#039; Some custom accelerators might use fixed-point or block floating-point arithmetic for efficiency. They often treat the network in quantized terms. The ONNX quantization specification (QuantizeLinear/DequantizeLinear ops) exists for representing quantized models (INT8 etc.), which is another domain beyond floating-point, but it overlaps as a precision issue. In those systems, accumulators for INT8 × INT8 multiplies are usually 32-bit (to avoid overflow for dot products up to certain lengths). The trade-off there is between integer vs float representation, which can give big speedups on CPUs/DSAs via SIMD instructions (many CPUs have vector int8 multiply-add instructions now for inference).&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Memory bandwidth on hardware:&#039;&#039;&#039; Some accelerators include special high-bandwidth memory or cache specifically for lower precision. For example, an accelerator might fetch two FP16 values in one 32-bit memory transaction – effectively doubling memory bandwidth for half precision. On some hardware, using FP16 can even reduce memory &#039;&#039;latency&#039;&#039; if multiple values can be packed.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Parallelism and Vectorization:&#039;&#039;&#039; Many hardware units process data in fixed vector widths (e.g., 256-bit registers). That could be 8 floats at once or 16 half-floats at once in the same 256 bits. If your code is in FP16, you might use the full vector width (16 values) whereas FP32 might only use half (8 values). This means FP16 can make better use of vector units. Similarly, matrix units often have fixed tile sizes, and smaller data types allow larger tiles or more tiles concurrently.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Energy efficiency:&#039;&#039;&#039; Lower precision often means less energy per operation, which on large deployments (data centers or battery-powered devices) is a significant advantage. This isn&#039;t directly performance, but it&#039;s an important trade: you might choose a slightly lower precision to save power even if accuracy drops a tiny bit.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Software overhead:&#039;&#039;&#039; Using mixed precision on hardware involves some overhead of casting and moving data between precisions. For instance, if you have to frequently cast FP16 to FP32 and back, that adds instructions and memory copies. A well-designed accelerator (like GPUs with unified memory for FP16/32 or special hardware casting) minimize this. Still, the runtime might insert extra operations (as we saw, ONNX Runtime CPU inserted many Cast nodes converting FP16 to FP32 and back&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;, which actually &#039;&#039;slowed down&#039;&#039; that FP16 model by 4x vs FP32 on a CPU, because the CPU had no native FP16). So ironically, on some hardware, using lower precision in ONNX might degrade performance if not supported (the CPU case). Always ensure the target hardware actually supports the low precision you plan to use; otherwise, you&#039;ll pay overhead for emulation.&lt;br /&gt;
&lt;br /&gt;
To illustrate trade-off: Suppose you have a model that achieves 99% accuracy in FP32 and drops to 98.5% in FP16. If FP16 runs 2x faster, many would accept the 0.5% accuracy drop for double throughput. If int8 runs 4x faster but accuracy drops to 95%, that might be too much drop for some applications, but acceptable for others that value speed. It&#039;s application-dependent.&lt;br /&gt;
&lt;br /&gt;
In critical applications, one might maintain higher precision accumulators or run certain validation steps in high precision to ensure nothing drifted. In research, sometimes a model is trained in mixed precision but validated in double to ensure any small numeric differences don’t influence the reported result (rare, but for scientific computing integration maybe).&lt;br /&gt;
&lt;br /&gt;
In the ONNX world, performance-vs-accuracy is often managed via &#039;&#039;&#039;Execution Providers&#039;&#039;&#039; (EPs) that target specific hardware. For example, the TensorRT EP in ONNX Runtime can take an ONNX model and run it using NVIDIA&#039;s high-performance inference library, which will use FP16 or INT8 if allowed, to maximize speed. The user can configure the EP with precision flags. If the accuracy drop is too high, one might restrict it to FP32. If it&#039;s fine, allow FP16. Tools exist to measure the accuracy difference (like calibrating int8 quantization).&lt;br /&gt;
&lt;br /&gt;
To conclude, the trade-offs revolve around picking the lowest precision that yields acceptable accuracy, and utilizing hardware features fully. ONNX being a high-level spec abstracts this, but it&#039;s ultimately up to the deployment to decide and sometimes up to the exporter to incorporate quantization. ONNX’s role is to be able to &#039;&#039;represent&#039;&#039; these choices (they have added quantized types, FP8 types, etc., to allow the graph to include those when needed).&lt;br /&gt;
&lt;br /&gt;
== Best Practices and Design Guidelines ==&lt;br /&gt;
&lt;br /&gt;
Given the above insights, here are some best practices and guidelines for dealing with numerical precision and accumulators in AI inference (and training) design:&lt;br /&gt;
&lt;br /&gt;
=== 8.1. Choosing an Accumulator Precision ===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Match precision to data length and range:&#039;&#039;&#039; If an operation accumulates a large number of terms, consider using a higher precision accumulator than the input data. For example, for summing &amp;gt;1000 values of type FP16, an FP32 accumulator is recommended to avoid large rounding error or overflow. If summing &amp;gt; million values of FP32, consider double (FP64) accumulation if high accuracy is needed (though this is rare in DL). &#039;&#039;Rule of thumb:&#039;&#039; if &amp;lt;math&amp;gt;N \cdot \varepsilon&amp;lt;/math&amp;gt; is not tiny (say &amp;gt;1e-3), you may benefit from higher precision. For FP16 (&#039;&#039;ε ≈ 1e-3&#039;&#039;), that threshold N is a few hundred. For FP32 (&#039;&#039;ε ≈ 1e-7&#039;&#039;), N would be in the millions before 1e-3 error.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Use higher precision in critical layers:&#039;&#039;&#039; Identify if certain layers of the model are more sensitive to precision. For instance, the output layer or a layer that feeds into a very sharp decision boundary might require more precision. If a small change there could flip the prediction, you don&#039;t want excessive numerical error. Using FP32 for logits and softmax is a common practice to maintain classification confidence stability.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Follow hardware defaults, but override if needed:&#039;&#039;&#039; Many hardware accelerators have sensible defaults (like accumulate in FP32 for tensor ops). It&#039;s usually best to use those defaults as they were chosen to balance accuracy and speed. Only override if you have evidence of a problem. For example, if using ONNX Runtime on CPU, you discovered it upcasts to float for you – great. But if using some custom accelerator that does pure FP16, you might manually insert casts in the ONNX graph around certain ops to force higher precision on those ops.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Maintain precision for gradient accumulation (training):&#039;&#039;&#039; This is more training-oriented, but worth noting: when summing gradients over batches or updating weights, use higher precision (FP32 or even 64) for those accumulations. This ensures training doesn&#039;t diverge due to numeric error. Many frameworks keep a &amp;quot;master copy&amp;quot; of weights in FP32 when training in FP16&amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;. For inference, this translates to: any running statistics or counters in the model (like if you had a running average inside the model) should probably be in higher precision to avoid drift.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Be mindful of intermediate cast costs:&#039;&#039;&#039; If you do decide to cast to higher precision for an operation in ONNX, remember it incurs memory copies and compute overhead. Make sure it&#039;s worth it. For example, if you cast a huge tensor to double, that could slow things drastically. Usually FP16-&amp;gt;FP32 is fine (hardware handles it well), but FP32-&amp;gt;FP64 might be expensive on GPUs.&lt;br /&gt;
&lt;br /&gt;
=== 8.2. Safe Reduction Lengths and Heuristics ===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Chunk long reductions:&#039;&#039;&#039; If you must do a very long sum in low precision (due to memory constraints or hardware), chunk the sum into parts, sum each to a higher precision local accumulator, then sum the partial sums. This is essentially pairwise summation. Many libraries use a fixed chunk size heuristic (e.g., sum in blocks of 256 in registers, then add those up). This bounds the error growth. If implementing manually, you can choose a chunk size such that chunk_length &#039;&#039; ε is small (like 256 &#039;&#039; 1e-3 = 0.256 for FP16, hmm maybe choose 100).&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Sort by magnitude if possible:&#039;&#039;&#039; In some cases (not typical in DL, but possible in post-processing), if you have numbers varying by orders of magnitude, summing from smallest to largest improves accuracy. It prevents small ones from being swallowed by big ones. This is more relevant if you output a sum of mixed-scale values (like some algorithm mixing big and small contributions). It’s not always feasible in real-time inference to sort, but keep in mind.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Use compensation for critical sums:&#039;&#039;&#039; If you find a particular sum or difference in your inference computation is crucial (like computing a small residual from two close numbers), consider using Kahan summation or another compensated scheme just for that part. For example, if you had an ONNX custom function that needs to sum a lot of almost cancelling terms, you might implement it with compensation internally.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Heuristics for FP8/low-bit:&#039;&#039;&#039; When using extremely low precision like FP8, adopt the recommended practices: use scaling (don&#039;t feed raw data into FP8, normalize it first), and perhaps only use FP8 on portions of the network that are empirically robust. The industry is still establishing heuristics, but one might be: use FP8 for matrix multiplications, but accumulate in FP16 or FP32; keep normalization in FP16; use FP16 for first and last layer maybe. This is evolving, so keep an eye on literature.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Keep an eye on sum length in softmax and normalization:&#039;&#039;&#039; If you have extremely large class counts or group sizes (e.g., a softmax over 1e6 items, or layer norm over tens of thousands of features), realize this is a giant reduction. You may want to ensure that reduction is done in higher precision. ONNX Softmax by default will likely not handle a million-class case in FP16 well (the sum would overflow since 1e6 &amp;gt; 65504). So either do that on CPU in FP32 or break it down (could do hierarchical softmax, etc.). This is a corner case, but relevant for huge vocab language models. In practice, vocab ~50k is fine as we said.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Test numeric differences:&#039;&#039;&#039; A practical tip: after quantizing or reducing precision, run some test inputs through both the high-precision model and low-precision model. Compare outputs (and maybe some intermediate signals if possible). If differences are consistently small relative to the application tolerance, you&#039;re good. If you see occasional large differences or instabilities (like outputs become NaN or Inf in low precision), that&#039;s a red flag. Use that testing to guide where you might need to increase precision or change algorithms.&lt;br /&gt;
&lt;br /&gt;
=== 8.3. When Higher Precision Is Required ===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Ill-conditioned calculations:&#039;&#039;&#039; If your model or pipeline performs any operation known to be ill-conditioned (matrix inversion, subtraction of nearly equal quantities, computing a tiny difference of large numbers), you likely need higher precision for that part. For example, if your ONNX model includes a custom op to solve a linear system, use double precision in that op if possible (or at least float with iterative refinement). In ML, this might occur in something like a physics-informed neural network or a differentiable simulation within the model.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Strict accuracy requirements:&#039;&#039;&#039; Some applications (medical, aerospace, finance) might have strict tolerances. If an inference model’s output needs to be correct to 5 decimal places, you probably cannot use FP16, which has only 3-4 decimal digits precision. FP32 might be barely enough (~7 digits). FP64 might be warranted to guarantee that level of accuracy in worst case. Most deep learning applications (like image classification) don&#039;t demand that level of numeric precision in outputs – as long as the top class is correct, it doesn&#039;t matter if its probability was 0.901 vs 0.899. But if you&#039;re doing something like summing probabilities to make a risk estimate or doing a cumulative sum over many predictions, be careful to accumulate precisely.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Large sums or products in post-processing:&#039;&#039;&#039; Sometimes the neural net is fine, but the &#039;&#039;post-processing&#039;&#039; of results might need precision. E.g., summing up probabilities of many events, or computing confidence intervals, etc. Do those calculations in double if needed outside the model.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Preventing drift in long sequences:&#039;&#039;&#039; For sequential models (RNNs processing thousands of steps), rounding error can accumulate over steps. If each step has a small error, after 10000 steps it could add up. In such cases, using a higher precision for the state might reduce drift. Some practitioners use FP64 for the hidden state accumulation in very long sequence processing to maintain numerical stability (though this is not common due to speed loss). Alternatively, periodically renormalize or reset small errors.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Simulations or differential equations:&#039;&#039;&#039; If your ONNX model involves simulating something (like a differential equation solver inside a model, or an ODE/RNN hybrid), those often need double precision to be stable for long durations. Recognize those situations and allocate precision accordingly.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Verification and debugging:&#039;&#039;&#039; Use higher precision as a tool to verify correctness. For example, run your model in FP64 (if possible) and compare to FP32 to see if there are differences. If not, FP32 is fine. If yes, you might find a particular calculation losing too much info in FP32, implying maybe you should keep it in FP64 in production as well. This is rare in inference but can be part of rigorous validation.&lt;br /&gt;
&lt;br /&gt;
In ONNX, using higher precision may involve casting to &amp;lt;code&amp;gt;tensor(double)&amp;lt;/code&amp;gt; for certain ops. Not all runtimes support double on GPU though (some do via emulation or just slower path). ONNX CPU does support double for general ops. So be mindful of the target.&lt;br /&gt;
&lt;br /&gt;
Finally, &#039;&#039;&#039;document your precision choices&#039;&#039;&#039;. If you deliver an ONNX model that uses mixed types (float16 inputs, float32 accumulations, int8 weights, etc.), ensure that is clear to whoever maintains or uses it. Precision issues can be sneaky, so clarity helps.&lt;br /&gt;
&lt;br /&gt;
== Limitations and Open Problems ==&lt;br /&gt;
&lt;br /&gt;
Despite advances in handling numerical precision, there remain limitations and open challenges in this domain:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Lack of Precision Specification in ONNX:&#039;&#039;&#039; As noted, ONNX currently has no way to formally specify that an op &#039;&#039;should&#039;&#039; use a higher precision internally. This can lead to nondeterministic differences across platforms&amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;. While proposals exist to add attributes for this&amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;, it is not yet standard. Thus an ONNX model might behave slightly differently on two compliant backends, and there&#039;s no built-in mechanism to ensure identical results. This is a limitation for applications requiring cross-platform consistency. Solving it may involve extending ONNX (e.g., an attribute like &amp;lt;code&amp;gt;accumulatePrecision=&amp;quot;FLOAT32&amp;quot;&amp;lt;/code&amp;gt; for MatMul) or tightening the spec, but those come with backward compatibility and implementation burden concerns.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Reproducibility and Determinism:&#039;&#039;&#039; Even with the same precision, parallel computation can introduce nondeterminism (due to different summation orders). Ensuring bit-identical results across runs (important for some sensitive domains) is non-trivial. Frameworks often provide &amp;quot;deterministic mode&amp;quot; which might use fixed ordering at cost of speed. In ONNX, there&#039;s no global flag for this; it&#039;s up to the runtime. More research could be done on algorithms that produce bit-wise identical results independent of parallel execution order (perhaps using techniques like pairwise summation or consistent partitioning). Stochastic rounding is another area: sometimes using probabilistic rounding can reduce bias in accumulated error, but it introduces randomness – reconciling that with reproducibility is tricky.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Ultra-low Precision (FP8 and below):&#039;&#039;&#039; While FP8 is being explored, going to 4-bit floats or mixed schemes is an open challenge. The OpenAI community is investigating 4-bit (quantization-wise) for LLMs with some success, but a true 4-bit float (like E3M1) would be extremely limited. Perhaps hybrid schemes (like block floating point, log scales, etc.) will come. ONNX already has some support (they mention 4-bit in documentation placeholders&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;), but it&#039;s nascent. Research into training techniques, calibration, and new number formats (like &#039;&#039;&#039;posit numbers&#039;&#039;&#039; or tapering precision floats) is ongoing. If a new format proves useful, ONNX will need to adapt to represent it.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Quantization Noise and Model Robustness:&#039;&#039;&#039; Reducing precision can sometimes expose models to issues like adversarial vulnerability or fragility – e.g., a slight perturbation might flip a decision if the margin was small and precision loss nudged it. Ensuring model robustness under quantization is an open problem. It might require re-training the model with quantization in the loop (quantization-aware training) or designing models that have large &amp;quot;margin&amp;quot; between classes to tolerate quantization noise.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Dynamic range during training vs inference:&#039;&#039;&#039; In training, activations can have wider distributions (especially early in training). Models often learn to constrain their ranges (via activation functions saturating or normalization), which makes inference more stable in lower precision. But if one tries to train in FP8 from scratch, it&#039;s very hard. This gap between what&#039;s possible in a post-trained inference model vs training is an open area – how to train directly in low precision without significant accuracy loss. Solving that could simplify workflows (no need for high-precision training then quantize; just train low-prec end to end). Techniques like loss scaling, gradient clipping, etc., are partial solutions. The development of optimizers that are resilient to low precision is ongoing.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Hardware variability:&#039;&#039;&#039; As new hardware emerges (with new numeric capabilities or quirks), ensuring that ONNX models run consistently is an ongoing task. For instance, one device might implement a fused op that reduces rounding error, another does it separately. Even something like convolution might use a different algorithm (FFT-based vs direct) which has different numeric properties. Open problem: could ONNX allow specifying numeric tolerances per op or preferred algorithms? That may be overkill for most, but for some HPC ML use cases, maybe desired.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Testing and validation of numerics:&#039;&#039;&#039; There isn&#039;t a standardized set of &amp;quot;numeric compliance&amp;quot; tests for ONNX runtimes beyond simple correctness. Perhaps in the future, test suites might include challenging numeric scenarios (large sums, nearly singular matrices) to ensure a runtime handles them gracefully (maybe by switching precision). Right now, it&#039;s mostly on the user to test their model under expected conditions.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Combining quantization with floating point:&#039;&#039;&#039; Many production models use a mix (e.g., int8 for some layers, FP16 for others). ONNX can represent this (with QuantizeLinear ops or MixedPrecision through casting), but tooling to automatically find the optimal mix is still developing. It&#039;s somewhat trial-and-error or using heuristics. An open question is how to automate precision assignment: given a model and a target hardware, how to automatically choose which layers can be int8, which should be FP16, etc., to meet an accuracy target. This crosses into NAS (neural architecture search) territory or automated quantization. &lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Numerical debugging tools:&#039;&#039;&#039; When a model goes wrong due to precision, it&#039;s often hard to pinpoint where. Better tooling to analyze numerical error layer by layer could be useful. For instance, one could run a model in double and float and diff the intermediate results to see where the largest divergence happens. Doing this systematically for big nets is an open tooling challenge. Some frameworks have hooks for this, but not standardized.&lt;br /&gt;
&lt;br /&gt;
In conclusion, while we&#039;ve come far with using lower precision in AI without sacrificing much accuracy, the landscape is always evolving. The key open problem is how to push precision lower (for efficiency gains) without crossing the threshold where accuracy falls off a cliff. Each new generation of hardware and techniques (like FP8, novel quantization schemes) extends that boundary, but also brings new questions on how to manage the numerical errors. ONNX as an interchange will need to keep pace, possibly by adding more expressive power for these numeric considerations.&lt;br /&gt;
&lt;br /&gt;
== See Also ==&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;IEEE 754 Floating-Point&#039;&#039;&#039; – The standard defining binary16, binary32, binary64 formats and rounding behavior.&lt;br /&gt;
* &#039;&#039;&#039;Floating-Point Arithmetic&#039;&#039;&#039; – General topic on how floating-point math works, common issues (overflow, underflow, cancellation).&lt;br /&gt;
* &#039;&#039;&#039;Kahan Summation Algorithm&#039;&#039;&#039; – Algorithm for compensated summation to reduce numerical error&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;.&lt;br /&gt;
* &#039;&#039;&#039;BFloat16&#039;&#039;&#039; – A short-format float with 8-bit exponent, used in TPUs and some GPUs for training networks.&lt;br /&gt;
* &#039;&#039;&#039;Mixed Precision Training&#039;&#039;&#039; – Technique of using FP16 (or BF16) along with FP32 accumulators to accelerate training&amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;.&lt;br /&gt;
* &#039;&#039;&#039;Quantization in Deep Learning&#039;&#039;&#039; – Converting networks to lower bit integer arithmetic for inference (INT8 quantization and calibration techniques).&lt;br /&gt;
* &#039;&#039;&#039;ONNX Operator Specification&#039;&#039;&#039; – Official docs for ONNX ops, which describe their mathematical function but not the required numeric precision (a point of potential extension in future).&lt;br /&gt;
* &#039;&#039;&#039;CUDA Tensor Cores &amp;amp; TF32&#039;&#039;&#039; – NVIDIA documentation on how tensor cores perform mixed precision and the TF32 format for accelerating FP32.&lt;br /&gt;
* &#039;&#039;&#039;Pairwise Summation&#039;&#039;&#039; – Summation algorithm with logN error growth&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;, used in many numerical libraries to improve accuracy of reductions.&lt;br /&gt;
* &#039;&#039;&#039;Softmax Stability&#039;&#039;&#039; – Articles on numerically stable softmax and log-sum-exp implementations to avoid overflow.&lt;br /&gt;
* &#039;&#039;&#039;Performance Tuning for Low Precision&#039;&#039;&#039; – Guides (e.g., NVIDIA or Intel whitepapers) on using low precision and the trade-offs involved in various hardware.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;references&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref1&amp;quot;&amp;gt;Accelerate and simplify Scikit-learn model inference with ONNX Runtime - Microsoft Open Source Blog — opensource.microsoft.com — https://opensource.microsoft.com/blog/2020/12/17/accelerate-simplify-scikit-learn-model-inference-onnx-runtime&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref2&amp;quot;&amp;gt;Floating Point Precision: Understanding FP64, FP32, and FP16 in Large Language Models - DEV Community — dev.to — https://dev.to/lukehinds/floating-point-precision-understanding-fp64-fp32-and-fp16-in-large-language-models-3gk6&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref3&amp;quot;&amp;gt;Floating Point Precision and Its Limitations | by Umair Akbar | Medium — akbu.medium.com — https://akbu.medium.com/floating-point-precision-and-its-limitations-cfb7247d7789&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref4&amp;quot;&amp;gt;Kahan summation algorithm - Wikipedia — en.wikipedia.org — https://en.wikipedia.org/wiki/Kahan_summation_algorithm&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref5&amp;quot;&amp;gt;Pairwise summation - Wikipedia — en.wikipedia.org — https://en.wikipedia.org/wiki/Pairwise_summation&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref6&amp;quot;&amp;gt;Numerical behavior of NVIDIA tensor cores - PMC — pmc.ncbi.nlm.nih.gov — https://pmc.ncbi.nlm.nih.gov/articles/PMC7959640/&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref7&amp;quot;&amp;gt;Mixed precision and performance-accuracy tuning (neuron-cc) — AWS Neuron Documentation — awsdocs-neuron.readthedocs-hosted.com — https://awsdocs-neuron.readthedocs-hosted.com/en/latest/about-neuron/appnotes/neuron-cc/mixed-precision.html&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref8&amp;quot;&amp;gt;Create Float16 and Mixed Precision Models - ONNX Runtime — onnxruntime.ai — https://onnxruntime.ai/docs/performance/model-optimizations/float16.html&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref9&amp;quot;&amp;gt;[Performance] ONNX FP16 model is having performance bottle neck when compared to FP32 variant · Issue #25824 · microsoft/onnxruntime · GitHub — github.com — https://github.com/microsoft/onnxruntime/issues/25824&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref10&amp;quot;&amp;gt;Float stored in 8 bits - ONNX 1.21.0 documentation — onnx.ai — https://onnx.ai/onnx/technical/float8.html&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref11&amp;quot;&amp;gt;[2209.05433] FP8 Formats for Deep Learning — arxiv.org — https://arxiv.org/abs/2209.05433&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref12&amp;quot;&amp;gt;Should MatMul op allow specification of accumulation precision? · Issue #7072 · onnx/onnx · GitHub — github.com — https://github.com/onnx/onnx/issues/7072&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref13&amp;quot;&amp;gt;Quantize ONNX models | onnxruntime — onnxruntime.ai — https://onnxruntime.ai/docs/performance/model-optimizations/quantization.html&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref14&amp;quot;&amp;gt;Keep Attention Softmax FP32 during FP16/ZeRO Training · Issue #1485 · hpcaitech/ColossalAI · GitHub — github.com — https://github.com/hpcaitech/ColossalAI/issues/1485&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;/references&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 5.3. Backend and Hardware-Dependent Behavior ===&lt;br /&gt;
&lt;br /&gt;
Because ONNX is implemented by many backends (ONNX Runtime, TensorRT, CoreML, PyTorch JIT, etc.), and these in turn run on varied hardware (CPU, GPU, accelerators), the numeric behavior can be backend-dependent. Some examples:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;CPU backends:&#039;&#039;&#039; Many CPUs do not have native float16 vector arithmetic (with a few exceptions in recent CPUs). ONNX Runtime’s default CPU execution provider will often upcast float16 to float32, compute, then downcast&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;. This ensures accuracy but means that if you thought you were getting a full float16 pipeline, you’re not – the CPU is essentially ignoring the low precision in compute. On the other hand, x86 CPUs have extended 80-bit precision in x87 registers (though less used now) and 64-bit SIMD registers for doubles; some compiler optimizations might carry intermediate results in higher precision. This could make CPU results slightly different from a GPU’s if, say, a sum is done in 80-bit on CPU versus 32-bit on GPU. Generally, modern compilers avoid such extended precision unless explicitly asked, to improve reproducibility.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;GPU backends:&#039;&#039;&#039; GPUs often perform summations in parallel using tree reductions. The order of operations can change depending on how threads are scheduled. This means even on the same GPU, summing an array in one launch versus another might not give bit-identical results if the internal parallel reduction algorithm differs (though it should be deterministically the same given the same library version and input, but if you change GPU model or BLAS library, it might differ). Also, GPUs have fused multiply-add (FMA) instructions that do &amp;lt;math&amp;gt;(a*b + c)&amp;lt;/math&amp;gt; in one step with one rounding, which can produce slightly different results than doing the multiply and add separately (which would round twice). If an ONNX backend uses a fused kernel (like a GEMM library) it might yield different low-order bits than a simple loop.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Vendor libraries:&#039;&#039;&#039; ONNX Runtime can use vendor BLAS or DNN libraries (MKL, cuDNN, TensorRT, etc.). These libraries often implement their own strategies for numerical stability. For example, NVIDIA’s cuDNN might use FP32 accumulation for FP16 inputs by default. TensorRT (NVIDIA’s inference engine) will sometimes automatically promote precisions or insert scaling to maintain stability&amp;lt;ref name=&amp;quot;ref13&amp;quot;/&amp;gt;. On the other hand, if you explicitly request FP16 in TensorRT with no FP32 fallback, it might do the entire op in FP16 and you’d see more error. Some libraries offer knobs: TensorRT allows choosing FP32, FP16, or INT8 precision for each layer, and will use FP32 internally for safety in some cases unless forced otherwise.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Hardware-specific quirks:&#039;&#039;&#039; As another example, NVIDIA’s &#039;&#039;&#039;TensorFloat-32 (TF32)&#039;&#039;&#039; on Ampere GPUs is a precision mode where matrix multiplications use 10-bit mantissa (so about FP16 precision for the multiply) but keep 8-bit exponent (range like FP32). By default, CUDA libraries use TF32 for FP32 matrix multiplies on Ampere to speed them up, unless you disable it. This means if you run an ONNX model with FP32 on an A100 GPU, your matmuls might actually be lower precision than FP32 (though higher than FP16)&amp;lt;ref name=&amp;quot;ref6&amp;quot;/&amp;gt;. It’s an example of hardware choosing an intermediate precision for performance. From ONNX’s perspective, the output is still FP32 and (hopefully) within error tolerances, but a user might be surprised that it’s not a “true” FP32 computation internally. If exactness is needed, one has to turn off TF32.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Non-determinism:&#039;&#039;&#039; Some backends (especially on GPUs) allow non-deterministic execution for performance. For instance, atomic adds in parallel reductions can produce results that vary run-to-run because the order of accumulation is race-condition-dependent. ONNX’s spec doesn’t cover this explicitly, but it’s something to consider. In ONNX Runtime, there are settings for deterministic computing, but by default it may use all available performance optimizations, even if results vary at the 1e-7 level.&lt;br /&gt;
&lt;br /&gt;
In short, the numerical behavior under ONNX can depend on the combination of backend software and hardware capabilities. The lack of a strict spec for accumulation means implementations have freedom to use clever strategies (higher precision, reordering) to improve accuracy or speed. Usually this is positive (e.g. getting better accuracy than a naive approach). But it could also lead to slight inconsistencies. For example, you might find that exporting a PyTorch model to ONNX and running it with one engine yields outputs slightly different from PyTorch’s – not because ONNX is wrong, but because the summation or convolution was done differently (maybe PyTorch summed in FP32 as well, but maybe not in exactly the same way).&lt;br /&gt;
&lt;br /&gt;
The key takeaway is that ONNX &#039;&#039;by design&#039;&#039; abstracts away these details, which is good for portability but means you need to &#039;&#039;&#039;trust the backend&#039;&#039;&#039; or verify its precision behavior for sensitive models. If necessary, you can enforce certain behavior by inserting ops (for example, if you want to ensure FP32 accumulation, you could insert a cast to FP32 for inputs of a MatMul and cast the result back to FP16 – this way the ONNX graph itself mandates FP32 compute in between). This of course sacrifices some performance or device-specific magic.&lt;br /&gt;
&lt;br /&gt;
Looking forward, as mentioned, there are proposals to enrich ONNX’s specification regarding precision. This might include explicit flags for “exact summation” or “accumulate in int32” etc., which would make models more self-descriptive but also potentially less portable (if a backend doesn’t support that, what then?). For now, users should be aware of these backend differences. Most of the time, well-engineered ONNX runtimes yield results close enough that they don&#039;t affect the application (e.g., classification top-1 remains the same even if low-level sums differ by 1e-5). But for high-stakes numerical computations, testing and maybe adjusting the model or backend settings is prudent.&lt;br /&gt;
&lt;br /&gt;
== Numerical Stability in Deep Learning Models ==&lt;br /&gt;
&lt;br /&gt;
Deep learning computations involve many operations that, while theoretically well-defined, can be sensitive to numerical issues when implemented in finite precision. We&#039;ll discuss a few common scenarios in neural networks where numerical stability is a concern, and how precision and accumulation play a role.&lt;br /&gt;
&lt;br /&gt;
=== 6.1. Dot Products and Large-Scale Reductions ===&lt;br /&gt;
&lt;br /&gt;
Neural networks are full of dot products: every fully-connected layer computes &amp;lt;math&amp;gt;\mathbf{y} = W\mathbf{x}&amp;lt;/math&amp;gt;, which is a series of inner products between weight vectors and the input. Convolutional layers compute weighted sums of local regions. Attention mechanisms compute large matrix products of query/key/value matrices. All these boil down to summing a lot of terms: &amp;lt;math&amp;gt;\sum_{k=1}^K a_k b_k&amp;lt;/math&amp;gt;. When &amp;lt;math&amp;gt;K&amp;lt;/math&amp;gt; is large (it could be in the thousands or more for a single neuron in a fully connected layer of a large model), the potential for floating-point error in that sum increases.&lt;br /&gt;
&lt;br /&gt;
If using FP32, the error may still be negligible in many cases, but in lower precision like FP16, it can become significant. For example, consider a dot product of length 2048 in FP16. Each product is in FP16 (with ~3 decimal digits precision) and if accumulated in FP16, the worst-case error could be on the order of &amp;lt;math&amp;gt;2048 \times \varepsilon_{FP16} \approx 2048 \times 0.0009766 \approx 2&amp;lt;/math&amp;gt; (i.e., you could lose a couple units in the third decimal place relative accuracy in worst case). Even in random error accumulation, you might see error on the order of &amp;lt;math&amp;gt;\sqrt{2048} \times 0.0009766 \approx 0.044&amp;lt;/math&amp;gt; or 4.4% (though this is a rough estimate). That’s quite significant. Fortunately, as discussed, most hardware will accumulate such sums in a higher precision like FP32, dramatically reducing that error (~7 orders of magnitude smaller &amp;lt;math&amp;gt;\varepsilon&amp;lt;/math&amp;gt;). But if it didn’t, the network’s outputs could get noisy. A fully-connected layer with many inputs might output slightly different results depending on summation order if done in low precision.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Large-scale reductions&#039;&#039;&#039; also occur in operations like global average pooling (summing all elements of a feature map) or computing the total loss by summing loss over batch samples. If one sums 100,000 values in FP32, the worst-case error could be on the order of 100,000 &#039;&#039; 1e-7 ≈ 1e-2 (1% uncertainty), though typical error would be much less. In FP16, summing 100k values is likely impossible without overflow: as noted, adding ~10^4 moderate numbers can overflow FP16&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. So large reductions &#039;&#039;must* be handled carefully (either in chunks or in higher precision). Frameworks generally do: for example, when performing a sum reduction on GPU, libraries break it into parallel blocks that accumulate in registers (usually 32-bit registers, even if inputs are 16-bit). &lt;br /&gt;
&lt;br /&gt;
Another aspect is that many dot products in neural nets involve values of varying magnitude. If the distribution of terms is such that some are much larger than others, the smaller terms might not contribute much if summed last. Ideally, one might sort by magnitude or use pairwise summation to improve that. Most BLAS libraries effectively do a pairwise strategy due to their parallel structure. So, while worst-case analyses are grim, practical implementations mitigate a lot of it.&lt;br /&gt;
&lt;br /&gt;
However, certain layers can suffer if not handled properly. One famous example is in RNNs or LSTMs: if one uses FP16 naive summation for the gating mechanism, errors could accumulate over many time steps and cause drift or instability. In modern practice, recurrent layers are often run in FP16 with FP32 accumulators to avoid this.&lt;br /&gt;
&lt;br /&gt;
In summary, &#039;&#039;&#039;dot products and large reductions need sufficient precision&#039;&#039;&#039; or robust algorithms. If you are implementing a custom kernel (say a custom ONNX op) and you have to sum a huge array, you should consider using a technique from Section 3 (pairwise summation, Kahan, or at least double accumulator if possible). Many deep learning computations, by virtue of hardware and library design, already do something like this under the hood. But it&#039;s always good to be aware, especially if pushing into lower precision territories.&lt;br /&gt;
&lt;br /&gt;
=== 6.2. Cancellation, Dynamic Range, and Conditioning ===&lt;br /&gt;
&lt;br /&gt;
We introduced &#039;&#039;cancellation&#039;&#039; earlier in the context of summation. In deep learning, cancellation can appear in a few places. One is in the computation of certain norms or variances. For instance, Batch Normalization computes mean and variance of activations. The variance formula &amp;lt;math&amp;gt;\mathrm{Var}(x) = E[x^2] - (E[x])^2&amp;lt;/math&amp;gt; is subject to catastrophic cancellation if &amp;lt;math&amp;gt;E[x]&amp;lt;/math&amp;gt; and &amp;lt;math&amp;gt;\sqrt{E[x^2]}&amp;lt;/math&amp;gt; are close in magnitude (which can happen if the variance is small compared to the mean). If implemented naively in single precision, you might get a negative variance due to numerical error (which would then be clamped to zero or sqrt would yield NaN). Robust implementations compute variance in a single pass or increase precision. Indeed, in many frameworks, batchnorm accumulation is done in FP32 even if the activations are FP16, precisely to avoid numerical issues in computing mean and variance. ONNX itself doesn’t specify this, but an ONNX backend is likely to do batchnorm stats in higher precision for safety. The &#039;&#039;conditioning&#039;&#039; of the variance calculation is poor when variance is tiny relative to mean, so it&#039;s a spot where higher precision is needed.&lt;br /&gt;
&lt;br /&gt;
Another example is in certain loss functions or regularizations. If you subtract two large numbers to get a small difference, you lose precision. Say you have two nearly identical predictions and you subtract them as part of some operation – if done in float16, you might get zero whereas float32 would have a slight difference. This could affect, say, a gradient if that difference was supposed to propagate information. Fortunately, neural nets often avoid directly subtracting large nearly equal numbers; but it can happen. In attention mechanisms, the &#039;&#039;score normalization&#039;&#039; (softmax) subtracts a max to avoid huge exponentials, which is good for range but if multiple scores are close to the max, you subtract numbers that are close. However, those subtractions are not catastrophic in the same way; they&#039;re intentional to reduce range and are typically fine in FP32 or even FP16.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Dynamic range&#039;&#039;&#039; issues are everywhere in deep learning. Activation functions like ReLU and exponential can produce very large or very small values. If subsequent computations don’t account for that, you get infinities or zeros. For example, consider a network output that produces a logit of 100 in FP16. &amp;lt;math&amp;gt;\exp(100)&amp;lt;/math&amp;gt; in FP16 overflows to infinity (since FP16 max ~ 6e4 and &amp;lt;math&amp;gt;e^{100} \sim 3.7e43&amp;lt;/math&amp;gt;). If you then do softmax, that one infinity will dominate the sum and you might get a probability of 1 for that class (or NaNs if not handled). The standard softmax implementation subtracts the max logit, so in this case it would subtract 100 from all logits, making the largest exponent &amp;lt;math&amp;gt;\exp(0)=1&amp;lt;/math&amp;gt; and others tiny – this avoids overflow. But what about underflow? If you had a very negative logit, &amp;lt;math&amp;gt;\exp(-100)&amp;lt;/math&amp;gt; in FP16 would underflow to 0. In softmax, that just contributes ~0 probability, which is fine (it effectively is zero anyway). Underflow tends to be less dangerous than overflow in inference — underflow usually just means “this probability/neuron is effectively 0,” whereas overflow can poison the results with NaNs or Infs. &lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Conditioning&#039;&#039;&#039; refers to how sensitive a function is to input perturbations. Neural networks themselves as functions can be poorly conditioned (e.g., tiny changes in weights cause big changes in output for some architectures), but typically they are designed to be reasonably well-behaved. Certain internal computations like matrix inversion (if one does a pseudoinverse or something for a layer) can be extremely ill-conditioned if the matrix is near singular. That again would need double precision perhaps. In ML, one common ill-conditioned operation is the &#039;&#039;&#039;softmax denominator&#039;&#039;&#039;: &amp;lt;math&amp;gt;\sum_j e^{x_j}&amp;lt;/math&amp;gt; can be very large and also very sensitive to changes in the largest &amp;lt;math&amp;gt;x_j&amp;lt;/math&amp;gt;. However, the softmax is more stable if computed carefully (max trick). Another is &#039;&#039;&#039;normalization&#039;&#039;&#039;: computing &amp;lt;math&amp;gt;\frac{x - \mu}{\sigma}&amp;lt;/math&amp;gt; for layer norm or batch norm – if &amp;lt;math&amp;gt;\sigma&amp;lt;/math&amp;gt; is very small, then the division blows up noise. In those cases, a small &amp;lt;math&amp;gt;\epsilon&amp;lt;/math&amp;gt; is usually added to &amp;lt;math&amp;gt;\sigma^2&amp;lt;/math&amp;gt; to prevent instability (this is a model-level fix, not a numeric one per se, but it acknowledges numeric limits).&lt;br /&gt;
&lt;br /&gt;
In training, &#039;&#039;&#039;optimizers&#039;&#039;&#039; can suffer from precision issues. For example, the Adam optimizer keeps a moving average of squared gradients; in half precision, these small updates might flush to zero or not change at some point. Some research found that certain adaptive optimizers break in FP16 because the small differences are lost (&amp;quot;Why FP16 training breaks NAdam&amp;quot; etc.). The solution is often to keep the optimizer states in FP32 while doing the bulk of compute in FP16. This again is a form of mixed precision.&lt;br /&gt;
&lt;br /&gt;
From an inference perspective, once the model is trained, the main concerns are avoiding overflow in activations and ensuring summations (like in softmax or reductions) are done with enough precision. The model’s weights are fixed, so we don&#039;t have to worry about cumulative error over iterations as in training, just the forward pass.&lt;br /&gt;
&lt;br /&gt;
=== 6.3. Normalization Layers and Softmax ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Normalization layers&#039;&#039;&#039; (BatchNorm, LayerNorm, etc.) and &#039;&#039;&#039;Softmax&#039;&#039;&#039; are specifically worth examining for numerical stability:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Batch Normalization:&#039;&#039;&#039; During inference, batchnorm applies an affine transform using precomputed mean and variance (from training). It’s basically: &amp;lt;math&amp;gt;y = \frac{x - \mu}{\sqrt{\sigma^2 + \epsilon}} \gamma + \beta&amp;lt;/math&amp;gt;. This is a stable operation as long as &amp;lt;math&amp;gt;\epsilon&amp;lt;/math&amp;gt; is provided (to avoid division by zero). Since &amp;lt;math&amp;gt;\mu, \sigma^2&amp;lt;/math&amp;gt; are constants in inference, it reduces to linear ops which are fine. The main numerical work (computing mean/variance) happened during training or model calibration, and frameworks usually did that in FP32. So batchnorm at inference time is not a source of instability (it’s just a scale and shift of the data).&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Layer Normalization / Group Normalization:&#039;&#039;&#039; These compute mean/var on the fly for each sample (and possibly channel group). In inference, they still need to compute these statistics for each forward pass. If implemented in low precision, the variance calculation can suffer cancellation as mentioned. However, it&#039;s likely that inference engines compute the mean and variance in FP32 even if inputs are FP16 (especially since these are per-sample operations, not huge batch reductions – it&#039;s feasible to do in higher precision). If someone implemented layer norm purely in FP16, they might see issues when the variance is very small. Usually there’s an &amp;lt;math&amp;gt;\epsilon&amp;lt;/math&amp;gt; added which might dominate a tiny variance and thus mitigate catastrophic cancellation (the result might just output something mostly from &amp;lt;math&amp;gt;\epsilon&amp;lt;/math&amp;gt;). In summary, normalization layers are usually fine as long as the implementation uses enough precision for the summation of squares.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Softmax:&#039;&#039;&#039; Softmax is notorious if done improperly. The safe algorithm: subtract the maximum input from all inputs (to bring the largest exponent to 1.0), exponentiate, sum them up, divide each exponent by the sum. The max subtraction handles overflow in &amp;lt;math&amp;gt;\exp()&amp;lt;/math&amp;gt;. But what about the sum? As discussed, if there are &amp;lt;math&amp;gt;N&amp;lt;/math&amp;gt; elements in the softmax, the sum is at most &amp;lt;math&amp;gt;N&amp;lt;/math&amp;gt; (when all inputs equal the max). In classification, &amp;lt;math&amp;gt;N&amp;lt;/math&amp;gt; might be the number of classes. For ImageNet, &amp;lt;math&amp;gt;N=1000&amp;lt;/math&amp;gt;, so worst sum ~1000 (which is fine in FP16, as 1000 &amp;lt;&amp;lt; 65504). For an extreme case like a language model, &amp;lt;math&amp;gt;N&amp;lt;/math&amp;gt; could be 50,000 (vocab size). Sum could be 50,000 in worst case (everyone equal probability). 50,000 is still below 65504, so actually an FP16 sum would &#039;&#039;not&#039;&#039; overflow in that scenario, and the rounding error on a sum of 50k identical terms would be maybe a few ULPs (which at 50k magnitude might be ~0.5 difference worst-case). So maybe softmax summation in FP16 is not catastrophic for output probabilities – you’d get essentially 1.0 after normalization instead of 0.99999 in some case. Usually fine.&lt;br /&gt;
&lt;br /&gt;
:The bigger issue is if one or a few terms dominate the softmax. If one term is much larger, the others underflow to 0 after the max subtraction. That’s fine: you get a one-hot distribution essentially. If the model expected a softer distribution, that could be a problem. But if the logits were really that far apart, even FP32 would give nearly one-hot. So not really a precision issue, more of a model property.&lt;br /&gt;
&lt;br /&gt;
:However, the &#039;&#039;&#039;stability during training&#039;&#039;&#039; is a big issue. The GitHub thread we saw (ColossalAI) mentioned training a 130B model where FP16 softmax in attention caused training to occasionally blow up&amp;lt;ref name=&amp;quot;ref14&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref14&amp;quot;/&amp;gt;. They observed attention scores in some heads ranging up to 1e4 or as low as 1e-3&amp;lt;ref name=&amp;quot;ref14&amp;quot;/&amp;gt;. 1e4 exponent in FP16 is definitely Inf (because &amp;lt;math&amp;gt;\exp(10^4)&amp;lt;/math&amp;gt; is astronomically huge, but even exp(100) is inf in half). The method CogView introduced (Precision Bottleneck Relaxation) involved subtracting the max &#039;&#039;per head&#039;&#039; to reduce range&amp;lt;ref name=&amp;quot;ref14&amp;quot;/&amp;gt;—which is basically the softmax max trick at a finer granularity or something similar. In the end, they found &#039;&#039;&#039;the simplest fix was to compute softmax in FP32&#039;&#039;&#039; for those layers&amp;lt;ref name=&amp;quot;ref14&amp;quot;/&amp;gt;. They reported that it had negligible speed impact but significantly improved stability&amp;lt;ref name=&amp;quot;ref14&amp;quot;/&amp;gt;. This highlights a general principle: certain operations like softmax (and sometimes matrix multiplications in attention) benefit from higher precision, and using FP32 in a few critical spots can save a mixed-precision training from crashing. In inference, using FP32 for softmax is also a safeguard if one is uncertain. It costs little because softmax is not a heavy compute compared to matrix multiplies.&lt;br /&gt;
&lt;br /&gt;
:ONNX does have a Softmax operator; if the input is float16, a backend might do exactly what they described: internally cast to float32 for the softmax computation, then cast output back to float16. Some inference engines (like DeepSpeed’s inference or others) keep attention softmax in FP32 even if rest is FP16, for robustness&amp;lt;ref name=&amp;quot;ref14&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Apart from softmax, other places where exponentials occur are in loss functions like cross-entropy (which uses log-softmax internally), or in activation functions like Sigmoid and Tanh (which are basically scaled logistic functions). Sigmoid(&amp;lt;math&amp;gt;x)=1/(1+e^{-x})&amp;lt;/math&amp;gt;. If &amp;lt;math&amp;gt;x&amp;lt;/math&amp;gt; is moderately large positive, &amp;lt;math&amp;gt;e^{-x}&amp;lt;/math&amp;gt; underflows to 0 in FP16 if &amp;lt;math&amp;gt;x&amp;gt;~11&amp;lt;/math&amp;gt;. That gives a sigmoid output of 1.0 exactly. In FP32, &amp;lt;math&amp;gt;e^{-x}&amp;lt;/math&amp;gt; for &amp;lt;math&amp;gt;x=11&amp;lt;/math&amp;gt; is about &amp;lt;math&amp;gt;5e-5&amp;lt;/math&amp;gt;, not underflow, so sigmoid ~0.99995. So FP16 will saturate some outputs to exactly 0 or 1 sooner than FP32 would. This can slightly change behaviors (e.g., a neuron might go to full saturation a bit early). Usually, this isn’t a huge deal, but it could make gradients zero earlier. In inference, we don’t care about gradients, just the output value. Getting exactly 1.0 vs 0.99995 probably has no practical difference for a classification output. But one should be aware that &#039;&#039;&#039;activation functions can saturate faster&#039;&#039;&#039; in lower precision due to limited range or precision. ReLU has no issue (it’s linear or 0), but exponential-based activations do. In practice, again, frameworks often simply compute those in the given precision and accept the slight difference. If it were a problem, one could do those in FP32 as well.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Summary:&#039;&#039;&#039; Numerical stability in inference is maintained by using robust computation methods: subtracting maxima in softmax/LogSumExp, adding epsilon in denominators for normalization, and using higher precision for summations or critical steps. Most of these are built into the algorithms. ONNX models assume these standard practices (they don&#039;t need to explicitly do them in the graph because the backend inherently will for ops like Softmax). It&#039;s wise when building or exporting models to ONNX to test them in the target precision and ensure there are no surprises (like an output becomes NaN or Inf due to precision). Usually if training was done in mixed precision, the model will have been vetted in that environment already.&lt;br /&gt;
&lt;br /&gt;
== Performance vs. Accuracy Trade-offs ==&lt;br /&gt;
&lt;br /&gt;
There is an inherent trade-off between numerical precision and computational performance (speed, memory usage) in AI systems. Choosing lower precision can dramatically increase throughput and reduce memory, but it may introduce arithmetic errors. Engineers must balance these considerations:&lt;br /&gt;
&lt;br /&gt;
=== 7.1. Throughput, Memory Bandwidth, and Precision ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Throughput (speed)&#039;&#039;&#039; is often higher for lower-precision operations. Modern hardware can execute more low-precision ops in parallel than high-precision ops. For example, an NVIDIA V100 GPU can perform 2× more half-precision FLOPs than single-precision per clock, and newer architectures with Tensor Cores can do 8× more (since a Tensor Core warp operation might perform 64 FP16 FMAs per cycle vs 16 FP32). Similarly, int8 operations can be even faster and more energy efficient. Lower precision numbers also use less memory bandwidth – fetching 16-bit data is twice as fast as 32-bit data from memory (assuming the same bus width), which can relieve memory bottlenecks. Many deep learning workloads are memory-bound, so halving the data size can nearly double throughput if compute is not the limiting factor. This is a huge incentive to use lower precision.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Memory and storage&#039;&#039;&#039;: Using FP16 or int8 weights reduces model size, which is important for deploying large models to memory-limited environments (like edge devices or GPUs with limited VRAM). It also means you can fit larger batch sizes in memory, or more models on a chip, etc. There is also a cascade effect: smaller data means better cache utilization, which further improves performance.&lt;br /&gt;
&lt;br /&gt;
However, &#039;&#039;&#039;accuracy&#039;&#039;&#039; suffers if precision is too low. We have discussed how rounding error and limited range can cause deviations in outputs. The question is: do those deviations matter for the application? Often for neural networks, a small amount of noise or error in intermediate calculations does not change the final prediction. Neural nets are somewhat error-tolerant due to their redundant distributed representations. This is why quantization and FP16 techniques can work at all. The final model accuracy (e.g. top-1 accuracy on ImageNet) might drop only 0.5% or less when using FP16 instead of FP32, or int8 instead of FP16, if done properly. For many use-cases, this loss is acceptable given the speed gains.&lt;br /&gt;
&lt;br /&gt;
If one goes too low (like naive FP8 or int4 without special care), accuracy might drop severely – perhaps making the model unusable. Thus the trade-off: as precision decreases, performance increases but at some point accuracy degrades beyond an acceptable threshold. Engineers aim to find the lowest precision that still preserves accuracy.&lt;br /&gt;
&lt;br /&gt;
There is also the consideration of &#039;&#039;&#039;accumulation strategy&#039;&#039;&#039; affecting performance: for instance, Kahan summation (compensated) is more accurate but ~4× slower than naive summation. If you need that accuracy, you pay a performance cost. In many AI inference scenarios, the slight extra accuracy from something like Kahan isn&#039;t needed, so it’s not used. But in a scenario where you sum millions of elements for a final result that is thresholded, maybe you do need it.&lt;br /&gt;
&lt;br /&gt;
Another dimension is &#039;&#039;&#039;determinism vs speed&#039;&#039;&#039;. Enforcing a deterministic reduction order (for reproducibility) might require disabling some parallelism, which could slow things down. If exact repeatability is critical (like certain scientific or financial applications of neural nets), one might sacrifice some performance to ensure the summation happens the same way every time. ONNX allows both modes depending on the runtime.&lt;br /&gt;
&lt;br /&gt;
From a memory perspective, if precision is higher than needed, you waste memory. For example, storing intermediate activations in FP32 when they could be FP16 uses 2× memory, which might mean lower batch size or more memory traffic. So there&#039;s strong motivation to keep things as low-precision as possible &#039;&#039;except&#039;&#039; where accuracy truly demands.&lt;br /&gt;
&lt;br /&gt;
In summary, the trade-off often manifests as &#039;&#039;diminishing returns&#039;&#039;: FP64 vs FP32 – huge slowdown for usually negligible accuracy gain in ML; FP32 vs FP16 – moderate speedup for usually minor accuracy hit; FP16 vs int8 – another big speedup but maybe more accuracy loss unless calibration is done; int8 vs int4 – speed gain but often big accuracy drop that might not be worth it for current models.&lt;br /&gt;
&lt;br /&gt;
Engineers use techniques like &#039;&#039;&#039;mixed precision&#039;&#039;&#039; to target high precision only where needed. For example, run most of the model in FP16, but keep a few sensitive layers in FP32. This yields most of the performance benefits while ensuring critical parts are accurate. We already saw softmax as one example. Another might be final output layer: sometimes using FP32 for the logits and softmax can be safer.&lt;br /&gt;
&lt;br /&gt;
Hardware is also trending to support mixed modes – like FP8 Tensor Cores accumulate in FP32, etc. So hardware tries to give accuracy cheaply.&lt;br /&gt;
&lt;br /&gt;
=== 7.2. Hardware Accelerators and Tensor Cores ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Hardware accelerators&#039;&#039;&#039; (GPUs, TPUs, FPGAs, ASICs like GraphCore IPU, etc.) have specific support for certain precisions. Leveraging these can provide massive speedups:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;NVIDIA Tensor Cores:&#039;&#039;&#039; These started in Volta (V100) with support for 4x4 matrix multiply-adds on FP16 inputs with FP32 accumulation&amp;lt;ref name=&amp;quot;ref6&amp;quot;/&amp;gt;. Using Tensor Cores can give up to 8x throughput compared to using regular FP32 cores. But to use them, the data needs to be FP16 (or now BF16 or FP8 on newer GPUs) and aligned to certain dimensions (multiples of 8 etc. in matrix sizes, which frameworks handle by padding if needed). If an ONNX model is all FP32, by default it would not use Tensor Cores unless the runtime automatically converts some ops to mixed precision (which some do via a flag or environment variable enabling TF32 or FP16). The introduction of &#039;&#039;&#039;TensorFloat-32 (TF32)&#039;&#039;&#039; in Ampere GPUs is essentially a compromise: treat FP32 inputs as two 16-bit significand halves (10-bit mantissa actually) so that it can run on Tensor Cores, giving ~8x speed, while still outputting FP32. TF32 has about the precision of FP16 but range of FP32, and it was chosen because it typically doesn’t hurt convnet accuracy. ONNX Runtime or others on Ampere will use TF32 by default for conv and matmul unless disabled, giving a free speed boost at minimal accuracy cost. If exact FP32 is needed, users have to turn it off (sacrificing speed).&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;TPU (Google):&#039;&#039;&#039; TPUs have native support for BF16 matrix ops with FP32 accumulators. They were designed specifically for the mixed precision approach in training. For inference, TPUs also support int8 and other quantized ops (e.g., via Edge TPU for lower-power). If exporting ONNX to run on a TPU (via some bridge), one might get automatic BF16 usage. BF16 has the advantage of no loss in range, so TPUs can more straightforwardly run models in BF16 without modifications.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;FPGAs and ASICs:&#039;&#039;&#039; Some custom accelerators might use fixed-point or block floating-point arithmetic for efficiency. They often treat the network in quantized terms. The ONNX quantization specification (QuantizeLinear/DequantizeLinear ops) exists for representing quantized models (INT8 etc.), which is another domain beyond floating-point, but it overlaps as a precision issue. In those systems, accumulators for INT8 × INT8 multiplies are usually 32-bit (to avoid overflow for dot products up to certain lengths). The trade-off there is between integer vs float representation, which can give big speedups on CPUs/DSAs via SIMD instructions (many CPUs have vector int8 multiply-add instructions now for inference).&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Memory bandwidth on hardware:&#039;&#039;&#039; Some accelerators include special high-bandwidth memory or cache specifically for lower precision. For example, an accelerator might fetch two FP16 values in one 32-bit memory transaction – effectively doubling memory bandwidth for half precision. On some hardware, using FP16 can even reduce memory &#039;&#039;latency&#039;&#039; if multiple values can be packed.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Parallelism and Vectorization:&#039;&#039;&#039; Many hardware units process data in fixed vector widths (e.g., 256-bit registers). That could be 8 floats at once or 16 half-floats at once in the same 256 bits. If your code is in FP16, you might use the full vector width (16 values) whereas FP32 might only use half (8 values). This means FP16 can make better use of vector units. Similarly, matrix units often have fixed tile sizes, and smaller data types allow larger tiles or more tiles concurrently.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Energy efficiency:&#039;&#039;&#039; Lower precision often means less energy per operation, which on large deployments (data centers or battery-powered devices) is a significant advantage. This isn&#039;t directly performance, but it&#039;s an important trade: you might choose a slightly lower precision to save power even if accuracy drops a tiny bit.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Software overhead:&#039;&#039;&#039; Using mixed precision on hardware involves some overhead of casting and moving data between precisions. For instance, if you have to frequently cast FP16 to FP32 and back, that adds instructions and memory copies. A well-designed accelerator (like GPUs with unified memory for FP16/32 or special hardware casting) minimize this. Still, the runtime might insert extra operations (as we saw, ONNX Runtime CPU inserted many Cast nodes converting FP16 to FP32 and back&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;, which actually &#039;&#039;slowed down&#039;&#039; that FP16 model by 4x vs FP32 on a CPU, because the CPU had no native FP16). So ironically, on some hardware, using lower precision in ONNX might degrade performance if not supported (the CPU case). Always ensure the target hardware actually supports the low precision you plan to use; otherwise, you&#039;ll pay overhead for emulation.&lt;br /&gt;
&lt;br /&gt;
To illustrate trade-off: Suppose you have a model that achieves 99% accuracy in FP32 and drops to 98.5% in FP16. If FP16 runs 2x faster, many would accept the 0.5% accuracy drop for double throughput. If int8 runs 4x faster but accuracy drops to 95%, that might be too much drop for some applications, but acceptable for others that value speed. It&#039;s application-dependent.&lt;br /&gt;
&lt;br /&gt;
In critical applications, one might maintain higher precision accumulators or run certain validation steps in high precision to ensure nothing drifted. In research, sometimes a model is trained in mixed precision but validated in double to ensure any small numeric differences don’t influence the reported result (rare, but for scientific computing integration maybe).&lt;br /&gt;
&lt;br /&gt;
In the ONNX world, performance-vs-accuracy is often managed via &#039;&#039;&#039;Execution Providers&#039;&#039;&#039; (EPs) that target specific hardware. For example, the TensorRT EP in ONNX Runtime can take an ONNX model and run it using NVIDIA&#039;s high-performance inference library, which will use FP16 or INT8 if allowed, to maximize speed. The user can configure the EP with precision flags. If the accuracy drop is too high, one might restrict it to FP32. If it&#039;s fine, allow FP16. Tools exist to measure the accuracy difference (like calibrating int8 quantization).&lt;br /&gt;
&lt;br /&gt;
To conclude, the trade-offs revolve around picking the lowest precision that yields acceptable accuracy, and utilizing hardware features fully. ONNX being a high-level spec abstracts this, but it&#039;s ultimately up to the deployment to decide and sometimes up to the exporter to incorporate quantization. ONNX’s role is to be able to &#039;&#039;represent&#039;&#039; these choices (they have added quantized types, FP8 types, etc., to allow the graph to include those when needed).&lt;br /&gt;
&lt;br /&gt;
== Best Practices and Design Guidelines ==&lt;br /&gt;
&lt;br /&gt;
Given the above insights, here are some best practices and guidelines for dealing with numerical precision and accumulators in AI inference (and training) design:&lt;br /&gt;
&lt;br /&gt;
=== 8.1. Choosing an Accumulator Precision ===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Match precision to data length and range:&#039;&#039;&#039; If an operation accumulates a large number of terms, consider using a higher precision accumulator than the input data. For example, for summing &amp;gt;1000 values of type FP16, an FP32 accumulator is recommended to avoid large rounding error or overflow. If summing &amp;gt; million values of FP32, consider double (FP64) accumulation if high accuracy is needed (though this is rare in DL). &#039;&#039;Rule of thumb:&#039;&#039; if &amp;lt;math&amp;gt;N \cdot \varepsilon&amp;lt;/math&amp;gt; is not tiny (say &amp;gt;1e-3), you may benefit from higher precision. For FP16 (&amp;lt;math&amp;gt;\varepsilon \approx 1e-3&amp;lt;/math&amp;gt;), that threshold N is a few hundred. For FP32 (&amp;lt;math&amp;gt;\varepsilon \approx 1e-7&amp;lt;/math&amp;gt;), N would be in the millions before 1e-3 error.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Use higher precision in critical layers:&#039;&#039;&#039; Identify if certain layers of the model are more sensitive to precision. For instance, the output layer or a layer that feeds into a very sharp decision boundary might require more precision. If a small change there could flip the prediction, you don&#039;t want excessive numerical error. Using FP32 for logits and softmax is a common practice to maintain classification confidence stability.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Follow hardware defaults, but override if needed:&#039;&#039;&#039; Many hardware accelerators have sensible defaults (like accumulate in FP32 for tensor ops). It&#039;s usually best to use those defaults as they were chosen to balance accuracy and speed. Only override if you have evidence of a problem. For example, if using ONNX Runtime on CPU, you discovered it upcasts to float for you – great. But if using some custom accelerator that does pure FP16, you might manually insert casts in the ONNX graph around certain ops to force higher precision on those ops.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Maintain precision for gradient accumulation (training):&#039;&#039;&#039; This is more training-oriented, but worth noting: when summing gradients over batches or updating weights, use higher precision (FP32 or even 64) for those accumulations. This ensures training doesn&#039;t diverge due to numeric error. Many frameworks keep a &amp;quot;master copy&amp;quot; of weights in FP32 when training in FP16&amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;. For inference, this translates to: any running statistics or counters in the model (like if you had a running average inside the model) should probably be in higher precision to avoid drift.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Be mindful of intermediate cast costs:&#039;&#039;&#039; If you do decide to cast to higher precision for an operation in ONNX, remember it incurs memory copies and compute overhead. Make sure it&#039;s worth it. For example, if you cast a huge tensor to double, that could slow things drastically. Usually FP16-&amp;gt;FP32 is fine (hardware handles it well), but FP32-&amp;gt;FP64 might be expensive on GPUs.&lt;br /&gt;
&lt;br /&gt;
=== 8.2. Safe Reduction Lengths and Heuristics ===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Chunk long reductions:&#039;&#039;&#039; If you must do a very long sum in low precision (due to memory constraints or hardware), chunk the sum into parts, sum each to a higher precision local accumulator, then sum the partial sums. This is essentially pairwise summation. Many libraries use a fixed chunk size heuristic (e.g., sum in blocks of 256 in registers, then add those up). This bounds the error growth. If implementing manually, you can choose a chunk size such that chunk_length &#039;&#039; ε is small (like 256 &#039;&#039; 1e-3 = 0.256 for FP16, hmm maybe choose 100).&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Sort by magnitude if possible:&#039;&#039;&#039; In some cases (not typical in DL, but possible in post-processing), if you have numbers varying by orders of magnitude, summing from smallest to largest improves accuracy. It prevents small ones from being swallowed by big ones. This is more relevant if you output a sum of mixed-scale values (like some algorithm mixing big and small contributions). It’s not always feasible in real-time inference to sort, but keep in mind.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Use compensation for critical sums:&#039;&#039;&#039; If you find a particular sum or difference in your inference computation is crucial (like computing a small residual from two close numbers), consider using Kahan summation or another compensated scheme just for that part. For example, if you had an ONNX custom function that needs to sum a lot of almost cancelling terms, you might implement it with compensation internally.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Heuristics for FP8/low-bit:&#039;&#039;&#039; When using extremely low precision like FP8, adopt the recommended practices: use scaling (don&#039;t feed raw data into FP8, normalize it first), and perhaps only use FP8 on portions of the network that are empirically robust. The industry is still establishing heuristics, but one might be: use FP8 for matrix multiplications, but accumulate in FP16 or FP32; keep normalization in FP16; use FP16 for first and last layer maybe. This is evolving, so keep an eye on literature.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Keep an eye on sum length in softmax and normalization:&#039;&#039;&#039; If you have extremely large class counts or group sizes (e.g., a softmax over 1e6 items, or layer norm over tens of thousands of features), realize this is a giant reduction. You may want to ensure that reduction is done in higher precision. ONNX Softmax by default will likely not handle a million-class case in FP16 well (the sum would overflow since 1e6 &amp;gt; 65504). So either do that on CPU in FP32 or break it down (could do hierarchical softmax, etc.). This is a corner case, but relevant for huge vocab language models. In practice, vocab ~50k is fine as we said.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Test numeric differences:&#039;&#039;&#039; A practical tip: after quantizing or reducing precision, run some test inputs through both the high-precision model and low-precision model. Compare outputs (and maybe some intermediate signals if possible). If differences are consistently small relative to the application tolerance, you&#039;re good. If you see occasional large differences or instabilities (like outputs become NaN or Inf in low precision), that&#039;s a red flag. Use that testing to guide where you might need to increase precision or change algorithms.&lt;br /&gt;
&lt;br /&gt;
=== 8.3. When Higher Precision Is Required ===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Ill-conditioned calculations:&#039;&#039;&#039; If your model or pipeline performs any operation known to be ill-conditioned (matrix inversion, subtraction of nearly equal quantities, computing a tiny difference of large numbers), you likely need higher precision for that part. For example, if your ONNX model includes a custom op to solve a linear system, use double precision in that op if possible (or at least float with iterative refinement). In ML, this might occur in something like a physics-informed neural network or a differentiable simulation within the model.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Strict accuracy requirements:&#039;&#039;&#039; Some applications (medical, aerospace, finance) might have strict tolerances. If an inference model’s output needs to be correct to 5 decimal places, you probably cannot use FP16, which has only 3-4 decimal digits precision. FP32 might be barely enough (~7 digits). FP64 might be warranted to guarantee that level of accuracy in worst case. Most deep learning applications (like image classification) don&#039;t demand that level of numeric precision in outputs – as long as the top class is correct, it doesn&#039;t matter if its probability was 0.901 vs 0.899. But if you&#039;re doing something like summing probabilities to make a risk estimate or doing a cumulative sum over many predictions, be careful to accumulate precisely.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Large sums or products in post-processing:&#039;&#039;&#039; Sometimes the neural net is fine, but the &#039;&#039;post-processing&#039;&#039; of results might need precision. E.g., summing up probabilities of many events, or computing confidence intervals, etc. Do those calculations in double if needed outside the model.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Preventing drift in long sequences:&#039;&#039;&#039; For sequential models (RNNs processing thousands of steps), rounding error can accumulate over steps. If each step has a small error, after 10000 steps it could add up. In such cases, using a higher precision for the state might reduce drift. Some practitioners use FP64 for the hidden state accumulation in very long sequence processing to maintain numerical stability (though this is not common due to speed loss). Alternatively, periodically renormalize or reset small errors.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Simulations or differential equations:&#039;&#039;&#039; If your ONNX model involves simulating something (like a differential equation solver inside a model, or an ODE/RNN hybrid), those often need double precision to be stable for long durations. Recognize those situations and allocate precision accordingly.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Verification and debugging:&#039;&#039;&#039; Use higher precision as a tool to verify correctness. For example, run your model in FP64 (if possible) and compare to FP32 to see if there are differences. If not, FP32 is fine. If yes, you might find a particular calculation losing too much info in FP32, implying maybe you should keep it in FP64 in production as well. This is rare in inference but can be part of rigorous validation.&lt;br /&gt;
&lt;br /&gt;
In ONNX, using higher precision may involve casting to &amp;lt;code&amp;gt;tensor(double)&amp;lt;/code&amp;gt; for certain ops. Not all runtimes support double on GPU though (some do via emulation or just slower path). ONNX CPU does support double for general ops. So be mindful of the target.&lt;br /&gt;
&lt;br /&gt;
Finally, &#039;&#039;&#039;document your precision choices&#039;&#039;&#039;. If you deliver an ONNX model that uses mixed types (float16 inputs, float32 accumulations, int8 weights, etc.), ensure that is clear to whoever maintains or uses it. Precision issues can be sneaky, so clarity helps.&lt;br /&gt;
&lt;br /&gt;
== Limitations and Open Problems ==&lt;br /&gt;
&lt;br /&gt;
Despite advances in handling numerical precision, there remain limitations and open challenges in this domain:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Lack of Precision Specification in ONNX:&#039;&#039;&#039; As noted, ONNX currently has no way to formally specify that an op &#039;&#039;should&#039;&#039; use a higher precision internally. This can lead to nondeterministic differences across platforms&amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;. While proposals exist to add attributes for this&amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;, it is not yet standard. Thus an ONNX model might behave slightly differently on two compliant backends, and there&#039;s no built-in mechanism to ensure identical results. This is a limitation for applications requiring cross-platform consistency. Solving it may involve extending ONNX (e.g., an attribute like &amp;lt;code&amp;gt;accumulatePrecision=&amp;quot;FLOAT32&amp;quot;&amp;lt;/code&amp;gt; for MatMul) or tightening the spec, but those come with backward compatibility and implementation burden concerns.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Reproducibility and Determinism:&#039;&#039;&#039; Even with the same precision, parallel computation can introduce nondeterminism (due to different summation orders). Ensuring bit-identical results across runs (important for some sensitive domains) is non-trivial. Frameworks often provide &amp;quot;deterministic mode&amp;quot; which might use fixed ordering at cost of speed. In ONNX, there&#039;s no global flag for this; it&#039;s up to the runtime. More research could be done on algorithms that produce bit-wise identical results independent of parallel execution order (perhaps using techniques like pairwise summation or consistent partitioning). Stochastic rounding is another area: sometimes using probabilistic rounding can reduce bias in accumulated error, but it introduces randomness – reconciling that with reproducibility is tricky.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Ultra-low Precision (FP8 and below):&#039;&#039;&#039; While FP8 is being explored, going to 4-bit floats or mixed schemes is an open challenge. The OpenAI community is investigating 4-bit (quantization-wise) for LLMs with some success, but a true 4-bit float (like E3M1) would be extremely limited. Perhaps hybrid schemes (like block floating point, log scales, etc.) will come. ONNX already has some support (they mention 4-bit in documentation placeholders&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;), but it&#039;s nascent. Research into training techniques, calibration, and new number formats (like &#039;&#039;&#039;posit numbers&#039;&#039;&#039; or tapering precision floats) is ongoing. If a new format proves useful, ONNX will need to adapt to represent it.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Quantization Noise and Model Robustness:&#039;&#039;&#039; Reducing precision can sometimes expose models to issues like adversarial vulnerability or fragility – e.g., a slight perturbation might flip a decision if the margin was small and precision loss nudged it. Ensuring model robustness under quantization is an open problem. It might require re-training the model with quantization in the loop (quantization-aware training) or designing models that have large &amp;quot;margin&amp;quot; between classes to tolerate quantization noise.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Dynamic range during training vs inference:&#039;&#039;&#039; In training, activations can have wider distributions (especially early in training). Models often learn to constrain their ranges (via activation functions saturating or normalization), which makes inference more stable in lower precision. But if one tries to train in FP8 from scratch, it&#039;s very hard. This gap between what&#039;s possible in a post-trained inference model vs training is an open area – how to train directly in low precision without significant accuracy loss. Solving that could simplify workflows (no need for high-precision training then quantize; just train low-prec end to end). Techniques like loss scaling, gradient clipping, etc., are partial solutions. The development of optimizers that are resilient to low precision is ongoing.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Hardware variability:&#039;&#039;&#039; As new hardware emerges (with new numeric capabilities or quirks), ensuring that ONNX models run consistently is an ongoing task. For instance, one device might implement a fused op that reduces rounding error, another does it separately. Even something like convolution might use a different algorithm (FFT-based vs direct) which has different numeric properties. Open problem: could ONNX allow specifying numeric tolerances per op or preferred algorithms? That may be overkill for most, but for some HPC ML use cases, maybe desired.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Testing and validation of numerics:&#039;&#039;&#039; There isn&#039;t a standardized set of &amp;quot;numeric compliance&amp;quot; tests for ONNX runtimes beyond simple correctness. Perhaps in the future, test suites might include challenging numeric scenarios (large sums, nearly singular matrices) to ensure a runtime handles them gracefully (maybe by switching precision). Right now, it&#039;s mostly on the user to test their model under expected conditions.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Combining quantization with floating point:&#039;&#039;&#039; Many production models use a mix (e.g., int8 for some layers, FP16 for others). ONNX can represent this (with QuantizeLinear ops or MixedPrecision through casting), but tooling to automatically find the optimal mix is still developing. It&#039;s somewhat trial-and-error or using heuristics. An open question is how to automate precision assignment: given a model and a target hardware, how to automatically choose which layers can be int8, which should be FP16, etc., to meet an accuracy target. This crosses into NAS (neural architecture search) territory or automated quantization. &lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Numerical debugging tools:&#039;&#039;&#039; When a model goes wrong due to precision, it&#039;s often hard to pinpoint where. Better tooling to analyze numerical error layer by layer could be useful. For instance, one could run a model in double and float and diff the intermediate results to see where the largest divergence happens. Doing this systematically for big nets is an open tooling challenge. Some frameworks have hooks for this, but not standardized.&lt;br /&gt;
&lt;br /&gt;
In conclusion, while we&#039;ve come far with using lower precision in AI without sacrificing much accuracy, the landscape is always evolving. The key open problem is how to push precision lower (for efficiency gains) without crossing the threshold where accuracy falls off a cliff. Each new generation of hardware and techniques (like FP8, novel quantization schemes) extends that boundary, but also brings new questions on how to manage the numerical errors. ONNX as an interchange will need to keep pace, possibly by adding more expressive power for these numeric considerations.&lt;br /&gt;
&lt;br /&gt;
== See Also ==&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;IEEE 754 Floating-Point&#039;&#039;&#039; – The standard defining binary16, binary32, binary64 formats and rounding behavior.&lt;br /&gt;
* &#039;&#039;&#039;Floating-Point Arithmetic&#039;&#039;&#039; – General topic on how floating-point math works, common issues (overflow, underflow, cancellation).&lt;br /&gt;
* &#039;&#039;&#039;Kahan Summation Algorithm&#039;&#039;&#039; – Algorithm for compensated summation to reduce numerical error&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;.&lt;br /&gt;
* &#039;&#039;&#039;BFloat16&#039;&#039;&#039; – A short-format float with 8-bit exponent, used in TPUs and some GPUs for training networks.&lt;br /&gt;
* &#039;&#039;&#039;Mixed Precision Training&#039;&#039;&#039; – Technique of using FP16 (or BF16) along with FP32 accumulators to accelerate training&amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;.&lt;br /&gt;
* &#039;&#039;&#039;Quantization in Deep Learning&#039;&#039;&#039; – Converting networks to lower bit integer arithmetic for inference (INT8 quantization and calibration techniques).&lt;br /&gt;
* &#039;&#039;&#039;ONNX Operator Specification&#039;&#039;&#039; – Official docs for ONNX ops, which describe their mathematical function but not the required numeric precision (a point of potential extension in future).&lt;br /&gt;
* &#039;&#039;&#039;CUDA Tensor Cores &amp;amp; TF32&#039;&#039;&#039; – NVIDIA documentation on how tensor cores perform mixed precision and the TF32 format for accelerating FP32.&lt;br /&gt;
* &#039;&#039;&#039;Pairwise Summation&#039;&#039;&#039; – Summation algorithm with logN error growth&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;, used in many numerical libraries to improve accuracy of reductions.&lt;br /&gt;
* &#039;&#039;&#039;Softmax Stability&#039;&#039;&#039; – Articles on numerically stable softmax and log-sum-exp implementations to avoid overflow.&lt;br /&gt;
* &#039;&#039;&#039;Performance Tuning for Low Precision&#039;&#039;&#039; – Guides (e.g., NVIDIA or Intel whitepapers) on using low precision and the trade-offs involved in various hardware.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;references&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref1&amp;quot;&amp;gt;Accelerate and simplify Scikit-learn model inference with ONNX Runtime - Microsoft Open Source Blog — opensource.microsoft.com — https://opensource.microsoft.com/blog/2020/12/17/accelerate-simplify-scikit-learn-model-inference-onnx-runtime&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref2&amp;quot;&amp;gt;Floating Point Precision: Understanding FP64, FP32, and FP16 in Large Language Models - DEV Community — dev.to — https://dev.to/lukehinds/floating-point-precision-understanding-fp64-fp32-and-fp16-in-large-language-models-3gk6&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref3&amp;quot;&amp;gt;Floating Point Precision and Its Limitations | by Umair Akbar | Medium — akbu.medium.com — https://akbu.medium.com/floating-point-precision-and-its-limitations-cfb7247d7789&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref4&amp;quot;&amp;gt;Kahan summation algorithm - Wikipedia — en.wikipedia.org — https://en.wikipedia.org/wiki/Kahan_summation_algorithm&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref5&amp;quot;&amp;gt;Pairwise summation - Wikipedia — en.wikipedia.org — https://en.wikipedia.org/wiki/Pairwise_summation&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref6&amp;quot;&amp;gt;Numerical behavior of NVIDIA tensor cores - PMC — pmc.ncbi.nlm.nih.gov — https://pmc.ncbi.nlm.nih.gov/articles/PMC7959640/&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref7&amp;quot;&amp;gt;Mixed precision and performance-accuracy tuning (neuron-cc) — AWS Neuron Documentation — awsdocs-neuron.readthedocs-hosted.com — https://awsdocs-neuron.readthedocs-hosted.com/en/latest/about-neuron/appnotes/neuron-cc/mixed-precision.html&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref8&amp;quot;&amp;gt;Create Float16 and Mixed Precision Models - ONNX Runtime — onnxruntime.ai — https://onnxruntime.ai/docs/performance/model-optimizations/float16.html&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref9&amp;quot;&amp;gt;[Performance] ONNX FP16 model is having performance bottle neck when compared to FP32 variant · Issue #25824 · microsoft/onnxruntime · GitHub — github.com — https://github.com/microsoft/onnxruntime/issues/25824&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref10&amp;quot;&amp;gt;Float stored in 8 bits - ONNX 1.21.0 documentation — onnx.ai — https://onnx.ai/onnx/technical/float8.html&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref11&amp;quot;&amp;gt;[2209.05433] FP8 Formats for Deep Learning — arxiv.org — https://arxiv.org/abs/2209.05433&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref12&amp;quot;&amp;gt;Should MatMul op allow specification of accumulation precision? · Issue #7072 · onnx/onnx · GitHub — github.com — https://github.com/onnx/onnx/issues/7072&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref13&amp;quot;&amp;gt;Quantize ONNX models | onnxruntime — onnxruntime.ai — https://onnxruntime.ai/docs/performance/model-optimizations/quantization.html&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref14&amp;quot;&amp;gt;Keep Attention Softmax FP32 during FP16/ZeRO Training · Issue #1485 · hpcaitech/ColossalAI · GitHub — github.com — https://github.com/hpcaitech/ColossalAI/issues/1485&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;/references&amp;gt;&lt;/div&gt;</summary>
		<author><name>Timo.stripf</name></author>
	</entry>
	<entry>
		<id>https://www.emmtrix.com/w139/index.php?title=Numerical_Precision_in_ONNX_and_AI_Inference&amp;diff=2904</id>
		<title>Numerical Precision in ONNX and AI Inference</title>
		<link rel="alternate" type="text/html" href="https://www.emmtrix.com/w139/index.php?title=Numerical_Precision_in_ONNX_and_AI_Inference&amp;diff=2904"/>
		<updated>2026-01-30T15:41:16Z</updated>

		<summary type="html">&lt;p&gt;Timo.stripf: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&#039;&#039;&#039;Open Neural Network Exchange (ONNX)&#039;&#039;&#039; is an open standard format for representing machine learning models and neural network computations across different frameworks and hardware&amp;lt;ref name=&amp;quot;ref1&amp;quot;/&amp;gt;. As models are exported and deployed via ONNX, the &#039;&#039;&#039;numerical precision&#039;&#039;&#039; of computations becomes critical. Deep learning inference involves a variety of floating-point operations, and small numerical discrepancies can accumulate and affect model accuracy or reproducibility. This article provides an in-depth overview of floating-point precision issues in numerical computing, with a focus on how they manifest in AI inference and the ONNX ecosystem. We cover floating-point formats and rounding behavior, accumulation strategies for summations, the use of low-precision formats (FP16, BFloat16, FP8) in modern AI, and how ONNX defines (or leaves unspecified) the precision of accumulators in operations. Strategies for maintaining numerical stability in deep neural networks are discussed, along with the trade-offs between computational performance and arithmetic accuracy. The goal is to give engineers and researchers a comprehensive, encyclopedic reference on numerical precision in ONNX-based AI inference systems.&lt;br /&gt;
&lt;br /&gt;
== Floating-Point Precision in Numerical Computing ==&lt;br /&gt;
&lt;br /&gt;
=== 2.1. IEEE 754 Floating-Point Formats ===&lt;br /&gt;
&lt;br /&gt;
Modern computers represent real numbers using the IEEE 754 floating-point standard, which defines formats like 32-bit &#039;&#039;single precision&#039;&#039; (FP32) and 64-bit &#039;&#039;double precision&#039;&#039; (FP64). In IEEE 754 binary formats, a floating-point number is composed of three fields: 1 sign bit, several exponent bits (with a bias), and several fraction (mantissa) bits. For example, FP32 consists of 1 sign bit, 8 exponent bits, and 23 fraction bits (24 bits of significand precision including the implicit leading 1)&amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;. This provides roughly 7 decimal digits of precision and an exponent range allowing values on the order of &amp;lt;math&amp;gt;10^{\pm38}&amp;lt;/math&amp;gt;. FP64 allocates 1 sign bit, 11 exponent bits, and 52 fraction bits (53-bit precision), yielding about 15–16 decimal digits of precision and a vastly larger range (~&amp;lt;math&amp;gt;10^{\pm308}&amp;lt;/math&amp;gt;)&amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;. These larger formats (especially FP64) are the &amp;quot;gold standard&amp;quot; for numerical accuracy in scientific computing, as they reduce round-off error in complex calculations&amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;. However, the increased precision comes at the cost of doubled storage and typically reduced computational throughput (e.g. many GPUs execute FP64 instructions at a much lower rate than FP32&amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
In addition to single and double precision, the IEEE 754-2008 standard introduced a 16-bit &#039;&#039;half precision&#039;&#039; format (FP16, or binary16). FP16 has 1 sign bit, 5 exponent bits, and 10 fraction bits (11-bit significand including the implicit 1)&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. This format covers a much smaller dynamic range (approximately &amp;lt;math&amp;gt;6.1\times10^{-5}&amp;lt;/math&amp;gt; to &amp;lt;math&amp;gt;6.5\times10^4&amp;lt;/math&amp;gt; for normalized values&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;) and offers only around 3 decimal digits of precision. Half precision was originally used in graphics, but it has gained popularity in machine learning for its speed and memory advantages (discussed later in Section 4). Another format relevant to AI is &#039;&#039;&#039;BFloat16&#039;&#039;&#039; (Brain Float 16), a 16-bit float with &#039;&#039;&#039;8 exponent bits and 7 fraction bits&#039;&#039;&#039;&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. BFloat16 sacrifices precision (only ~7 fraction bits ≈ 2–3 decimal digits) in order to have the &#039;&#039;&#039;same exponent range as FP32&#039;&#039;&#039;&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. This means BFloat16 can represent very large or very small numbers (∼&amp;lt;math&amp;gt;10^{\pm38}&amp;lt;/math&amp;gt;, similar range as FP32) but with much less precision between representable values&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. BFloat16 was introduced for deep learning by Google (for TPUs) to allow reduced precision &#039;&#039;training&#039;&#039; without frequent overflow/underflow, relying on the wide range and accepting the coarse precision.&lt;br /&gt;
&lt;br /&gt;
Each floating-point format has a &#039;&#039;&#039;unit roundoff&#039;&#039;&#039; or machine precision, denoted &amp;lt;math&amp;gt;\epsilon&amp;lt;/math&amp;gt;. This is effectively the spacing between 1.0 and the next representable number. For FP32, &amp;lt;math&amp;gt;\epsilon \approx 2^{-23} \approx 1.19\times10^{-7}&amp;lt;/math&amp;gt;, corresponding to about 7–8 decimal digits of precision&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. In FP64, &amp;lt;math&amp;gt;\epsilon = 2^{-52} \approx 2.22\times10^{-16}&amp;lt;/math&amp;gt; (15–16 decimal digits). Half precision is much less precise: FP16 has &amp;lt;math&amp;gt;\epsilon = 2^{-10} \approx 9.76\times10^{-4}&amp;lt;/math&amp;gt;&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;, and BFloat16 has &amp;lt;math&amp;gt;\epsilon = 2^{-7} = 7.8125\times10^{-3}&amp;lt;/math&amp;gt;&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. These values quantify how finely the continuum of real numbers is quantized in each format.&lt;br /&gt;
&lt;br /&gt;
=== 2.2. Machine Epsilon and Rounding Error ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Machine epsilon (&amp;lt;math&amp;gt;\varepsilon&amp;lt;/math&amp;gt;)&#039;&#039;&#039; is formally defined as the smallest positive number such that &amp;lt;math&amp;gt;1.0 + \varepsilon &amp;gt; 1.0&amp;lt;/math&amp;gt; in the floating-point format&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. It effectively measures the &#039;&#039;&#039;precision&#039;&#039;&#039; of the representation. In FP32, &amp;lt;math&amp;gt;\varepsilon = 2^{-23} \approx 1.19\times10^{-7}&amp;lt;/math&amp;gt;, meaning any result is rounded to about 7 decimal digits. FP16’s &amp;lt;math&amp;gt;\varepsilon = 2^{-10} \approx 9.77\times10^{-4}&amp;lt;/math&amp;gt;, over 1000× larger (so only ~3 decimal digits of precision), and BFloat16’s &amp;lt;math&amp;gt;\varepsilon = 2^{-7} \approx 7.8\times10^{-3}&amp;lt;/math&amp;gt;, reflecting its mere ~8-bit precision&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. Rounding in IEEE 754 by default uses &#039;&#039;&#039;round-to-nearest (ties to even)&#039;&#039;&#039;, which ensures the rounding error for any single operation is at most half a unit in the last place (0.5 ULP). We can model a floating-point addition as: &lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;$\text{fl}(a+b) = (a+b)\,(1 + \delta),&amp;lt;/math&amp;gt;$ &lt;br /&gt;
&lt;br /&gt;
where &amp;lt;math&amp;gt;|\delta| \le \varepsilon&amp;lt;/math&amp;gt; for that format&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. Here &amp;lt;math&amp;gt;\varepsilon/2&amp;lt;/math&amp;gt; is the maximum &#039;&#039;&#039;relative error&#039;&#039;&#039; in one rounding (also called unit roundoff). For example, when adding two FP32 numbers, the result is accurate to within about &amp;lt;math&amp;gt;10^{-7}&amp;lt;/math&amp;gt; of the true sum (relative), whereas in BF16, the result can deviate by up to &amp;lt;math&amp;gt;\sim7.8\times10^{-3}&amp;lt;/math&amp;gt; (nearly 0.78% relative error per operation)&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. Thus, &#039;&#039;&#039;each arithmetic operation incurs a small rounding error&#039;&#039;&#039; (on the order of machine epsilon) that can propagate or accumulate through a sequence of calculations.&lt;br /&gt;
&lt;br /&gt;
Because floating-point representations have finite exponent range, we must also consider &#039;&#039;&#039;overflow&#039;&#039;&#039; and &#039;&#039;&#039;underflow&#039;&#039;&#039;. If a result’s magnitude exceeds the maximum representable value, it overflows to infinity; if it falls below the minimum normal (or minimum subnormal) value, it underflows to zero (or a denormalized tiny value). FP32 and BF16 share an exponent size (8 bits), so their range for normal numbers is roughly &amp;lt;math&amp;gt;[10^{-38}, 10^{38}]&amp;lt;/math&amp;gt; (approx &amp;lt;math&amp;gt;~\!3.4\times10^{38}&amp;lt;/math&amp;gt; max)&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. FP16’s 5-bit exponent allows max &amp;lt;math&amp;gt;\sim 6.55\times10^4&amp;lt;/math&amp;gt; and min normal &amp;lt;math&amp;gt;\sim 6.10\times10^{-5}&amp;lt;/math&amp;gt;&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. In practice, &#039;&#039;&#039;underflow/overflow&#039;&#039;&#039; can occur in low-precision arithmetic if values are not appropriately scaled. For instance, summing a large number of moderately sized terms in FP16 could overflow where FP32 would not. As an example, summing on the order of &amp;lt;math&amp;gt;10^4&amp;lt;/math&amp;gt; values of magnitude ~&amp;lt;math&amp;gt;10^1&amp;lt;/math&amp;gt; (say, adding 10,000 terms around 10 each) would produce a total &amp;lt;math&amp;gt;\sim10^5&amp;lt;/math&amp;gt;, which exceeds FP16’s max finite value ~&amp;lt;math&amp;gt;6.5\times10^4&amp;lt;/math&amp;gt; and would overflow to &amp;lt;math&amp;gt;+\infty&amp;lt;/math&amp;gt; in half precision&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. Underflow is usually a concern when subtracting nearly equal numbers (catastrophic cancellation yielding a tiny difference that might flush to zero in low precision).&lt;br /&gt;
&lt;br /&gt;
=== 2.3. Accumulation Error in Reductions ===&lt;br /&gt;
&lt;br /&gt;
Floating-point addition is &#039;&#039;&#039;not associative&#039;&#039;&#039;: &amp;lt;math&amp;gt;(a+b)+c&amp;lt;/math&amp;gt; can differ from &amp;lt;math&amp;gt;a+(b+c)&amp;lt;/math&amp;gt; due to rounding, and thus the order in which we sum a list of numbers can change the result slightly. When summing a large sequence of &amp;lt;math&amp;gt;N&amp;lt;/math&amp;gt; numbers &amp;lt;math&amp;gt;x_1, x_2, ..., x_N&amp;lt;/math&amp;gt;, the &#039;&#039;&#039;accumulated rounding error&#039;&#039;&#039; can be significant. In the &#039;&#039;worst case&#039;&#039; (e.g., adding many positive and negative terms that cause maximal cancellation or always rounding one way), the error can grow on the order of &amp;lt;math&amp;gt;O(N \varepsilon)&amp;lt;/math&amp;gt; relative to the exact sum&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;. Simply summing &amp;lt;math&amp;gt;N&amp;lt;/math&amp;gt; numbers in sequence (the &#039;&#039;&#039;naive linear summation&#039;&#039;&#039;) has a worst-case error bound proportional to &amp;lt;math&amp;gt;N\,\varepsilon&amp;lt;/math&amp;gt;&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. This worst-case occurs for adversarial arrangements of values; in more typical scenarios with rounding errors of random signs, one can model the error as a random walk, yielding an expected root-mean-square (RMS) error growth on the order of &amp;lt;math&amp;gt;O(\sqrt{N}\,\varepsilon)&amp;lt;/math&amp;gt;&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;. Still, with very large &amp;lt;math&amp;gt;N&amp;lt;/math&amp;gt;, even &amp;lt;math&amp;gt;\sqrt{N}&amp;lt;/math&amp;gt; times &amp;lt;math&amp;gt;\varepsilon&amp;lt;/math&amp;gt; can become large enough to matter.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Cancellation&#039;&#039;&#039; is a particular problem: if we add two nearly equal numbers of opposite sign, most leading digits cancel and the result loses significant bits of precision. For example, in single precision, subtracting two numbers that agree in the first 6 decimal digits will yield a result with only ~1 digit of accuracy left. Catastrophic cancellation can dramatically increase relative error. Summation is especially sensitive to cancellation if large positive and negative terms are summed naively. The &#039;&#039;&#039;condition number&#039;&#039;&#039; of a summation (the ratio of the sum of absolute values &amp;lt;math&amp;gt;\sum|x_i|&amp;lt;/math&amp;gt; to the absolute value of the true sum &amp;lt;math&amp;gt;|\sum x_i|&amp;lt;/math&amp;gt;) indicates how sensitive the result is to perturbations&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;. If the numbers have mixed signs and nearly cancel out (making the condition number large), even an optimal summation order will have a large relative error. For instance, summing [1.0, &amp;lt;math&amp;gt;10^{100}&amp;lt;/math&amp;gt;, 1.0, &amp;lt;math&amp;gt;-10^{100}&amp;lt;/math&amp;gt;] in double precision has an exact sum of 2.0, but naive summation may yield 0.0 due to cancellation; an improved summation algorithm yields the correct 2.0&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
In practical terms, when summing a long list of floating-point numbers (such as accumulating a dot product or reducing a tensor), the &#039;&#039;&#039;accumulation error&#039;&#039;&#039; can become noticeable if no precautions are taken. Techniques to mitigate this error include carefully choosing the summation order or using higher precision for intermediate sums. We will explore these strategies in the next section.&lt;br /&gt;
&lt;br /&gt;
== Accumulators and Reduction Strategies ==&lt;br /&gt;
&lt;br /&gt;
When performing reductions (sums, dot products, etc.), the method of accumulation can greatly affect the accuracy of the result. Below, we discuss various strategies:&lt;br /&gt;
&lt;br /&gt;
=== 3.1. Naive Linear Accumulation ===&lt;br /&gt;
&lt;br /&gt;
The simplest accumulation strategy is to use a single accumulator and add each element sequentially. In C-like pseudocode:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;c&amp;quot;&amp;gt;&lt;br /&gt;
float sum = 0.0f;&lt;br /&gt;
for (int i = 0; i &amp;lt; N; ++i) {&lt;br /&gt;
    sum += array[i];&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This &#039;&#039;&#039;naive linear summation&#039;&#039;&#039; adds values one by one to the running total. While straightforward and fast, it is prone to the rounding error accumulation described earlier. The partial sum &amp;lt;code&amp;gt;sum&amp;lt;/code&amp;gt; continually grows (in absolute value) as terms are added, so new small terms may fall outside its precision. For example, if &amp;lt;code&amp;gt;sum&amp;lt;/code&amp;gt; is large and positive, adding a comparatively tiny positive number may produce no change because the small term&#039;s significant digits vanish in the presence of the large sum (the increment is below the rounding threshold of &amp;lt;code&amp;gt;sum&amp;lt;/code&amp;gt;). Conversely, adding a small number of opposite sign can suffer from cancellation. The order of addition matters: summing from smallest magnitude to largest (ascending order) tends to yield a more accurate result than summing in descending order, because adding small terms first preserves their contributions before the sum becomes very large. The naive loop above implicitly sums in the given index order; if the data is unsorted, this order is essentially arbitrary.&lt;br /&gt;
&lt;br /&gt;
The performance of linear accumulation is excellent (minimal overhead), and for many applications the resulting error is within acceptable limits. However, for &#039;&#039;&#039;very large N&#039;&#039;&#039; or ill-conditioned sums, the inaccuracy can become a problem. In a reduction involving millions of elements, a single-precision sum could lose several digits of accuracy. In deep learning, this could translate to small discrepancies in layer outputs or aggregated gradients if done naively in low precision.&lt;br /&gt;
&lt;br /&gt;
=== 3.2. Pairwise and Tree-Based Reductions ===&lt;br /&gt;
&lt;br /&gt;
A better approach to summation is &#039;&#039;&#039;pairwise summation&#039;&#039;&#039;, which sums numbers in a binary tree structure rather than linearly. The idea is to recursively split the array into halves, sum each half, then add the two partial sums. This can be implemented as:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;text&amp;quot;&amp;gt;&lt;br /&gt;
function pairwise_sum(x[1..n]):&lt;br /&gt;
    if n == 0:&lt;br /&gt;
        return 0&lt;br /&gt;
    if n == 1:&lt;br /&gt;
        return x[1]&lt;br /&gt;
    m = floor(n/2)&lt;br /&gt;
    left_sum  = pairwise_sum(x[1..m])&lt;br /&gt;
    right_sum = pairwise_sum(x[m+1..n])&lt;br /&gt;
    return left_sum + right_sum&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
By always adding numbers of similar magnitudes first (small subsets are reduced to subtotals, then those are added, etc.), pairwise summation avoids the situation of a very large running total absorbing small additions until the final steps. The error of pairwise summation grows logarithmically with &amp;lt;math&amp;gt;N&amp;lt;/math&amp;gt; in the worst case (roughly &amp;lt;math&amp;gt;O(\varepsilon \log_2 N)&amp;lt;/math&amp;gt;)&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;, instead of linearly. Intuitively, at each level of the tree, rounding error is constrained, and there are about &amp;lt;math&amp;gt;\log_2 N&amp;lt;/math&amp;gt; levels. In fact, the worst-case relative error bound for pairwise summation (base case of 1 element) is:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math display=&amp;quot;block&amp;quot;&amp;gt;|E_N| \le \frac{\varepsilon \log_2 N}{1 - \varepsilon \log_2 N} \sum_{i=1}^N |x_i|,&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
which for practical &amp;lt;math&amp;gt;N&amp;lt;/math&amp;gt; simplifies to on the order of &amp;lt;math&amp;gt;\varepsilon \log_2 N&amp;lt;/math&amp;gt; times the condition number of the sum&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;. In typical random-error scenarios, the error behaves even better (on the order of &amp;lt;math&amp;gt;\sqrt{\log N}&amp;lt;/math&amp;gt;)&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;. The key is that &#039;&#039;&#039;pairwise (tree) reduction balances the accumulations&#039;&#039;&#039;, preventing one sum from overwhelming the others too early.&lt;br /&gt;
&lt;br /&gt;
From a performance standpoint, pairwise summation has the same &amp;lt;math&amp;gt;O(N)&amp;lt;/math&amp;gt; total additions as linear summation, just a different order. It is also highly amenable to parallelization: independent partial sums can be computed in parallel at each level of the tree. Many numerical libraries and BLAS implementations use a form of blocked or pairwise summation in dot products for improved accuracy at negligible performance cost. For example, NumPy and Julia perform partial pairwise summation by default for better numerical behavior&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=== 3.3. Compensated Summation (Kahan, Neumaier) ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Compensated summation&#039;&#039;&#039; is an alternative technique to improve summation accuracy by tracking the small “lost” quantities. The most famous algorithm is the &#039;&#039;&#039;Kahan summation algorithm&#039;&#039;&#039; (also called Kahan–Babuška)&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. Kahan summation uses an extra variable to accumulate the round-off error. In pseudocode:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;c&amp;quot;&amp;gt;&lt;br /&gt;
double sum = 0.0;&lt;br /&gt;
double c = 0.0;    // compensation for lost low-order bits&lt;br /&gt;
for (i = 1; i &amp;lt;= N; ++i) {&lt;br /&gt;
    double y = x[i] - c;    // recover low-order bits by subtracting compensation&lt;br /&gt;
    double t = sum + y;     // perform the addition&lt;br /&gt;
    c = (t - sum) - y;      // compute new compensation (the error in t)&lt;br /&gt;
    sum = t;&lt;br /&gt;
}&lt;br /&gt;
return sum;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Here &amp;lt;code&amp;gt;c&amp;lt;/code&amp;gt; accumulates the tiny leftovers that would otherwise be dropped&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. Each iteration essentially subtracts the last error from the new addend (&amp;lt;code&amp;gt;y = x[i] - c&amp;lt;/code&amp;gt;) so that &amp;lt;code&amp;gt;t = sum + y&amp;lt;/code&amp;gt; includes the small bits from the previous operation. The new error is then stored in &amp;lt;code&amp;gt;c&amp;lt;/code&amp;gt; for the next iteration. The result is that the final &amp;lt;code&amp;gt;sum&amp;lt;/code&amp;gt; is as if computed with higher precision than the machine’s: Kahan’s algorithm can &#039;&#039;&#039;greatly reduce the error&#039;&#039;&#039; in summing a sequence of floats. In fact, with exact arithmetic for the compensation, Kahan summation yields a result accurate to within 1 or 2 ULP of the true sum, regardless of N (assuming the sequence of partial sums doesn’t overflow)&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. More precisely, Kahan summation achieves a &#039;&#039;&#039;worst-case error independent of N&#039;&#039;&#039; (error bound depends only on machine precision, not the number of terms)&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. In practice, using a double accumulator for summing single-precision numbers via Kahan can give near double-precision accuracy.&lt;br /&gt;
&lt;br /&gt;
An important subtlety is that the compensation variable &amp;lt;code&amp;gt;c&amp;lt;/code&amp;gt; should be kept in &#039;&#039;full precision&#039;&#039;. In the implementation above, we used &amp;lt;code&amp;gt;double&amp;lt;/code&amp;gt; for both sum and c. If summing FP32 values, one might use &amp;lt;code&amp;gt;float sum&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;float c&amp;lt;/code&amp;gt; (both 32-bit), which still improves accuracy, but using a higher precision for the accumulator and compensation (i.e. double) yields even better results. Many implementations of Kahan summation use the same type for sum and c, but it&#039;s assumed to be higher precision than the inputs when possible.&lt;br /&gt;
&lt;br /&gt;
Kahan’s algorithm carries a performance cost: each iteration does five arithmetic operations (two adds, one subtract, etc.) and has a data dependency, which can hinder parallelism or pipelining&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. Thus, it runs slower than naive summation (roughly 2–3× slower or more, depending on CPU pipeline). In loops where summation is a bottleneck, this may be significant. Nonetheless, for modest N or where maximum accuracy is required, compensated summation is extremely useful. In fact, Python’s &amp;lt;code&amp;gt;math.fsum&amp;lt;/code&amp;gt; function uses a variant of Kahan’s algorithm (actually a more advanced multi-part compensation) to sum iterables with high precision.&lt;br /&gt;
&lt;br /&gt;
There are variations and improvements on basic Kahan summation. &#039;&#039;&#039;Neumaier’s algorithm&#039;&#039;&#039; (sometimes called &#039;&#039;Kahan-Babuška-Neumaier&#039;&#039;) improves Kahan’s method by also handling cases where the next input is larger in magnitude than the running sum&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. In such cases, the original Kahan formula could suffer error; Neumaier’s version effectively swaps roles when a huge value comes in. Its pseudocode is:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;text&amp;quot;&amp;gt;&lt;br /&gt;
function NeumaierSum(array):&lt;br /&gt;
    sum = 0.0&lt;br /&gt;
    compensation = 0.0&lt;br /&gt;
    for each x in array:&lt;br /&gt;
        t = sum + x&lt;br /&gt;
        if (fabs(sum) &amp;gt;= fabs(x))&lt;br /&gt;
            compensation += (sum - t) + x;&lt;br /&gt;
        else&lt;br /&gt;
            compensation += (x - t) + sum;&lt;br /&gt;
        sum = t;&lt;br /&gt;
    return sum + compensation;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This algorithm ensures the compensation covers both scenarios: when the sum is the bigger addend or when the new term is bigger&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. In the end, it adds the final &amp;lt;code&amp;gt;compensation&amp;lt;/code&amp;gt; once to the result. Neumaier’s method can produce the correct result in cases where basic Kahan fails. For instance, summing [1.0, &amp;lt;math&amp;gt;+10^{100}&amp;lt;/math&amp;gt;, 1.0, &amp;lt;math&amp;gt;-10^{100}&amp;lt;/math&amp;gt;] in double precision: regular Kahan summation might give 0.0 (losing the small terms), whereas Neumaier’s algorithm yields 2.0 exactly&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. There are also higher-order compensation methods (carrying multiple compensation terms, e.g., the &#039;&#039;&#039;Klein summation&#039;&#039;&#039; variant&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt; or using arbitrary precision for intermediate sums) that further reduce error at increased computational cost&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
In summary, compensated summation algorithms trade extra computation for greatly improved numerical accuracy. They are especially valuable when summing large arrays of numbers with varying magnitudes, where naive summation could lose most of the low-order bits. Many scientific computing environments provide compensated sum routines for this reason. In deep learning, however, a different approach to improved precision is often taken: &#039;&#039;&#039;using a wider accumulator type&#039;&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
=== 3.4. Mixed-Precision Accumulation ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Mixed-precision accumulation&#039;&#039;&#039; refers to performing arithmetic in a higher precision than the input data. A common scenario is accumulating sums in 32-bit floats when the operands are 16-bit floats. Rather than implementing a compensation algorithm, one can simply use a wider accumulator so that intermediate rounding error is smaller. This is effectively what modern GPUs do for tensor operations: for example, NVIDIA &#039;&#039;&#039;Tensor Cores operate on FP16 input data with an FP32 accumulator&#039;&#039;&#039; (the 16-bit products are accumulated in 32-bit)&amp;lt;ref name=&amp;quot;ref6&amp;quot;/&amp;gt;. In this way, summing 2048 products in a dot product has far less precision loss than it would with a 16-bit accumulator. Hardware that supports mixed precision can give the best of both worlds: fast low-precision multiplies and high-precision accumulation.&lt;br /&gt;
&lt;br /&gt;
Software can also leverage mixed precision. For instance, one can sum an array of &amp;lt;code&amp;gt;float&amp;lt;/code&amp;gt; values into a &amp;lt;code&amp;gt;double&amp;lt;/code&amp;gt; variable, accumulating the result in double precision and then converting back to float at the end:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;c&amp;quot;&amp;gt;&lt;br /&gt;
double sum = 0.0;&lt;br /&gt;
for (int i=0; i&amp;lt;N; ++i) {&lt;br /&gt;
    sum += (double) arr_float[i];&lt;br /&gt;
}&lt;br /&gt;
float result = (float) sum;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This approach often yields a more accurate &amp;lt;code&amp;gt;result&amp;lt;/code&amp;gt; than summing in single precision directly, because the intermediate sum carries 52-bit precision. The final rounding to float happens only once at the end (minimizing overall rounding error). In fact, the above is one way to implement &amp;lt;code&amp;gt;math.fsum&amp;lt;/code&amp;gt; in Python (it internally does something similar with multiple partial sums in double precision).&lt;br /&gt;
&lt;br /&gt;
In machine learning training and inference, &#039;&#039;&#039;mixed precision&#039;&#039;&#039; is widely used. When using FP16 for speed, it&#039;s common to accumulate with FP32. For example, &#039;&#039;&#039;Google&#039;s TPUs use BFloat16 for multiplication but accumulate in FP32&#039;&#039;&#039; for matrix ops&amp;lt;ref name=&amp;quot;ref6&amp;quot;/&amp;gt;. AWS’s Inferentia (Neuron) hardware similarly supports FP16/BF16 matrix multiplication with FP32 accumulation&amp;lt;ref name=&amp;quot;ref7&amp;quot;/&amp;gt;. The motivation is that summing hundreds or thousands of half-precision values would otherwise introduce intolerable error or risk overflow. With an FP32 accumulator, the reduction error is drastically reduced (by about &amp;lt;math&amp;gt;2^{13}&amp;lt;/math&amp;gt; times, since FP32 has 13 more precision bits than FP16). Mixed-precision accumulation thus provides a simple and effective form of compensation: &#039;&#039;&#039;use a larger bucket to collect many small contributions&#039;&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
There is a spectrum of options: summing int8 or int16 integers might use 32-bit integers as accumulators for the same reason (to avoid overflow). Some BLAS libraries even offer &#039;&#039;extended precision accumulators&#039;&#039; for single precision dot products (accumulating in double). The downside of mixed precision is the extra memory and maybe throughput cost of the wider type. But on many platforms, the add latency is dominated by hardware design rather than bit-width, so using a 32-bit adder vs 16-bit has negligible speed impact if supported natively. The key is whether data movement or register pressure increases.&lt;br /&gt;
&lt;br /&gt;
In summary, &#039;&#039;&#039;using a higher precision accumulator is often the easiest way to improve numerical accuracy&#039;&#039;&#039; for reductions. It is a primary strategy in deep learning hardware to maintain stability while exploiting low precision for storage and initial computations.&lt;br /&gt;
&lt;br /&gt;
== Precision Formats in AI and Machine Learning ==&lt;br /&gt;
&lt;br /&gt;
Modern AI systems use a variety of numeric formats to balance speed, memory, and accuracy. We discuss common precisions and their roles:&lt;br /&gt;
&lt;br /&gt;
=== 4.1. FP32 and FP64 in Training and Inference ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;FP32 (single precision)&#039;&#039;&#039; has been the workhorse of deep learning for years. Most training scripts in frameworks like TensorFlow and PyTorch used FP32 by default, as it offers sufficient precision to propagate gradients and accumulate weight updates over millions of iterations without excessive error. With ~7 decimal digits of precision and a wide exponent range, FP32 can handle the dynamic range of activations and gradients in typical neural networks. In inference, FP32 is often more precision than strictly necessary for final predictions, but it has remained common because of its ease and the fact that many GPU operations were optimized for FP32.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;FP64 (double precision)&#039;&#039;&#039; is rarely used in deep learning model training or inference, except in specific scenarios requiring extreme precision. FP64 is common in scientific computing and certain types of numerical simulations. In neural networks, however, the extra precision is usually not needed for model accuracy—using doubles tends to yield negligible improvements in model metrics but at a large performance cost (since on GPUs FP64 throughput can be 1/2, 1/8, or even 1/32 of FP32 throughput depending on hardware). One exception might be when integrating neural nets into larger physics simulations or when solving ill-conditioned problems as part of the model. Some researchers use FP64 to diagnose numerical issues or ensure reproducibility. Generally though, deep learning has favored lower precision for efficiency, and FP64 is considered overkill for most networks&amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
That said, certain classical machine learning algorithms benefit from double precision. For example, computing the inverse of a matrix (as in a Gaussian Process regression or Kalman filter embedded in a model) can incur large rounding errors in single precision, so using FP64 can significantly improve the result&amp;lt;ref name=&amp;quot;ref1&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref1&amp;quot;/&amp;gt;. The ONNX standard supports a float64 tensor type for such needs. In ONNX Runtime, however, emphasis has been on FP32 performance, and some pipelines default to float even if the original model was double&amp;lt;ref name=&amp;quot;ref1&amp;quot;/&amp;gt;. If a model’s prediction computation involves operations that are numerically sensitive (like matrix inversion, Cholesky decomposition, or summing extremely large counts), then FP64 may be required to avoid noticeable discrepancies&amp;lt;ref name=&amp;quot;ref1&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref1&amp;quot;/&amp;gt;. A good practice is to test a model in both float and double if you suspect precision issues; significant differences indicate an ill-conditioned calculation that might need higher precision.&lt;br /&gt;
&lt;br /&gt;
In summary, FP32 remains the &#039;&#039;&#039;default&#039;&#039;&#039; format for most training and inference due to its balance of accuracy and efficiency&amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;, while FP64 is reserved for niche cases or precision-critical subroutines. FP32 typically achieves the same accuracy as FP64 for end-to-end deep learning tasks, because network training can often compensate for small noise, and other sources of error (like generalization error) dominate. Therefore, the community trend has been moving to &#039;&#039;&#039;lower&#039;&#039;&#039; precisions, not higher, to gain speed — provided the models still converge and perform well.&lt;br /&gt;
&lt;br /&gt;
=== 4.2. FP16 and BFloat16 in Deep Learning ===&lt;br /&gt;
&lt;br /&gt;
To further speed up neural network training and inference, &#039;&#039;&#039;half-precision (FP16)&#039;&#039;&#039; was adopted, especially with the advent of GPUs supporting FP16 math at twice the rate of FP32. Using FP16 reduces memory storage and bandwidth by 50% compared to FP32, which is significant for large models. However, FP16’s small exponent range and precision posed challenges: training a large network purely in FP16 can fail due to gradient underflow/overflow or weight updates not registering (because changes are below the 0.001 quantization step). To address this, frameworks introduced &#039;&#039;&#039;mixed-precision training&#039;&#039;&#039;, where compute-intensive parts (matrix multiplies, convolutions) run in FP16, but certain accumulations and model parameters remain in FP32. For instance, NVIDIA’s &#039;&#039;&#039;Tensor Core&#039;&#039;&#039; units require FP16 (or BF16) inputs but accumulate results in FP32, effectively ensuring that reductions are done in higher precision&amp;lt;ref name=&amp;quot;ref6&amp;quot;/&amp;gt;. Research showed that with proper loss scaling and mixed precision, models can train to FP32-level accuracy using mostly FP16 arithmetic, achieving significant speedups.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;BFloat16 (BF16)&#039;&#039;&#039; emerged as an alternative 16-bit format primarily from Google’s TPU training systems. BF16 has a larger exponent (8 bits, same as FP32) but only 7 fraction bits&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. This means BF16 can handle the &#039;&#039;&#039;same dynamic range&#039;&#039;&#039; as FP32 (no overflows where FP32 wouldn’t) but at the cost of even lower precision than FP16 (BF16 has about 2–3 decimal digits of precision vs FP16’s ~3–4). Despite its coarse precision, BF16 turns out to be effective for training deep networks because the algorithms (stochastic gradient descent, etc.) are somewhat robust to that noise, and having the wide range prevents catastrophic loss of signal (no gradient will overflow to Inf unless it was going to in FP32 anyway). Many training frameworks (PyTorch, TensorFlow) support BF16 training on hardware like &#039;&#039;&#039;TPU, NVIDIA A100+, Intel CPUs with AVX-512 BF16, etc&#039;&#039;&#039;. Typically, when using BF16, &#039;&#039;&#039;accumulations are still done in FP32&#039;&#039;&#039; (for example, a TPU v2/v3 multiplies BF16 matrices and accumulates into an FP32 result for the dot product&amp;lt;ref name=&amp;quot;ref6&amp;quot;/&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
In inference, both FP16 and BF16 are used to deploy models for faster speed or lower memory. FP16 is popular on NVIDIA GPUs (which have specialized FP16 units). BF16 is advantageous on TPUs and some CPUs that support it, and increasingly on GPUs (Ampere and newer GPUs also support BF16 arithmetic). The choice between FP16 and BF16 can depend on hardware: BF16’s advantage is easier software conversion (you can often take an FP32-trained model and cast weights to BF16 with minimal loss, since the dynamic range is intact), whereas FP16 might require adjusting the model or ensuring values fit in range (sometimes needing techniques like maintaining a master copy of weights in FP32 during training, or handling out-of-range values).&lt;br /&gt;
&lt;br /&gt;
For ONNX, both Float16 and BFloat16 types are part of the standard. ONNX models can be converted to FP16 (truncating weights and inserting cast nodes for computations) using tools like &amp;lt;code&amp;gt;float16_converter&amp;lt;/code&amp;gt; in ONNX Runtime&amp;lt;ref name=&amp;quot;ref8&amp;quot;/&amp;gt;. ONNX Runtime on GPUs will typically execute those FP16 ops with mixed precision on tensor cores (FP16 input, FP32 accumulate), whereas on CPU it may emulate FP16 or upcast to FP32 (as many CPU implementations do not natively support half-precision math)&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;. In fact, as an example, running an ONNX model in FP16 on a CPU might insert hidden Cast operations to FP32 behind the scenes, because the CPU kernel for that op only exists in FP32&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;. This ensures correctness (the computation is done in higher precision) but can reduce performance. We will revisit such backend-dependent behavior in Section 5.3.&lt;br /&gt;
&lt;br /&gt;
In summary, &#039;&#039;&#039;FP16 and BF16&#039;&#039;&#039; are now established as important low-precision formats in deep learning. FP16 offers more precision bits, which can matter for e.g. small differences, but has a limited range (care needed to avoid overflow). BF16 offers safety in range but less precision (which can slightly affect convergence or final accuracy in some cases). Both typically rely on FP32 for accumulation to achieve acceptable accuracy. When deploying models, one should choose a format supported efficiently by the target hardware and verify that the accuracy is not significantly degraded. Often, CNNs and transformers can run in FP16/BF16 with virtually no drop in score, but certain operations (like softmax, see Section 6.3) might need special handling.&lt;br /&gt;
&lt;br /&gt;
=== 4.3. FP8 Formats (E4M3, E5M2) and Scaling ===&lt;br /&gt;
&lt;br /&gt;
The push for ever lower precision has led to experimental &#039;&#039;&#039;8-bit floating point (FP8)&#039;&#039;&#039; formats for deep learning. In 2022, researchers from NVIDIA, Intel, and Arm proposed an FP8 standard with two variants: &#039;&#039;&#039;E4M3&#039;&#039;&#039; (4 exponent bits, 3 fraction bits) and &#039;&#039;&#039;E5M2&#039;&#039;&#039; (5 exponent bits, 2 fraction bits)&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;. Both have 1 sign bit, for a total of 8 bits. E5M2 has a wider exponent range (5 bits gives bias 15) but one fewer mantissa bit than E4M3. The idea is to use E4M3 for numbers that need more precision (like weights or activations around 1.0) and E5M2 for numbers that need more dynamic range (like gradient updates which can be very small or large). In fact, the study found that using E4M3 for forward activations/weights and E5M2 for backward gradients can allow training of large networks with FP8 to accuracy on par with FP16 training&amp;lt;ref name=&amp;quot;ref11&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
FP8 formats are &#039;&#039;not&#039;&#039; IEEE 754 standard (though IEEE is considering lower precision in its 2019 revision). Notably, the E4M3 format as used by NVIDIA does not allocate any bit-pattern for &amp;lt;math&amp;gt;+\infty&amp;lt;/math&amp;gt; or &amp;lt;math&amp;gt;-\infty&amp;lt;/math&amp;gt; – it has only NaN for invalid results, using that freed bit pattern to extend the representable range by one exponent value&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;. Essentially, E4M3 can represent numbers up to ~&amp;lt;math&amp;gt;2^{15}&amp;lt;/math&amp;gt; (around 3e4) instead of capping at infinity at &amp;lt;math&amp;gt;2^{16}&amp;lt;/math&amp;gt;, by foregoing infinities&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;. Variations also exist regarding negative zero and how NaNs are handled&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;. For instance, GraphCore’s IPU uses FP8 formats that disallow negative zero and infinities, to maximize useable range&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Using 8-bit floats for neural networks requires additional techniques, chiefly &#039;&#039;&#039;scaling&#039;&#039;&#039;. Because 8-bit has very limited precision and range, one typically applies a power-of-two scale factor to groups of values to normalize them into an FP8-friendly range. This is akin to block floating-point or “microscaling” per layer or per tensor. For example, one might keep a FP32 scale for each channel or tensor, and represent values as &amp;lt;math&amp;gt;(FP8\_value) \times 2^{scale}&amp;lt;/math&amp;gt;. The ONNX standard has introduced support for FP8 in version 1.15, including types for E4M3 and E5M2 (with variations FNUZ: finite-only and no negative zero)&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;, and even a type called &#039;&#039;&#039;E8M0&#039;&#039;&#039; which is an 8-bit exponent-only number used purely as a scale factor&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;. The E8M0 type has 8 exponent bits, no mantissa, effectively representing powers of two from &amp;lt;math&amp;gt;2^{-127}&amp;lt;/math&amp;gt; to &amp;lt;math&amp;gt;2^{128}&amp;lt;/math&amp;gt; for scaling purposes&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;. This indicates an approach where an FP8 tensor would be accompanied by an E8M0 scale (or scales) to interpret its value correctly.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Rounding&#039;&#039;&#039; becomes critical when casting values to FP8. The quantization error is much larger relative to FP16 or FP32. Studies have shown that choosing the right rounding mode (e.g., round-to-nearest, stochastic rounding, etc.) and clamping behavior (saturating to max instead of inf) can affect model accuracy&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;. ONNX added a &amp;lt;code&amp;gt;round_mode&amp;lt;/code&amp;gt; attribute to its Cast operator to allow controlling this for FP8&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;. In particular, one paper found that &#039;&#039;&#039;rounding up with saturation&#039;&#039;&#039; (round-to-nearest ties away from 0, and clamp overflows to max finite rather than inf) gave better accuracy in large language model training than other rounding modes&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
At present, FP8 is on the cutting edge. NVIDIA’s H100 GPU supports FP8 tensor cores (for both E4M3 and E5M2), and early results show minimal accuracy loss on some vision and NLP tasks when properly tuned. However, using FP8 requires fine-tuning hyperparameters like scaling factors for each layer and perhaps tweaking training procedures. ONNX’s inclusion of FP8 types means you can represent and transport models using FP8 weights or activations, but actual hardware support is limited and typically paired with calibration or dynamic scaling logic outside of ONNX graph (or using custom ONNX functions for scaling). It remains an &#039;&#039;&#039;open research area&#039;&#039;&#039; how to fully automate FP8 quantization for arbitrary models without accuracy loss.&lt;br /&gt;
&lt;br /&gt;
In summary, FP8 formats promise further compression of neural network computation and data. They are an order of magnitude more error-prone than FP16, but when combined with careful scaling, mixed usage of E4M3/E5M2, and maybe modified training techniques, they have shown surprising success. ONNX is actively evolving to support these ultra-low precision types, signaling their potential importance in future AI inference deployments.&lt;br /&gt;
&lt;br /&gt;
== Accumulation Semantics in ONNX ==&lt;br /&gt;
&lt;br /&gt;
One of the roles of the ONNX specification is to define the mathematical behavior of operators (Conv, MatMul, Sum, etc.) in a framework-agnostic way. However, ONNX mostly defines &#039;&#039;what&#039;&#039; an operator computes in exact arithmetic, not &#039;&#039;how&#039;&#039; the computation is carried out in finite precision. This leaves certain details – like accumulator precision or summation order – up to the implementation (runtime or hardware). We&#039;ll examine how ONNX treats accumulation and what guarantees (or lack thereof) are provided.&lt;br /&gt;
&lt;br /&gt;
=== 5.1. Operator Definitions and Numerical Guarantees ===&lt;br /&gt;
&lt;br /&gt;
Each ONNX operator is described as performing an ideal mathematical function on its inputs. For example, &amp;lt;code&amp;gt;MatMul&amp;lt;/code&amp;gt; is defined as the standard matrix product of two tensors, and &amp;lt;code&amp;gt;ReduceSum&amp;lt;/code&amp;gt; computes a summation along given axes. The ONNX spec, as of current versions, &#039;&#039;&#039;does not mandate the precision of intermediate calculations&#039;&#039;&#039; beyond the types of the inputs and outputs. If an operator’s inputs and outputs are float16, the spec implies the computation is done in float16 in a sense, but it does not forbid an implementation from using higher precision internally – it simply doesn&#039;t mention it. There is &#039;&#039;&#039;no explicit guarantee of identical bit-for-bit results&#039;&#039;&#039; across different ONNX backends for floating-point ops. Instead, results are expected to be numerically close, within tolerances one would normally expect from floating-point differences (due to different hardware or algorithms). The ONNX Backend Test suite typically uses a relative or absolute error tolerance when validating outputs for this reason.&lt;br /&gt;
&lt;br /&gt;
Because of this, important details like the use of extended precision or accumulation in higher precision are left ambiguous. This has led to discussions in the ONNX community about making such behavior more explicit. For example, a proposal was raised to allow specification of accumulation precision for certain ops like MatMul&amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;. The reasoning is that as lower-precision types (float16, bfloat16, float8) become more common, knowing or controlling whether an op internally accumulates in a higher precision is important for both accuracy and reproducibility&amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;. In June 2025, an issue on the ONNX GitHub suggested adding an attribute to MatMul (and similarly Gemm, Softmax, etc.) to specify the accumulator type&amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;. By default it would use input precision (status quo), but an attribute could request (for instance) 32-bit accumulation even if inputs are 16-bit&amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;. As of this writing, however, no such attribute exists in the official spec – it&#039;s an open issue. Thus, the numerical semantics are effectively: &#039;&#039;“compute the result as if in the same type as inputs, but actual implementations may use more precision internally as an optimization.”&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
From a guarantee standpoint, ONNX operators promise the mathematically correct result assuming real-number arithmetic, but they &#039;&#039;&#039;do not guarantee bit-exact results&#039;&#039;&#039; due to floating-point effects. There is an understanding that differences can arise from different summation orders, fused operations (e.g., an implementation might use an FMA (fused multiply-add) which has one rounding instead of two), or use of extended precision registers. ONNX does not formally define rounding modes either – it relies on IEEE 754 defaults, but if a backend used something like deterministic summation or alternative rounding, ONNX has no mechanism to express that.&lt;br /&gt;
&lt;br /&gt;
In summary, ONNX&#039;s current stance is largely &#039;&#039;&#039;implementation-dependent&#039;&#039;&#039; for numerical precision. It provides minimal explicit numerical guarantees beyond type constraints. This is why an ONNX model run on two different hardware or runtime libraries can have slightly different results, especially in reduced precision. The burden is on backends to produce results &amp;quot;close enough&amp;quot; to each other that they don&#039;t affect the model&#039;s decisions in any significant way.&lt;br /&gt;
&lt;br /&gt;
=== 5.2. MatMul, Conv, and Reduction Operators ===&lt;br /&gt;
&lt;br /&gt;
Consider ONNX &amp;lt;code&amp;gt;MatMul&amp;lt;/code&amp;gt;: it takes two tensors (often two matrices) and produces their matrix product. In pure math, &amp;lt;math&amp;gt;C_{ij} = \sum_k A_{ik} &#039;&#039; B_{kj}&amp;lt;/math&amp;gt;. If A and B are float16, should that sum be computed in float16 or float32? As discussed, ONNX doesn’t specify – it simply says the output is a float16 matrix containing the product. In practice, many backends will &#039;&#039;promote&#039;&#039; the computation: for example, NVIDIA GPUs will do the multiplication and addition in a mixed FP16/FP32 mode by default (because of tensor cores), then convert the final result back to float16. ONNX Runtime on CPU might not support float16 multiplication natively, so it will insert cast ops to float32, perform the MatMul in float32, then cast back to float16&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;. From ONNX’s perspective, as long as the final result is within acceptable error of the “ideal” float16 computation, it’s fine. But note, an “ideal” float16 computation (where every multiply and add is rounded to FP16) might actually be &#039;&#039;less accurate* than what the backend did with FP32. This means that a backend using higher precision can produce a result that is not bit-equal to a pure FP16 implementation – it&#039;s actually closer to the true real number result. This is usually considered a good thing (improved accuracy), but it does highlight a lack of consistency: another backend might do everything in pure FP16 and get a slightly different answer.&lt;br /&gt;
&lt;br /&gt;
The same situation occurs for &#039;&#039;&#039;convolution&#039;&#039;&#039; (&amp;lt;code&amp;gt;Conv&amp;lt;/code&amp;gt; op) which is essentially a batched sum of products (like MatMul in sliding window), and for explicit reduction ops like &amp;lt;code&amp;gt;ReduceSum&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;ReduceMean&amp;lt;/code&amp;gt;, etc. If you have an ONNX &amp;lt;code&amp;gt;ReduceSum&amp;lt;/code&amp;gt; that sums 1000 float16 values, the spec doesn’t say how to sum them. A GPU kernel might sum in a tree using FP32 accumulators; a naive implementation might sum in order with FP16 and possibly overflow or underflow intermediate steps. Both are allowed as long as they output a float16 result that is “reasonable.” In practice, ONNX Runtime and others take care to avoid obviously bad behavior (like overflow) by using higher precision or by splitting the sum. But again, it’s not mandated by the ONNX standard explicitly.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Softmax&#039;&#039;&#039; is an interesting case: Softmax involves an internal sum of exponentials. ONNX defines the Softmax operator as &amp;lt;math&amp;gt;$\mathrm{Softmax}(x)_i = \frac{\exp(x_i)}{\sum_j \exp(x_j)},&amp;lt;/math&amp;gt;&amp;lt;math&amp;gt; typically applied along a specified axis. Directly computing this in float16 can be problematic for large vectors, as discussed in Section 6.3. Indeed, the issue of accumulation precision arises: should the sum &amp;lt;/math&amp;gt;\sum_j \exp(x_j)&amp;lt;math&amp;gt; be done in FP16 or can it be in FP32? Many implementations will do it in FP32 even if &amp;lt;/math&amp;gt;x&amp;lt;math&amp;gt; is FP16, because the dynamic range of &amp;lt;/math&amp;gt;\exp(x)$ is large. The ONNX Softmax spec doesn’t detail this, but it expects the result to be a proper probability distribution (summing to 1.0 within normal floating error). A pure FP16 softmax on, say, 1000 elements could sum to something like 0.996 or 1.004 due to rounding, whereas FP32 accumulation would be much closer to 1.000. ONNX likely allows both; the small difference is usually inconsequential, but if it were large (say the FP16 sum overflowed to inf), that would be a correctness issue. A robust backend will prevent that (by using the common max-subtraction trick and higher precision accumulation). The ONNX issue 7072 indeed calls out Softmax as another op where explicit control of accumulation precision would be useful&amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
In summary, &#039;&#039;&#039;MatMul, Conv, and reduction ops&#039;&#039;&#039; in ONNX are defined mathematically but with &#039;&#039;implicit&#039;&#039; numerical behavior. Implementations commonly use higher precision for intermediate sums especially when input is low precision, but this is not standardized. Consequently, results can differ slightly between implementations. For critical applications, one must be aware of these differences. A model exported to ONNX with float16 weights might perform slightly differently on one runtime vs another if one does all float16 math and the other uses float32 accumulations. Both conform to ONNX. As a user, if you require a certain precision for accumulation, currently you have to ensure it by the way you export or run the model (for example, insert manual cast nodes in the ONNX graph to force certain ops to FP32). We may see future ONNX versions address this with attributes or metadata as mentioned, but for now these ops are effectively “compute with best effort accuracy”.&lt;br /&gt;
&lt;br /&gt;
=== 5.3. Backend and Hardware-Dependent Behavior ===&lt;br /&gt;
&lt;br /&gt;
Because ONNX is implemented by many backends (ONNX Runtime, TensorRT, CoreML, PyTorch JIT, etc.), and these in turn run on varied hardware (CPU, GPU, accelerators), the numeric behavior can be backend-dependent. Some examples:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;CPU backends:&#039;&#039;&#039; Many CPUs do not have native float16 vector arithmetic (with a few exceptions in recent CPUs). ONNX Runtime’s default CPU execution provider will often upcast float16 to float32, compute, then downcast&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;. This ensures accuracy but means that if you thought you were getting a full float16 pipeline, you’re not – the CPU is essentially ignoring the low precision in compute. On the other hand, x86 CPUs have extended 80-bit precision in x87 registers (though less used now) and 64-bit SIMD registers for doubles; some compiler optimizations might carry intermediate results in higher precision. This could make CPU results slightly different from a GPU’s if, say, a sum is done in 80-bit on CPU versus 32-bit on GPU. Generally, modern compilers avoid such extended precision unless explicitly asked, to improve reproducibility.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;GPU backends:&#039;&#039;&#039; GPUs often perform summations in parallel using tree reductions. The order of operations can change depending on how threads are scheduled. This means even on the same GPU, summing an array in one launch versus another might not give bit-identical results if the internal parallel reduction algorithm differs (though it should be deterministically the same given the same library version and input, but if you change GPU model or BLAS library, it might differ). Also, GPUs have fused multiply-add (FMA) instructions that do &amp;lt;math&amp;gt;(a*b + c)&amp;lt;/math&amp;gt; in one step with one rounding, which can produce slightly different results than doing the multiply and add separately (which would round twice). If an ONNX backend uses a fused kernel (like a GEMM library) it might yield different low-order bits than a simple loop.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Vendor libraries:&#039;&#039;&#039; ONNX Runtime can use vendor BLAS or DNN libraries (MKL, cuDNN, TensorRT, etc.). These libraries often implement their own strategies for numerical stability. For example, NVIDIA’s cuDNN might use FP32 accumulation for FP16 inputs by default. TensorRT (NVIDIA’s inference engine) will sometimes automatically promote precisions or insert scaling to maintain stability&amp;lt;ref name=&amp;quot;ref13&amp;quot;/&amp;gt;. On the other hand, if you explicitly request FP16 in TensorRT with no FP32 fallback, it might do the entire op in FP16 and you’d see more error. Some libraries offer knobs: TensorRT allows choosing FP32, FP16, or INT8 precision for each layer, and will use FP32 internally for safety in some cases unless forced otherwise.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Hardware-specific quirks:&#039;&#039;&#039; As another example, NVIDIA’s &#039;&#039;&#039;TensorFloat-32 (TF32)&#039;&#039;&#039; on Ampere GPUs is a precision mode where matrix multiplications use 10-bit mantissa (so about FP16 precision for the multiply) but keep 8-bit exponent (range like FP32). By default, CUDA libraries use TF32 for FP32 matrix multiplies on Ampere to speed them up, unless you disable it. This means if you run an ONNX model with FP32 on an A100 GPU, your matmuls might actually be lower precision than FP32 (though higher than FP16)&amp;lt;ref name=&amp;quot;ref6&amp;quot;/&amp;gt;. It’s an example of hardware choosing an intermediate precision for performance. From ONNX’s perspective, the output is still FP32 and (hopefully) within error tolerances, but a user might be surprised that it’s not a “true” FP32 computation internally. If exactness is needed, one has to turn off TF32.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Non-determinism:&#039;&#039;&#039; Some backends (especially on GPUs) allow non-deterministic execution for performance. For instance, atomic adds in parallel reductions can produce results that vary run-to-run because the order of accumulation is race-condition-dependent. ONNX’s spec doesn’t cover this explicitly, but it’s something to consider. In ONNX Runtime, there are settings for deterministic computing, but by default it may use all available performance optimizations, even if results vary at the 1e-7 level.&lt;br /&gt;
&lt;br /&gt;
In short, the numerical behavior under ONNX can depend on the combination of backend software and hardware capabilities. The lack of a strict spec for accumulation means implementations have freedom to use clever strategies (higher precision, reordering) to improve accuracy or speed. Usually this is positive (e.g. getting better accuracy than a naive approach). But it could also lead to slight inconsistencies. For example, you might find that exporting a PyTorch model to ONNX and running it with one engine yields outputs slightly different from PyTorch’s – not because ONNX is wrong, but because the summation or convolution was done differently (maybe PyTorch summed in FP32 as well, but maybe not in exactly the same way).&lt;br /&gt;
&lt;br /&gt;
The key takeaway is that ONNX &#039;&#039;by design&#039;&#039; abstracts away these details, which is good for portability but means you need to &#039;&#039;&#039;trust the backend&#039;&#039;&#039; or verify its precision behavior for sensitive models. If necessary, you can enforce certain behavior by inserting ops (for example, if you want to ensure FP32 accumulation, you could insert a cast to FP32 for inputs of a MatMul and cast the result back to FP16 – this way the ONNX graph itself mandates FP32 compute in between). This of course sacrifices some performance or device-specific magic.&lt;br /&gt;
&lt;br /&gt;
Looking forward, as mentioned, there are proposals to enrich ONNX’s specification regarding precision. This might include explicit flags for “exact summation” or “accumulate in int32” etc., which would make models more self-descriptive but also potentially less portable (if a backend doesn’t support that, what then?). For now, users should be aware of these backend differences. Most of the time, well-engineered ONNX runtimes yield results close enough that they don&#039;t affect the application (e.g., classification top-1 remains the same even if low-level sums differ by 1e-5). But for high-stakes numerical computations, testing and maybe adjusting the model or backend settings is prudent.&lt;br /&gt;
&lt;br /&gt;
== Numerical Stability in Deep Learning Models ==&lt;br /&gt;
&lt;br /&gt;
Deep learning computations involve many operations that, while theoretically well-defined, can be sensitive to numerical issues when implemented in finite precision. We&#039;ll discuss a few common scenarios in neural networks where numerical stability is a concern, and how precision and accumulation play a role.&lt;br /&gt;
&lt;br /&gt;
=== 6.1. Dot Products and Large-Scale Reductions ===&lt;br /&gt;
&lt;br /&gt;
Neural networks are full of dot products: every fully-connected layer computes &amp;lt;math&amp;gt;\mathbf{y} = W\mathbf{x}&amp;lt;/math&amp;gt;, which is a series of inner products between weight vectors and the input. Convolutional layers compute weighted sums of local regions. Attention mechanisms compute large matrix products of query/key/value matrices. All these boil down to summing a lot of terms: &amp;lt;math&amp;gt;\sum_{k=1}^K a_k b_k&amp;lt;/math&amp;gt;. When &amp;lt;math&amp;gt;K&amp;lt;/math&amp;gt; is large (it could be in the thousands or more for a single neuron in a fully connected layer of a large model), the potential for floating-point error in that sum increases.&lt;br /&gt;
&lt;br /&gt;
If using FP32, the error may still be negligible in many cases, but in lower precision like FP16, it can become significant. For example, consider a dot product of length 2048 in FP16. Each product is in FP16 (with ~3 decimal digits precision) and if accumulated in FP16, the worst-case error could be on the order of &amp;lt;math&amp;gt;2048 \times \varepsilon_{FP16} \approx 2048 \times 0.0009766 \approx 2&amp;lt;/math&amp;gt; (i.e., you could lose a couple units in the third decimal place relative accuracy in worst case). Even in random error accumulation, you might see error on the order of &amp;lt;math&amp;gt;\sqrt{2048} \times 0.0009766 \approx 0.044&amp;lt;/math&amp;gt; or 4.4% (though this is a rough estimate). That’s quite significant. Fortunately, as discussed, most hardware will accumulate such sums in a higher precision like FP32, dramatically reducing that error (~7 orders of magnitude smaller &amp;lt;math&amp;gt;\varepsilon&amp;lt;/math&amp;gt;). But if it didn’t, the network’s outputs could get noisy. A fully-connected layer with many inputs might output slightly different results depending on summation order if done in low precision.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Large-scale reductions&#039;&#039;&#039; also occur in operations like global average pooling (summing all elements of a feature map) or computing the total loss by summing loss over batch samples. If one sums 100,000 values in FP32, the worst-case error could be on the order of 100,000 &#039;&#039; 1e-7 ≈ 1e-2 (1% uncertainty), though typical error would be much less. In FP16, summing 100k values is likely impossible without overflow: as noted, adding ~10^4 moderate numbers can overflow FP16&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. So large reductions &#039;&#039;must* be handled carefully (either in chunks or in higher precision). Frameworks generally do: for example, when performing a sum reduction on GPU, libraries break it into parallel blocks that accumulate in registers (usually 32-bit registers, even if inputs are 16-bit). &lt;br /&gt;
&lt;br /&gt;
Another aspect is that many dot products in neural nets involve values of varying magnitude. If the distribution of terms is such that some are much larger than others, the smaller terms might not contribute much if summed last. Ideally, one might sort by magnitude or use pairwise summation to improve that. Most BLAS libraries effectively do a pairwise strategy due to their parallel structure. So, while worst-case analyses are grim, practical implementations mitigate a lot of it.&lt;br /&gt;
&lt;br /&gt;
However, certain layers can suffer if not handled properly. One famous example is in RNNs or LSTMs: if one uses FP16 naive summation for the gating mechanism, errors could accumulate over many time steps and cause drift or instability. In modern practice, recurrent layers are often run in FP16 with FP32 accumulators to avoid this.&lt;br /&gt;
&lt;br /&gt;
In summary, &#039;&#039;&#039;dot products and large reductions need sufficient precision&#039;&#039;&#039; or robust algorithms. If you are implementing a custom kernel (say a custom ONNX op) and you have to sum a huge array, you should consider using a technique from Section 3 (pairwise summation, Kahan, or at least double accumulator if possible). Many deep learning computations, by virtue of hardware and library design, already do something like this under the hood. But it&#039;s always good to be aware, especially if pushing into lower precision territories.&lt;br /&gt;
&lt;br /&gt;
=== 6.2. Cancellation, Dynamic Range, and Conditioning ===&lt;br /&gt;
&lt;br /&gt;
We introduced &#039;&#039;cancellation&#039;&#039; earlier in the context of summation. In deep learning, cancellation can appear in a few places. One is in the computation of certain norms or variances. For instance, Batch Normalization computes mean and variance of activations. The variance formula &amp;lt;math&amp;gt;\mathrm{Var}(x) = E[x^2] - (E[x])^2&amp;lt;/math&amp;gt; is subject to catastrophic cancellation if &amp;lt;math&amp;gt;E[x]&amp;lt;/math&amp;gt; and &amp;lt;math&amp;gt;\sqrt{E[x^2]}&amp;lt;/math&amp;gt; are close in magnitude (which can happen if the variance is small compared to the mean). If implemented naively in single precision, you might get a negative variance due to numerical error (which would then be clamped to zero or sqrt would yield NaN). Robust implementations compute variance in a single pass or increase precision. Indeed, in many frameworks, batchnorm accumulation is done in FP32 even if the activations are FP16, precisely to avoid numerical issues in computing mean and variance. ONNX itself doesn’t specify this, but an ONNX backend is likely to do batchnorm stats in higher precision for safety. The &#039;&#039;conditioning&#039;&#039; of the variance calculation is poor when variance is tiny relative to mean, so it&#039;s a spot where higher precision is needed.&lt;br /&gt;
&lt;br /&gt;
Another example is in certain loss functions or regularizations. If you subtract two large numbers to get a small difference, you lose precision. Say you have two nearly identical predictions and you subtract them as part of some operation – if done in float16, you might get zero whereas float32 would have a slight difference. This could affect, say, a gradient if that difference was supposed to propagate information. Fortunately, neural nets often avoid directly subtracting large nearly equal numbers; but it can happen. In attention mechanisms, the &#039;&#039;score normalization&#039;&#039; (softmax) subtracts a max to avoid huge exponentials, which is good for range but if multiple scores are close to the max, you subtract numbers that are close. However, those subtractions are not catastrophic in the same way; they&#039;re intentional to reduce range and are typically fine in FP32 or even FP16.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Dynamic range&#039;&#039;&#039; issues are everywhere in deep learning. Activation functions like ReLU and exponential can produce very large or very small values. If subsequent computations don’t account for that, you get infinities or zeros. For example, consider a network output that produces a logit of 100 in FP16. &amp;lt;math&amp;gt;\exp(100)&amp;lt;/math&amp;gt; in FP16 overflows to infinity (since FP16 max ~ 6e4 and &amp;lt;math&amp;gt;e^{100} \sim 3.7e43&amp;lt;/math&amp;gt;). If you then do softmax, that one infinity will dominate the sum and you might get a probability of 1 for that class (or NaNs if not handled). The standard softmax implementation subtracts the max logit, so in this case it would subtract 100 from all logits, making the largest exponent &amp;lt;math&amp;gt;\exp(0)=1&amp;lt;/math&amp;gt; and others tiny – this avoids overflow. But what about underflow? If you had a very negative logit, &amp;lt;math&amp;gt;\exp(-100)&amp;lt;/math&amp;gt; in FP16 would underflow to 0. In softmax, that just contributes ~0 probability, which is fine (it effectively is zero anyway). Underflow tends to be less dangerous than overflow in inference — underflow usually just means “this probability/neuron is effectively 0,” whereas overflow can poison the results with NaNs or Infs. &lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Conditioning&#039;&#039;&#039; refers to how sensitive a function is to input perturbations. Neural networks themselves as functions can be poorly conditioned (e.g., tiny changes in weights cause big changes in output for some architectures), but typically they are designed to be reasonably well-behaved. Certain internal computations like matrix inversion (if one does a pseudoinverse or something for a layer) can be extremely ill-conditioned if the matrix is near singular. That again would need double precision perhaps. In ML, one common ill-conditioned operation is the &#039;&#039;&#039;softmax denominator&#039;&#039;&#039;: &amp;lt;math&amp;gt;\sum_j e^{x_j}&amp;lt;/math&amp;gt; can be very large and also very sensitive to changes in the largest &amp;lt;math&amp;gt;x_j&amp;lt;/math&amp;gt;. However, the softmax is more stable if computed carefully (max trick). Another is &#039;&#039;&#039;normalization&#039;&#039;&#039;: computing &amp;lt;math&amp;gt;\frac{x - \mu}{\sigma}&amp;lt;/math&amp;gt; for layer norm or batch norm – if &amp;lt;math&amp;gt;\sigma&amp;lt;/math&amp;gt; is very small, then the division blows up noise. In those cases, a small &amp;lt;math&amp;gt;\epsilon&amp;lt;/math&amp;gt; is usually added to &amp;lt;math&amp;gt;\sigma^2&amp;lt;/math&amp;gt; to prevent instability (this is a model-level fix, not a numeric one per se, but it acknowledges numeric limits).&lt;br /&gt;
&lt;br /&gt;
In training, &#039;&#039;&#039;optimizers&#039;&#039;&#039; can suffer from precision issues. For example, the Adam optimizer keeps a moving average of squared gradients; in half precision, these small updates might flush to zero or not change at some point. Some research found that certain adaptive optimizers break in FP16 because the small differences are lost (&amp;quot;Why FP16 training breaks NAdam&amp;quot; etc.). The solution is often to keep the optimizer states in FP32 while doing the bulk of compute in FP16. This again is a form of mixed precision.&lt;br /&gt;
&lt;br /&gt;
From an inference perspective, once the model is trained, the main concerns are avoiding overflow in activations and ensuring summations (like in softmax or reductions) are done with enough precision. The model’s weights are fixed, so we don&#039;t have to worry about cumulative error over iterations as in training, just the forward pass.&lt;br /&gt;
&lt;br /&gt;
=== 6.3. Normalization Layers and Softmax ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Normalization layers&#039;&#039;&#039; (BatchNorm, LayerNorm, etc.) and &#039;&#039;&#039;Softmax&#039;&#039;&#039; are specifically worth examining for numerical stability:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Batch Normalization:&#039;&#039;&#039; During inference, batchnorm applies an affine transform using precomputed mean and variance (from training). It’s basically: &amp;lt;math&amp;gt;y = \frac{x - \mu}{\sqrt{\sigma^2 + \epsilon}} \gamma + \beta&amp;lt;/math&amp;gt;. This is a stable operation as long as &amp;lt;math&amp;gt;\epsilon&amp;lt;/math&amp;gt; is provided (to avoid division by zero). Since &amp;lt;math&amp;gt;\mu, \sigma^2&amp;lt;/math&amp;gt; are constants in inference, it reduces to linear ops which are fine. The main numerical work (computing mean/variance) happened during training or model calibration, and frameworks usually did that in FP32. So batchnorm at inference time is not a source of instability (it’s just a scale and shift of the data).&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Layer Normalization / Group Normalization:&#039;&#039;&#039; These compute mean/var on the fly for each sample (and possibly channel group). In inference, they still need to compute these statistics for each forward pass. If implemented in low precision, the variance calculation can suffer cancellation as mentioned. However, it&#039;s likely that inference engines compute the mean and variance in FP32 even if inputs are FP16 (especially since these are per-sample operations, not huge batch reductions – it&#039;s feasible to do in higher precision). If someone implemented layer norm purely in FP16, they might see issues when the variance is very small. Usually there’s an &amp;lt;math&amp;gt;\epsilon&amp;lt;/math&amp;gt; added which might dominate a tiny variance and thus mitigate catastrophic cancellation (the result might just output something mostly from &amp;lt;math&amp;gt;\epsilon&amp;lt;/math&amp;gt;). In summary, normalization layers are usually fine as long as the implementation uses enough precision for the summation of squares.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Softmax:&#039;&#039;&#039; Softmax is notorious if done improperly. The safe algorithm: subtract the maximum input from all inputs (to bring the largest exponent to 1.0), exponentiate, sum them up, divide each exponent by the sum. The max subtraction handles overflow in &amp;lt;math&amp;gt;\exp()&amp;lt;/math&amp;gt;. But what about the sum? As discussed, if there are &amp;lt;math&amp;gt;N&amp;lt;/math&amp;gt; elements in the softmax, the sum is at most &amp;lt;math&amp;gt;N&amp;lt;/math&amp;gt; (when all inputs equal the max). In classification, &amp;lt;math&amp;gt;N&amp;lt;/math&amp;gt; might be the number of classes. For ImageNet, &amp;lt;math&amp;gt;N=1000&amp;lt;/math&amp;gt;, so worst sum ~1000 (which is fine in FP16, as 1000 &amp;lt;&amp;lt; 65504). For an extreme case like a language model, &amp;lt;math&amp;gt;N&amp;lt;/math&amp;gt; could be 50,000 (vocab size). Sum could be 50,000 in worst case (everyone equal probability). 50,000 is still below 65504, so actually an FP16 sum would &#039;&#039;not&#039;&#039; overflow in that scenario, and the rounding error on a sum of 50k identical terms would be maybe a few ULPs (which at 50k magnitude might be ~0.5 difference worst-case). So maybe softmax summation in FP16 is not catastrophic for output probabilities – you’d get essentially 1.0 after normalization instead of 0.99999 in some case. Usually fine.&lt;br /&gt;
&lt;br /&gt;
:The bigger issue is if one or a few terms dominate the softmax. If one term is much larger, the others underflow to 0 after the max subtraction. That’s fine: you get a one-hot distribution essentially. If the model expected a softer distribution, that could be a problem. But if the logits were really that far apart, even FP32 would give nearly one-hot. So not really a precision issue, more of a model property.&lt;br /&gt;
&lt;br /&gt;
:However, the &#039;&#039;&#039;stability during training&#039;&#039;&#039; is a big issue. The GitHub thread we saw (ColossalAI) mentioned training a 130B model where FP16 softmax in attention caused training to occasionally blow up&amp;lt;ref name=&amp;quot;ref14&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref14&amp;quot;/&amp;gt;. They observed attention scores in some heads ranging up to 1e4 or as low as 1e-3&amp;lt;ref name=&amp;quot;ref14&amp;quot;/&amp;gt;. 1e4 exponent in FP16 is definitely Inf (because &amp;lt;math&amp;gt;\exp(10^4)&amp;lt;/math&amp;gt; is astronomically huge, but even exp(100) is inf in half). The method CogView introduced (Precision Bottleneck Relaxation) involved subtracting the max &#039;&#039;per head&#039;&#039; to reduce range&amp;lt;ref name=&amp;quot;ref14&amp;quot;/&amp;gt;—which is basically the softmax max trick at a finer granularity or something similar. In the end, they found &#039;&#039;&#039;the simplest fix was to compute softmax in FP32&#039;&#039;&#039; for those layers&amp;lt;ref name=&amp;quot;ref14&amp;quot;/&amp;gt;. They reported that it had negligible speed impact but significantly improved stability&amp;lt;ref name=&amp;quot;ref14&amp;quot;/&amp;gt;. This highlights a general principle: certain operations like softmax (and sometimes matrix multiplications in attention) benefit from higher precision, and using FP32 in a few critical spots can save a mixed-precision training from crashing. In inference, using FP32 for softmax is also a safeguard if one is uncertain. It costs little because softmax is not a heavy compute compared to matrix multiplies.&lt;br /&gt;
&lt;br /&gt;
:ONNX does have a Softmax operator; if the input is float16, a backend might do exactly what they described: internally cast to float32 for the softmax computation, then cast output back to float16. Some inference engines (like DeepSpeed’s inference or others) keep attention softmax in FP32 even if rest is FP16, for robustness&amp;lt;ref name=&amp;quot;ref14&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Apart from softmax, other places where exponentials occur are in loss functions like cross-entropy (which uses log-softmax internally), or in activation functions like Sigmoid and Tanh (which are basically scaled logistic functions). Sigmoid(&amp;lt;math&amp;gt;x)=1/(1+e^{-x})&amp;lt;/math&amp;gt;. If &amp;lt;math&amp;gt;x&amp;lt;/math&amp;gt; is moderately large positive, &amp;lt;math&amp;gt;e^{-x}&amp;lt;/math&amp;gt; underflows to 0 in FP16 if &amp;lt;math&amp;gt;x&amp;gt;~11&amp;lt;/math&amp;gt;. That gives a sigmoid output of 1.0 exactly. In FP32, &amp;lt;math&amp;gt;e^{-x}&amp;lt;/math&amp;gt; for &amp;lt;math&amp;gt;x=11&amp;lt;/math&amp;gt; is about &amp;lt;math&amp;gt;5e-5&amp;lt;/math&amp;gt;, not underflow, so sigmoid ~0.99995. So FP16 will saturate some outputs to exactly 0 or 1 sooner than FP32 would. This can slightly change behaviors (e.g., a neuron might go to full saturation a bit early). Usually, this isn’t a huge deal, but it could make gradients zero earlier. In inference, we don’t care about gradients, just the output value. Getting exactly 1.0 vs 0.99995 probably has no practical difference for a classification output. But one should be aware that &#039;&#039;&#039;activation functions can saturate faster&#039;&#039;&#039; in lower precision due to limited range or precision. ReLU has no issue (it’s linear or 0), but exponential-based activations do. In practice, again, frameworks often simply compute those in the given precision and accept the slight difference. If it were a problem, one could do those in FP32 as well.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Summary:&#039;&#039;&#039; Numerical stability in inference is maintained by using robust computation methods: subtracting maxima in softmax/LogSumExp, adding epsilon in denominators for normalization, and using higher precision for summations or critical steps. Most of these are built into the algorithms. ONNX models assume these standard practices (they don&#039;t need to explicitly do them in the graph because the backend inherently will for ops like Softmax). It&#039;s wise when building or exporting models to ONNX to test them in the target precision and ensure there are no surprises (like an output becomes NaN or Inf due to precision). Usually if training was done in mixed precision, the model will have been vetted in that environment already.&lt;br /&gt;
&lt;br /&gt;
== Performance vs. Accuracy Trade-offs ==&lt;br /&gt;
&lt;br /&gt;
There is an inherent trade-off between numerical precision and computational performance (speed, memory usage) in AI systems. Choosing lower precision can dramatically increase throughput and reduce memory, but it may introduce arithmetic errors. Engineers must balance these considerations:&lt;br /&gt;
&lt;br /&gt;
=== 7.1. Throughput, Memory Bandwidth, and Precision ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Throughput (speed)&#039;&#039;&#039; is often higher for lower-precision operations. Modern hardware can execute more low-precision ops in parallel than high-precision ops. For example, an NVIDIA V100 GPU can perform 2× more half-precision FLOPs than single-precision per clock, and newer architectures with Tensor Cores can do 8× more (since a Tensor Core warp operation might perform 64 FP16 FMAs per cycle vs 16 FP32). Similarly, int8 operations can be even faster and more energy efficient. Lower precision numbers also use less memory bandwidth – fetching 16-bit data is twice as fast as 32-bit data from memory (assuming the same bus width), which can relieve memory bottlenecks. Many deep learning workloads are memory-bound, so halving the data size can nearly double throughput if compute is not the limiting factor. This is a huge incentive to use lower precision.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Memory and storage&#039;&#039;&#039;: Using FP16 or int8 weights reduces model size, which is important for deploying large models to memory-limited environments (like edge devices or GPUs with limited VRAM). It also means you can fit larger batch sizes in memory, or more models on a chip, etc. There is also a cascade effect: smaller data means better cache utilization, which further improves performance.&lt;br /&gt;
&lt;br /&gt;
However, &#039;&#039;&#039;accuracy&#039;&#039;&#039; suffers if precision is too low. We have discussed how rounding error and limited range can cause deviations in outputs. The question is: do those deviations matter for the application? Often for neural networks, a small amount of noise or error in intermediate calculations does not change the final prediction. Neural nets are somewhat error-tolerant due to their redundant distributed representations. This is why quantization and FP16 techniques can work at all. The final model accuracy (e.g. top-1 accuracy on ImageNet) might drop only 0.5% or less when using FP16 instead of FP32, or int8 instead of FP16, if done properly. For many use-cases, this loss is acceptable given the speed gains.&lt;br /&gt;
&lt;br /&gt;
If one goes too low (like naive FP8 or int4 without special care), accuracy might drop severely – perhaps making the model unusable. Thus the trade-off: as precision decreases, performance increases but at some point accuracy degrades beyond an acceptable threshold. Engineers aim to find the lowest precision that still preserves accuracy.&lt;br /&gt;
&lt;br /&gt;
There is also the consideration of &#039;&#039;&#039;accumulation strategy&#039;&#039;&#039; affecting performance: for instance, Kahan summation (compensated) is more accurate but ~4× slower than naive summation. If you need that accuracy, you pay a performance cost. In many AI inference scenarios, the slight extra accuracy from something like Kahan isn&#039;t needed, so it’s not used. But in a scenario where you sum millions of elements for a final result that is thresholded, maybe you do need it.&lt;br /&gt;
&lt;br /&gt;
Another dimension is &#039;&#039;&#039;determinism vs speed&#039;&#039;&#039;. Enforcing a deterministic reduction order (for reproducibility) might require disabling some parallelism, which could slow things down. If exact repeatability is critical (like certain scientific or financial applications of neural nets), one might sacrifice some performance to ensure the summation happens the same way every time. ONNX allows both modes depending on the runtime.&lt;br /&gt;
&lt;br /&gt;
From a memory perspective, if precision is higher than needed, you waste memory. For example, storing intermediate activations in FP32 when they could be FP16 uses 2× memory, which might mean lower batch size or more memory traffic. So there&#039;s strong motivation to keep things as low-precision as possible &#039;&#039;except&#039;&#039; where accuracy truly demands.&lt;br /&gt;
&lt;br /&gt;
In summary, the trade-off often manifests as &#039;&#039;diminishing returns&#039;&#039;: FP64 vs FP32 – huge slowdown for usually negligible accuracy gain in ML; FP32 vs FP16 – moderate speedup for usually minor accuracy hit; FP16 vs int8 – another big speedup but maybe more accuracy loss unless calibration is done; int8 vs int4 – speed gain but often big accuracy drop that might not be worth it for current models.&lt;br /&gt;
&lt;br /&gt;
Engineers use techniques like &#039;&#039;&#039;mixed precision&#039;&#039;&#039; to target high precision only where needed. For example, run most of the model in FP16, but keep a few sensitive layers in FP32. This yields most of the performance benefits while ensuring critical parts are accurate. We already saw softmax as one example. Another might be final output layer: sometimes using FP32 for the logits and softmax can be safer.&lt;br /&gt;
&lt;br /&gt;
Hardware is also trending to support mixed modes – like FP8 Tensor Cores accumulate in FP32, etc. So hardware tries to give accuracy cheaply.&lt;br /&gt;
&lt;br /&gt;
=== 7.2. Hardware Accelerators and Tensor Cores ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Hardware accelerators&#039;&#039;&#039; (GPUs, TPUs, FPGAs, ASICs like GraphCore IPU, etc.) have specific support for certain precisions. Leveraging these can provide massive speedups:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;NVIDIA Tensor Cores:&#039;&#039;&#039; These started in Volta (V100) with support for 4x4 matrix multiply-adds on FP16 inputs with FP32 accumulation&amp;lt;ref name=&amp;quot;ref6&amp;quot;/&amp;gt;. Using Tensor Cores can give up to 8x throughput compared to using regular FP32 cores. But to use them, the data needs to be FP16 (or now BF16 or FP8 on newer GPUs) and aligned to certain dimensions (multiples of 8 etc. in matrix sizes, which frameworks handle by padding if needed). If an ONNX model is all FP32, by default it would not use Tensor Cores unless the runtime automatically converts some ops to mixed precision (which some do via a flag or environment variable enabling TF32 or FP16). The introduction of &#039;&#039;&#039;TensorFloat-32 (TF32)&#039;&#039;&#039; in Ampere GPUs is essentially a compromise: treat FP32 inputs as two 16-bit significand halves (10-bit mantissa actually) so that it can run on Tensor Cores, giving ~8x speed, while still outputting FP32. TF32 has about the precision of FP16 but range of FP32, and it was chosen because it typically doesn’t hurt convnet accuracy. ONNX Runtime or others on Ampere will use TF32 by default for conv and matmul unless disabled, giving a free speed boost at minimal accuracy cost. If exact FP32 is needed, users have to turn it off (sacrificing speed).&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;TPU (Google):&#039;&#039;&#039; TPUs have native support for BF16 matrix ops with FP32 accumulators. They were designed specifically for the mixed precision approach in training. For inference, TPUs also support int8 and other quantized ops (e.g., via Edge TPU for lower-power). If exporting ONNX to run on a TPU (via some bridge), one might get automatic BF16 usage. BF16 has the advantage of no loss in range, so TPUs can more straightforwardly run models in BF16 without modifications.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;FPGAs and ASICs:&#039;&#039;&#039; Some custom accelerators might use fixed-point or block floating-point arithmetic for efficiency. They often treat the network in quantized terms. The ONNX quantization specification (QuantizeLinear/DequantizeLinear ops) exists for representing quantized models (INT8 etc.), which is another domain beyond floating-point, but it overlaps as a precision issue. In those systems, accumulators for INT8 × INT8 multiplies are usually 32-bit (to avoid overflow for dot products up to certain lengths). The trade-off there is between integer vs float representation, which can give big speedups on CPUs/DSAs via SIMD instructions (many CPUs have vector int8 multiply-add instructions now for inference).&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Memory bandwidth on hardware:&#039;&#039;&#039; Some accelerators include special high-bandwidth memory or cache specifically for lower precision. For example, an accelerator might fetch two FP16 values in one 32-bit memory transaction – effectively doubling memory bandwidth for half precision. On some hardware, using FP16 can even reduce memory &#039;&#039;latency&#039;&#039; if multiple values can be packed.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Parallelism and Vectorization:&#039;&#039;&#039; Many hardware units process data in fixed vector widths (e.g., 256-bit registers). That could be 8 floats at once or 16 half-floats at once in the same 256 bits. If your code is in FP16, you might use the full vector width (16 values) whereas FP32 might only use half (8 values). This means FP16 can make better use of vector units. Similarly, matrix units often have fixed tile sizes, and smaller data types allow larger tiles or more tiles concurrently.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Energy efficiency:&#039;&#039;&#039; Lower precision often means less energy per operation, which on large deployments (data centers or battery-powered devices) is a significant advantage. This isn&#039;t directly performance, but it&#039;s an important trade: you might choose a slightly lower precision to save power even if accuracy drops a tiny bit.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Software overhead:&#039;&#039;&#039; Using mixed precision on hardware involves some overhead of casting and moving data between precisions. For instance, if you have to frequently cast FP16 to FP32 and back, that adds instructions and memory copies. A well-designed accelerator (like GPUs with unified memory for FP16/32 or special hardware casting) minimize this. Still, the runtime might insert extra operations (as we saw, ONNX Runtime CPU inserted many Cast nodes converting FP16 to FP32 and back&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;, which actually &#039;&#039;slowed down&#039;&#039; that FP16 model by 4x vs FP32 on a CPU, because the CPU had no native FP16). So ironically, on some hardware, using lower precision in ONNX might degrade performance if not supported (the CPU case). Always ensure the target hardware actually supports the low precision you plan to use; otherwise, you&#039;ll pay overhead for emulation.&lt;br /&gt;
&lt;br /&gt;
To illustrate trade-off: Suppose you have a model that achieves 99% accuracy in FP32 and drops to 98.5% in FP16. If FP16 runs 2x faster, many would accept the 0.5% accuracy drop for double throughput. If int8 runs 4x faster but accuracy drops to 95%, that might be too much drop for some applications, but acceptable for others that value speed. It&#039;s application-dependent.&lt;br /&gt;
&lt;br /&gt;
In critical applications, one might maintain higher precision accumulators or run certain validation steps in high precision to ensure nothing drifted. In research, sometimes a model is trained in mixed precision but validated in double to ensure any small numeric differences don’t influence the reported result (rare, but for scientific computing integration maybe).&lt;br /&gt;
&lt;br /&gt;
In the ONNX world, performance-vs-accuracy is often managed via &#039;&#039;&#039;Execution Providers&#039;&#039;&#039; (EPs) that target specific hardware. For example, the TensorRT EP in ONNX Runtime can take an ONNX model and run it using NVIDIA&#039;s high-performance inference library, which will use FP16 or INT8 if allowed, to maximize speed. The user can configure the EP with precision flags. If the accuracy drop is too high, one might restrict it to FP32. If it&#039;s fine, allow FP16. Tools exist to measure the accuracy difference (like calibrating int8 quantization).&lt;br /&gt;
&lt;br /&gt;
To conclude, the trade-offs revolve around picking the lowest precision that yields acceptable accuracy, and utilizing hardware features fully. ONNX being a high-level spec abstracts this, but it&#039;s ultimately up to the deployment to decide and sometimes up to the exporter to incorporate quantization. ONNX’s role is to be able to &#039;&#039;represent&#039;&#039; these choices (they have added quantized types, FP8 types, etc., to allow the graph to include those when needed).&lt;br /&gt;
&lt;br /&gt;
== Best Practices and Design Guidelines ==&lt;br /&gt;
&lt;br /&gt;
Given the above insights, here are some best practices and guidelines for dealing with numerical precision and accumulators in AI inference (and training) design:&lt;br /&gt;
&lt;br /&gt;
=== 8.1. Choosing an Accumulator Precision ===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Match precision to data length and range:&#039;&#039;&#039; If an operation accumulates a large number of terms, consider using a higher precision accumulator than the input data. For example, for summing &amp;gt;1000 values of type FP16, an FP32 accumulator is recommended to avoid large rounding error or overflow. If summing &amp;gt; million values of FP32, consider double (FP64) accumulation if high accuracy is needed (though this is rare in DL). &#039;&#039;Rule of thumb:&#039;&#039; if &amp;lt;math&amp;gt;N \cdot \varepsilon&amp;lt;/math&amp;gt; is not tiny (say &amp;gt;1e-3), you may benefit from higher precision. For FP16 (&amp;lt;math&amp;gt;\varepsilon \approx 1e-3&amp;lt;/math&amp;gt;), that threshold N is a few hundred. For FP32 (&amp;lt;math&amp;gt;\varepsilon \approx 1e-7&amp;lt;/math&amp;gt;), N would be in the millions before 1e-3 error.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Use higher precision in critical layers:&#039;&#039;&#039; Identify if certain layers of the model are more sensitive to precision. For instance, the output layer or a layer that feeds into a very sharp decision boundary might require more precision. If a small change there could flip the prediction, you don&#039;t want excessive numerical error. Using FP32 for logits and softmax is a common practice to maintain classification confidence stability.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Follow hardware defaults, but override if needed:&#039;&#039;&#039; Many hardware accelerators have sensible defaults (like accumulate in FP32 for tensor ops). It&#039;s usually best to use those defaults as they were chosen to balance accuracy and speed. Only override if you have evidence of a problem. For example, if using ONNX Runtime on CPU, you discovered it upcasts to float for you – great. But if using some custom accelerator that does pure FP16, you might manually insert casts in the ONNX graph around certain ops to force higher precision on those ops.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Maintain precision for gradient accumulation (training):&#039;&#039;&#039; This is more training-oriented, but worth noting: when summing gradients over batches or updating weights, use higher precision (FP32 or even 64) for those accumulations. This ensures training doesn&#039;t diverge due to numeric error. Many frameworks keep a &amp;quot;master copy&amp;quot; of weights in FP32 when training in FP16&amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;. For inference, this translates to: any running statistics or counters in the model (like if you had a running average inside the model) should probably be in higher precision to avoid drift.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Be mindful of intermediate cast costs:&#039;&#039;&#039; If you do decide to cast to higher precision for an operation in ONNX, remember it incurs memory copies and compute overhead. Make sure it&#039;s worth it. For example, if you cast a huge tensor to double, that could slow things drastically. Usually FP16-&amp;gt;FP32 is fine (hardware handles it well), but FP32-&amp;gt;FP64 might be expensive on GPUs.&lt;br /&gt;
&lt;br /&gt;
=== 8.2. Safe Reduction Lengths and Heuristics ===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Chunk long reductions:&#039;&#039;&#039; If you must do a very long sum in low precision (due to memory constraints or hardware), chunk the sum into parts, sum each to a higher precision local accumulator, then sum the partial sums. This is essentially pairwise summation. Many libraries use a fixed chunk size heuristic (e.g., sum in blocks of 256 in registers, then add those up). This bounds the error growth. If implementing manually, you can choose a chunk size such that chunk_length &#039;&#039; ε is small (like 256 &#039;&#039; 1e-3 = 0.256 for FP16, hmm maybe choose 100).&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Sort by magnitude if possible:&#039;&#039;&#039; In some cases (not typical in DL, but possible in post-processing), if you have numbers varying by orders of magnitude, summing from smallest to largest improves accuracy. It prevents small ones from being swallowed by big ones. This is more relevant if you output a sum of mixed-scale values (like some algorithm mixing big and small contributions). It’s not always feasible in real-time inference to sort, but keep in mind.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Use compensation for critical sums:&#039;&#039;&#039; If you find a particular sum or difference in your inference computation is crucial (like computing a small residual from two close numbers), consider using Kahan summation or another compensated scheme just for that part. For example, if you had an ONNX custom function that needs to sum a lot of almost cancelling terms, you might implement it with compensation internally.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Heuristics for FP8/low-bit:&#039;&#039;&#039; When using extremely low precision like FP8, adopt the recommended practices: use scaling (don&#039;t feed raw data into FP8, normalize it first), and perhaps only use FP8 on portions of the network that are empirically robust. The industry is still establishing heuristics, but one might be: use FP8 for matrix multiplications, but accumulate in FP16 or FP32; keep normalization in FP16; use FP16 for first and last layer maybe. This is evolving, so keep an eye on literature.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Keep an eye on sum length in softmax and normalization:&#039;&#039;&#039; If you have extremely large class counts or group sizes (e.g., a softmax over 1e6 items, or layer norm over tens of thousands of features), realize this is a giant reduction. You may want to ensure that reduction is done in higher precision. ONNX Softmax by default will likely not handle a million-class case in FP16 well (the sum would overflow since 1e6 &amp;gt; 65504). So either do that on CPU in FP32 or break it down (could do hierarchical softmax, etc.). This is a corner case, but relevant for huge vocab language models. In practice, vocab ~50k is fine as we said.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Test numeric differences:&#039;&#039;&#039; A practical tip: after quantizing or reducing precision, run some test inputs through both the high-precision model and low-precision model. Compare outputs (and maybe some intermediate signals if possible). If differences are consistently small relative to the application tolerance, you&#039;re good. If you see occasional large differences or instabilities (like outputs become NaN or Inf in low precision), that&#039;s a red flag. Use that testing to guide where you might need to increase precision or change algorithms.&lt;br /&gt;
&lt;br /&gt;
=== 8.3. When Higher Precision Is Required ===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Ill-conditioned calculations:&#039;&#039;&#039; If your model or pipeline performs any operation known to be ill-conditioned (matrix inversion, subtraction of nearly equal quantities, computing a tiny difference of large numbers), you likely need higher precision for that part. For example, if your ONNX model includes a custom op to solve a linear system, use double precision in that op if possible (or at least float with iterative refinement). In ML, this might occur in something like a physics-informed neural network or a differentiable simulation within the model.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Strict accuracy requirements:&#039;&#039;&#039; Some applications (medical, aerospace, finance) might have strict tolerances. If an inference model’s output needs to be correct to 5 decimal places, you probably cannot use FP16, which has only 3-4 decimal digits precision. FP32 might be barely enough (~7 digits). FP64 might be warranted to guarantee that level of accuracy in worst case. Most deep learning applications (like image classification) don&#039;t demand that level of numeric precision in outputs – as long as the top class is correct, it doesn&#039;t matter if its probability was 0.901 vs 0.899. But if you&#039;re doing something like summing probabilities to make a risk estimate or doing a cumulative sum over many predictions, be careful to accumulate precisely.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Large sums or products in post-processing:&#039;&#039;&#039; Sometimes the neural net is fine, but the &#039;&#039;post-processing&#039;&#039; of results might need precision. E.g., summing up probabilities of many events, or computing confidence intervals, etc. Do those calculations in double if needed outside the model.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Preventing drift in long sequences:&#039;&#039;&#039; For sequential models (RNNs processing thousands of steps), rounding error can accumulate over steps. If each step has a small error, after 10000 steps it could add up. In such cases, using a higher precision for the state might reduce drift. Some practitioners use FP64 for the hidden state accumulation in very long sequence processing to maintain numerical stability (though this is not common due to speed loss). Alternatively, periodically renormalize or reset small errors.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Simulations or differential equations:&#039;&#039;&#039; If your ONNX model involves simulating something (like a differential equation solver inside a model, or an ODE/RNN hybrid), those often need double precision to be stable for long durations. Recognize those situations and allocate precision accordingly.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Verification and debugging:&#039;&#039;&#039; Use higher precision as a tool to verify correctness. For example, run your model in FP64 (if possible) and compare to FP32 to see if there are differences. If not, FP32 is fine. If yes, you might find a particular calculation losing too much info in FP32, implying maybe you should keep it in FP64 in production as well. This is rare in inference but can be part of rigorous validation.&lt;br /&gt;
&lt;br /&gt;
In ONNX, using higher precision may involve casting to &amp;lt;code&amp;gt;tensor(double)&amp;lt;/code&amp;gt; for certain ops. Not all runtimes support double on GPU though (some do via emulation or just slower path). ONNX CPU does support double for general ops. So be mindful of the target.&lt;br /&gt;
&lt;br /&gt;
Finally, &#039;&#039;&#039;document your precision choices&#039;&#039;&#039;. If you deliver an ONNX model that uses mixed types (float16 inputs, float32 accumulations, int8 weights, etc.), ensure that is clear to whoever maintains or uses it. Precision issues can be sneaky, so clarity helps.&lt;br /&gt;
&lt;br /&gt;
== Limitations and Open Problems ==&lt;br /&gt;
&lt;br /&gt;
Despite advances in handling numerical precision, there remain limitations and open challenges in this domain:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Lack of Precision Specification in ONNX:&#039;&#039;&#039; As noted, ONNX currently has no way to formally specify that an op &#039;&#039;should&#039;&#039; use a higher precision internally. This can lead to nondeterministic differences across platforms&amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;. While proposals exist to add attributes for this&amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;, it is not yet standard. Thus an ONNX model might behave slightly differently on two compliant backends, and there&#039;s no built-in mechanism to ensure identical results. This is a limitation for applications requiring cross-platform consistency. Solving it may involve extending ONNX (e.g., an attribute like &amp;lt;code&amp;gt;accumulatePrecision=&amp;quot;FLOAT32&amp;quot;&amp;lt;/code&amp;gt; for MatMul) or tightening the spec, but those come with backward compatibility and implementation burden concerns.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Reproducibility and Determinism:&#039;&#039;&#039; Even with the same precision, parallel computation can introduce nondeterminism (due to different summation orders). Ensuring bit-identical results across runs (important for some sensitive domains) is non-trivial. Frameworks often provide &amp;quot;deterministic mode&amp;quot; which might use fixed ordering at cost of speed. In ONNX, there&#039;s no global flag for this; it&#039;s up to the runtime. More research could be done on algorithms that produce bit-wise identical results independent of parallel execution order (perhaps using techniques like pairwise summation or consistent partitioning). Stochastic rounding is another area: sometimes using probabilistic rounding can reduce bias in accumulated error, but it introduces randomness – reconciling that with reproducibility is tricky.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Ultra-low Precision (FP8 and below):&#039;&#039;&#039; While FP8 is being explored, going to 4-bit floats or mixed schemes is an open challenge. The OpenAI community is investigating 4-bit (quantization-wise) for LLMs with some success, but a true 4-bit float (like E3M1) would be extremely limited. Perhaps hybrid schemes (like block floating point, log scales, etc.) will come. ONNX already has some support (they mention 4-bit in documentation placeholders&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;), but it&#039;s nascent. Research into training techniques, calibration, and new number formats (like &#039;&#039;&#039;posit numbers&#039;&#039;&#039; or tapering precision floats) is ongoing. If a new format proves useful, ONNX will need to adapt to represent it.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Quantization Noise and Model Robustness:&#039;&#039;&#039; Reducing precision can sometimes expose models to issues like adversarial vulnerability or fragility – e.g., a slight perturbation might flip a decision if the margin was small and precision loss nudged it. Ensuring model robustness under quantization is an open problem. It might require re-training the model with quantization in the loop (quantization-aware training) or designing models that have large &amp;quot;margin&amp;quot; between classes to tolerate quantization noise.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Dynamic range during training vs inference:&#039;&#039;&#039; In training, activations can have wider distributions (especially early in training). Models often learn to constrain their ranges (via activation functions saturating or normalization), which makes inference more stable in lower precision. But if one tries to train in FP8 from scratch, it&#039;s very hard. This gap between what&#039;s possible in a post-trained inference model vs training is an open area – how to train directly in low precision without significant accuracy loss. Solving that could simplify workflows (no need for high-precision training then quantize; just train low-prec end to end). Techniques like loss scaling, gradient clipping, etc., are partial solutions. The development of optimizers that are resilient to low precision is ongoing.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Hardware variability:&#039;&#039;&#039; As new hardware emerges (with new numeric capabilities or quirks), ensuring that ONNX models run consistently is an ongoing task. For instance, one device might implement a fused op that reduces rounding error, another does it separately. Even something like convolution might use a different algorithm (FFT-based vs direct) which has different numeric properties. Open problem: could ONNX allow specifying numeric tolerances per op or preferred algorithms? That may be overkill for most, but for some HPC ML use cases, maybe desired.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Testing and validation of numerics:&#039;&#039;&#039; There isn&#039;t a standardized set of &amp;quot;numeric compliance&amp;quot; tests for ONNX runtimes beyond simple correctness. Perhaps in the future, test suites might include challenging numeric scenarios (large sums, nearly singular matrices) to ensure a runtime handles them gracefully (maybe by switching precision). Right now, it&#039;s mostly on the user to test their model under expected conditions.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Combining quantization with floating point:&#039;&#039;&#039; Many production models use a mix (e.g., int8 for some layers, FP16 for others). ONNX can represent this (with QuantizeLinear ops or MixedPrecision through casting), but tooling to automatically find the optimal mix is still developing. It&#039;s somewhat trial-and-error or using heuristics. An open question is how to automate precision assignment: given a model and a target hardware, how to automatically choose which layers can be int8, which should be FP16, etc., to meet an accuracy target. This crosses into NAS (neural architecture search) territory or automated quantization. &lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Numerical debugging tools:&#039;&#039;&#039; When a model goes wrong due to precision, it&#039;s often hard to pinpoint where. Better tooling to analyze numerical error layer by layer could be useful. For instance, one could run a model in double and float and diff the intermediate results to see where the largest divergence happens. Doing this systematically for big nets is an open tooling challenge. Some frameworks have hooks for this, but not standardized.&lt;br /&gt;
&lt;br /&gt;
In conclusion, while we&#039;ve come far with using lower precision in AI without sacrificing much accuracy, the landscape is always evolving. The key open problem is how to push precision lower (for efficiency gains) without crossing the threshold where accuracy falls off a cliff. Each new generation of hardware and techniques (like FP8, novel quantization schemes) extends that boundary, but also brings new questions on how to manage the numerical errors. ONNX as an interchange will need to keep pace, possibly by adding more expressive power for these numeric considerations.&lt;br /&gt;
&lt;br /&gt;
== See Also ==&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;IEEE 754 Floating-Point&#039;&#039;&#039; – The standard defining binary16, binary32, binary64 formats and rounding behavior.&lt;br /&gt;
* &#039;&#039;&#039;Floating-Point Arithmetic&#039;&#039;&#039; – General topic on how floating-point math works, common issues (overflow, underflow, cancellation).&lt;br /&gt;
* &#039;&#039;&#039;Kahan Summation Algorithm&#039;&#039;&#039; – Algorithm for compensated summation to reduce numerical error&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;.&lt;br /&gt;
* &#039;&#039;&#039;BFloat16&#039;&#039;&#039; – A short-format float with 8-bit exponent, used in TPUs and some GPUs for training networks.&lt;br /&gt;
* &#039;&#039;&#039;Mixed Precision Training&#039;&#039;&#039; – Technique of using FP16 (or BF16) along with FP32 accumulators to accelerate training&amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;.&lt;br /&gt;
* &#039;&#039;&#039;Quantization in Deep Learning&#039;&#039;&#039; – Converting networks to lower bit integer arithmetic for inference (INT8 quantization and calibration techniques).&lt;br /&gt;
* &#039;&#039;&#039;ONNX Operator Specification&#039;&#039;&#039; – Official docs for ONNX ops, which describe their mathematical function but not the required numeric precision (a point of potential extension in future).&lt;br /&gt;
* &#039;&#039;&#039;CUDA Tensor Cores &amp;amp; TF32&#039;&#039;&#039; – NVIDIA documentation on how tensor cores perform mixed precision and the TF32 format for accelerating FP32.&lt;br /&gt;
* &#039;&#039;&#039;Pairwise Summation&#039;&#039;&#039; – Summation algorithm with logN error growth&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;, used in many numerical libraries to improve accuracy of reductions.&lt;br /&gt;
* &#039;&#039;&#039;Softmax Stability&#039;&#039;&#039; – Articles on numerically stable softmax and log-sum-exp implementations to avoid overflow.&lt;br /&gt;
* &#039;&#039;&#039;Performance Tuning for Low Precision&#039;&#039;&#039; – Guides (e.g., NVIDIA or Intel whitepapers) on using low precision and the trade-offs involved in various hardware.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;references&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref1&amp;quot;&amp;gt;Accelerate and simplify Scikit-learn model inference with ONNX Runtime - Microsoft Open Source Blog — opensource.microsoft.com — https://opensource.microsoft.com/blog/2020/12/17/accelerate-simplify-scikit-learn-model-inference-onnx-runtime&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref2&amp;quot;&amp;gt;Floating Point Precision: Understanding FP64, FP32, and FP16 in Large Language Models - DEV Community — dev.to — https://dev.to/lukehinds/floating-point-precision-understanding-fp64-fp32-and-fp16-in-large-language-models-3gk6&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref3&amp;quot;&amp;gt;Floating Point Precision and Its Limitations | by Umair Akbar | Medium — akbu.medium.com — https://akbu.medium.com/floating-point-precision-and-its-limitations-cfb7247d7789&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref4&amp;quot;&amp;gt;Kahan summation algorithm - Wikipedia — en.wikipedia.org — https://en.wikipedia.org/wiki/Kahan_summation_algorithm&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref5&amp;quot;&amp;gt;Pairwise summation - Wikipedia — en.wikipedia.org — https://en.wikipedia.org/wiki/Pairwise_summation&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref6&amp;quot;&amp;gt;Numerical behavior of NVIDIA tensor cores - PMC — pmc.ncbi.nlm.nih.gov — https://pmc.ncbi.nlm.nih.gov/articles/PMC7959640/&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref7&amp;quot;&amp;gt;Mixed precision and performance-accuracy tuning (neuron-cc) — AWS Neuron Documentation — awsdocs-neuron.readthedocs-hosted.com — https://awsdocs-neuron.readthedocs-hosted.com/en/latest/about-neuron/appnotes/neuron-cc/mixed-precision.html&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref8&amp;quot;&amp;gt;Create Float16 and Mixed Precision Models - ONNX Runtime — onnxruntime.ai — https://onnxruntime.ai/docs/performance/model-optimizations/float16.html&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref9&amp;quot;&amp;gt;[Performance] ONNX FP16 model is having performance bottle neck when compared to FP32 variant · Issue #25824 · microsoft/onnxruntime · GitHub — github.com — https://github.com/microsoft/onnxruntime/issues/25824&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref10&amp;quot;&amp;gt;Float stored in 8 bits - ONNX 1.21.0 documentation — onnx.ai — https://onnx.ai/onnx/technical/float8.html&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref11&amp;quot;&amp;gt;[2209.05433] FP8 Formats for Deep Learning — arxiv.org — https://arxiv.org/abs/2209.05433&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref12&amp;quot;&amp;gt;Should MatMul op allow specification of accumulation precision? · Issue #7072 · onnx/onnx · GitHub — github.com — https://github.com/onnx/onnx/issues/7072&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref13&amp;quot;&amp;gt;Quantize ONNX models | onnxruntime — onnxruntime.ai — https://onnxruntime.ai/docs/performance/model-optimizations/quantization.html&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref14&amp;quot;&amp;gt;Keep Attention Softmax FP32 during FP16/ZeRO Training · Issue #1485 · hpcaitech/ColossalAI · GitHub — github.com — https://github.com/hpcaitech/ColossalAI/issues/1485&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;/references&amp;gt;&lt;/div&gt;</summary>
		<author><name>Timo.stripf</name></author>
	</entry>
	<entry>
		<id>https://www.emmtrix.com/w139/index.php?title=Numerical_Precision_in_ONNX_and_AI_Inference&amp;diff=2903</id>
		<title>Numerical Precision in ONNX and AI Inference</title>
		<link rel="alternate" type="text/html" href="https://www.emmtrix.com/w139/index.php?title=Numerical_Precision_in_ONNX_and_AI_Inference&amp;diff=2903"/>
		<updated>2026-01-30T15:26:41Z</updated>

		<summary type="html">&lt;p&gt;Timo.stripf: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&#039;&#039;&#039;Open Neural Network Exchange (ONNX)&#039;&#039;&#039; is an open standard format for representing machine learning models and neural network computations across different frameworks and hardware&amp;lt;ref name=&amp;quot;ref1&amp;quot;/&amp;gt;. As models are exported and deployed via ONNX, the &#039;&#039;&#039;numerical precision&#039;&#039;&#039; of computations becomes critical. Deep learning inference involves a variety of floating-point operations, and small numerical discrepancies can accumulate and affect model accuracy or reproducibility. This article provides an in-depth overview of floating-point precision issues in numerical computing, with a focus on how they manifest in AI inference and the ONNX ecosystem. We cover floating-point formats and rounding behavior, accumulation strategies for summations, the use of low-precision formats (FP16, BFloat16, FP8) in modern AI, and how ONNX defines (or leaves unspecified) the precision of accumulators in operations. Strategies for maintaining numerical stability in deep neural networks are discussed, along with the trade-offs between computational performance and arithmetic accuracy. The goal is to give engineers and researchers a comprehensive, encyclopedic reference on numerical precision in ONNX-based AI inference systems.&lt;br /&gt;
&lt;br /&gt;
== Floating-Point Precision in Numerical Computing ==&lt;br /&gt;
&lt;br /&gt;
=== 2.1. IEEE 754 Floating-Point Formats ===&lt;br /&gt;
&lt;br /&gt;
Modern computers represent real numbers using the IEEE 754 floating-point standard, which defines formats like 32-bit &#039;&#039;single precision&#039;&#039; (FP32) and 64-bit &#039;&#039;double precision&#039;&#039; (FP64). In IEEE 754 binary formats, a floating-point number is composed of three fields: 1 sign bit, several exponent bits (with a bias), and several fraction (mantissa) bits. For example, FP32 consists of 1 sign bit, 8 exponent bits, and 23 fraction bits (24 bits of significand precision including the implicit leading 1)&amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;. This provides roughly 7 decimal digits of precision and an exponent range allowing values on the order of &amp;lt;math&amp;gt;10^{\pm38}&amp;lt;/math&amp;gt;. FP64 allocates 1 sign bit, 11 exponent bits, and 52 fraction bits (53-bit precision), yielding about 15–16 decimal digits of precision and a vastly larger range (~&amp;lt;math&amp;gt;10^{\pm308}&amp;lt;/math&amp;gt;)&amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;. These larger formats (especially FP64) are the &amp;quot;gold standard&amp;quot; for numerical accuracy in scientific computing, as they reduce round-off error in complex calculations&amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;. However, the increased precision comes at the cost of doubled storage and typically reduced computational throughput (e.g. many GPUs execute FP64 instructions at a much lower rate than FP32&amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
In addition to single and double precision, the IEEE 754-2008 standard introduced a 16-bit &#039;&#039;half precision&#039;&#039; format (FP16, or binary16). FP16 has 1 sign bit, 5 exponent bits, and 10 fraction bits (11-bit significand including the implicit 1)&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. This format covers a much smaller dynamic range (approximately &amp;lt;math&amp;gt;6.1\times10^{-5}&amp;lt;/math&amp;gt; to &amp;lt;math&amp;gt;6.5\times10^4&amp;lt;/math&amp;gt; for normalized values&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;) and offers only around 3 decimal digits of precision. Half precision was originally used in graphics, but it has gained popularity in machine learning for its speed and memory advantages (discussed later in Section 4). Another format relevant to AI is &#039;&#039;&#039;BFloat16&#039;&#039;&#039; (Brain Float 16), a 16-bit float with &#039;&#039;&#039;8 exponent bits and 7 fraction bits&#039;&#039;&#039;&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. BFloat16 sacrifices precision (only ~7 fraction bits ≈ 2–3 decimal digits) in order to have the &#039;&#039;&#039;same exponent range as FP32&#039;&#039;&#039;&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. This means BFloat16 can represent very large or very small numbers (∼&amp;lt;math&amp;gt;10^{\pm38}&amp;lt;/math&amp;gt;, similar range as FP32) but with much less precision between representable values&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. BFloat16 was introduced for deep learning by Google (for TPUs) to allow reduced precision &#039;&#039;training&#039;&#039; without frequent overflow/underflow, relying on the wide range and accepting the coarse precision.&lt;br /&gt;
&lt;br /&gt;
Each floating-point format has a &#039;&#039;&#039;unit roundoff&#039;&#039;&#039; or machine precision, denoted &amp;lt;math&amp;gt;\epsilon&amp;lt;/math&amp;gt;. This is effectively the spacing between 1.0 and the next representable number. For FP32, &amp;lt;math&amp;gt;\epsilon \approx 2^{-23} \approx 1.19\times10^{-7}&amp;lt;/math&amp;gt;, corresponding to about 7–8 decimal digits of precision&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. In FP64, &amp;lt;math&amp;gt;\epsilon = 2^{-52} \approx 2.22\times10^{-16}&amp;lt;/math&amp;gt; (15–16 decimal digits). Half precision is much less precise: FP16 has &amp;lt;math&amp;gt;\epsilon = 2^{-10} \approx 9.76\times10^{-4}&amp;lt;/math&amp;gt;&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;, and BFloat16 has &amp;lt;math&amp;gt;\epsilon = 2^{-7} = 7.8125\times10^{-3}&amp;lt;/math&amp;gt;&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. These values quantify how finely the continuum of real numbers is quantized in each format.&lt;br /&gt;
&lt;br /&gt;
=== 2.2. Machine Epsilon and Rounding Error ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Machine epsilon (&amp;lt;math&amp;gt;\varepsilon&amp;lt;/math&amp;gt;)&#039;&#039;&#039; is formally defined as the smallest positive number such that &amp;lt;math&amp;gt;1.0 + \varepsilon &amp;gt; 1.0&amp;lt;/math&amp;gt; in the floating-point format&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. It effectively measures the &#039;&#039;&#039;precision&#039;&#039;&#039; of the representation. In FP32, &amp;lt;math&amp;gt;\varepsilon = 2^{-23} \approx 1.19\times10^{-7}&amp;lt;/math&amp;gt;, meaning any result is rounded to about 7 decimal digits. FP16’s &amp;lt;math&amp;gt;\varepsilon = 2^{-10} \approx 9.77\times10^{-4}&amp;lt;/math&amp;gt;, over 1000× larger (so only ~3 decimal digits of precision), and BFloat16’s &amp;lt;math&amp;gt;\varepsilon = 2^{-7} \approx 7.8\times10^{-3}&amp;lt;/math&amp;gt;, reflecting its mere ~8-bit precision&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. Rounding in IEEE 754 by default uses &#039;&#039;&#039;round-to-nearest (ties to even)&#039;&#039;&#039;, which ensures the rounding error for any single operation is at most half a unit in the last place (0.5 ULP). We can model a floating-point addition as: &lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;$\text{fl}(a+b) = (a+b)\,(1 + \delta),&amp;lt;/math&amp;gt;$ &lt;br /&gt;
&lt;br /&gt;
where &amp;lt;math&amp;gt;|\delta| \le \varepsilon&amp;lt;/math&amp;gt; for that format&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. Here &amp;lt;math&amp;gt;\varepsilon/2&amp;lt;/math&amp;gt; is the maximum &#039;&#039;&#039;relative error&#039;&#039;&#039; in one rounding (also called unit roundoff). For example, when adding two FP32 numbers, the result is accurate to within about &amp;lt;math&amp;gt;10^{-7}&amp;lt;/math&amp;gt; of the true sum (relative), whereas in BF16, the result can deviate by up to &amp;lt;math&amp;gt;\sim7.8\times10^{-3}&amp;lt;/math&amp;gt; (nearly 0.78% relative error per operation)&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. Thus, &#039;&#039;&#039;each arithmetic operation incurs a small rounding error&#039;&#039;&#039; (on the order of machine epsilon) that can propagate or accumulate through a sequence of calculations.&lt;br /&gt;
&lt;br /&gt;
Because floating-point representations have finite exponent range, we must also consider &#039;&#039;&#039;overflow&#039;&#039;&#039; and &#039;&#039;&#039;underflow&#039;&#039;&#039;. If a result’s magnitude exceeds the maximum representable value, it overflows to infinity; if it falls below the minimum normal (or minimum subnormal) value, it underflows to zero (or a denormalized tiny value). FP32 and BF16 share an exponent size (8 bits), so their range for normal numbers is roughly &amp;lt;math&amp;gt;[10^{-38}, 10^{38}]&amp;lt;/math&amp;gt; (approx &amp;lt;math&amp;gt;~\!3.4\times10^{38}&amp;lt;/math&amp;gt; max)&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. FP16’s 5-bit exponent allows max &amp;lt;math&amp;gt;\sim 6.55\times10^4&amp;lt;/math&amp;gt; and min normal &amp;lt;math&amp;gt;\sim 6.10\times10^{-5}&amp;lt;/math&amp;gt;&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. In practice, &#039;&#039;&#039;underflow/overflow&#039;&#039;&#039; can occur in low-precision arithmetic if values are not appropriately scaled. For instance, summing a large number of moderately sized terms in FP16 could overflow where FP32 would not. As an example, summing on the order of &amp;lt;math&amp;gt;10^4&amp;lt;/math&amp;gt; values of magnitude ~&amp;lt;math&amp;gt;10^1&amp;lt;/math&amp;gt; (say, adding 10,000 terms around 10 each) would produce a total &amp;lt;math&amp;gt;\sim10^5&amp;lt;/math&amp;gt;, which exceeds FP16’s max finite value ~&amp;lt;math&amp;gt;6.5\times10^4&amp;lt;/math&amp;gt; and would overflow to &amp;lt;math&amp;gt;+\infty&amp;lt;/math&amp;gt; in half precision&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. Underflow is usually a concern when subtracting nearly equal numbers (catastrophic cancellation yielding a tiny difference that might flush to zero in low precision).&lt;br /&gt;
&lt;br /&gt;
=== 2.3. Accumulation Error in Reductions ===&lt;br /&gt;
&lt;br /&gt;
Floating-point addition is &#039;&#039;&#039;not associative&#039;&#039;&#039;: &amp;lt;math&amp;gt;(a+b)+c&amp;lt;/math&amp;gt; can differ from &amp;lt;math&amp;gt;a+(b+c)&amp;lt;/math&amp;gt; due to rounding, and thus the order in which we sum a list of numbers can change the result slightly. When summing a large sequence of &amp;lt;math&amp;gt;N&amp;lt;/math&amp;gt; numbers &amp;lt;math&amp;gt;x_1, x_2, ..., x_N&amp;lt;/math&amp;gt;, the &#039;&#039;&#039;accumulated rounding error&#039;&#039;&#039; can be significant. In the &#039;&#039;worst case&#039;&#039; (e.g., adding many positive and negative terms that cause maximal cancellation or always rounding one way), the error can grow on the order of &amp;lt;math&amp;gt;O(N \varepsilon)&amp;lt;/math&amp;gt; relative to the exact sum&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;. Simply summing &amp;lt;math&amp;gt;N&amp;lt;/math&amp;gt; numbers in sequence (the &#039;&#039;&#039;naive linear summation&#039;&#039;&#039;) has a worst-case error bound proportional to &amp;lt;math&amp;gt;N\,\varepsilon&amp;lt;/math&amp;gt;&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. This worst-case occurs for adversarial arrangements of values; in more typical scenarios with rounding errors of random signs, one can model the error as a random walk, yielding an expected root-mean-square (RMS) error growth on the order of &amp;lt;math&amp;gt;O(\sqrt{N}\,\varepsilon)&amp;lt;/math&amp;gt;&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;. Still, with very large &amp;lt;math&amp;gt;N&amp;lt;/math&amp;gt;, even &amp;lt;math&amp;gt;\sqrt{N}&amp;lt;/math&amp;gt; times &amp;lt;math&amp;gt;\varepsilon&amp;lt;/math&amp;gt; can become large enough to matter.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Cancellation&#039;&#039;&#039; is a particular problem: if we add two nearly equal numbers of opposite sign, most leading digits cancel and the result loses significant bits of precision. For example, in single precision, subtracting two numbers that agree in the first 6 decimal digits will yield a result with only ~1 digit of accuracy left. Catastrophic cancellation can dramatically increase relative error. Summation is especially sensitive to cancellation if large positive and negative terms are summed naively. The &#039;&#039;&#039;condition number&#039;&#039;&#039; of a summation (the ratio of the sum of absolute values &amp;lt;math&amp;gt;\sum|x_i|&amp;lt;/math&amp;gt; to the absolute value of the true sum &amp;lt;math&amp;gt;|\sum x_i|&amp;lt;/math&amp;gt;) indicates how sensitive the result is to perturbations&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;. If the numbers have mixed signs and nearly cancel out (making the condition number large), even an optimal summation order will have a large relative error. For instance, summing [1.0, &amp;lt;math&amp;gt;10^{100}&amp;lt;/math&amp;gt;, 1.0, &amp;lt;math&amp;gt;-10^{100}&amp;lt;/math&amp;gt;] in double precision has an exact sum of 2.0, but naive summation may yield 0.0 due to cancellation; an improved summation algorithm yields the correct 2.0&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
In practical terms, when summing a long list of floating-point numbers (such as accumulating a dot product or reducing a tensor), the &#039;&#039;&#039;accumulation error&#039;&#039;&#039; can become noticeable if no precautions are taken. Techniques to mitigate this error include carefully choosing the summation order or using higher precision for intermediate sums. We will explore these strategies in the next section.&lt;br /&gt;
&lt;br /&gt;
== Accumulators and Reduction Strategies ==&lt;br /&gt;
&lt;br /&gt;
When performing reductions (sums, dot products, etc.), the method of accumulation can greatly affect the accuracy of the result. Below, we discuss various strategies:&lt;br /&gt;
&lt;br /&gt;
=== 3.1. Naive Linear Accumulation ===&lt;br /&gt;
&lt;br /&gt;
The simplest accumulation strategy is to use a single accumulator and add each element sequentially. In C-like pseudocode:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;c&amp;quot;&amp;gt;&lt;br /&gt;
float sum = 0.0f;&lt;br /&gt;
for (int i = 0; i &amp;lt; N; ++i) {&lt;br /&gt;
    sum += array[i];&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This &#039;&#039;&#039;naive linear summation&#039;&#039;&#039; adds values one by one to the running total. While straightforward and fast, it is prone to the rounding error accumulation described earlier. The partial sum &amp;lt;code&amp;gt;sum&amp;lt;/code&amp;gt; continually grows (in absolute value) as terms are added, so new small terms may fall outside its precision. For example, if &amp;lt;code&amp;gt;sum&amp;lt;/code&amp;gt; is large and positive, adding a comparatively tiny positive number may produce no change because the small term&#039;s significant digits vanish in the presence of the large sum (the increment is below the rounding threshold of &amp;lt;code&amp;gt;sum&amp;lt;/code&amp;gt;). Conversely, adding a small number of opposite sign can suffer from cancellation. The order of addition matters: summing from smallest magnitude to largest (ascending order) tends to yield a more accurate result than summing in descending order, because adding small terms first preserves their contributions before the sum becomes very large. The naive loop above implicitly sums in the given index order; if the data is unsorted, this order is essentially arbitrary.&lt;br /&gt;
&lt;br /&gt;
The performance of linear accumulation is excellent (minimal overhead), and for many applications the resulting error is within acceptable limits. However, for &#039;&#039;&#039;very large N&#039;&#039;&#039; or ill-conditioned sums, the inaccuracy can become a problem. In a reduction involving millions of elements, a single-precision sum could lose several digits of accuracy. In deep learning, this could translate to small discrepancies in layer outputs or aggregated gradients if done naively in low precision.&lt;br /&gt;
&lt;br /&gt;
=== 3.2. Pairwise and Tree-Based Reductions ===&lt;br /&gt;
&lt;br /&gt;
A better approach to summation is &#039;&#039;&#039;pairwise summation&#039;&#039;&#039;, which sums numbers in a binary tree structure rather than linearly. The idea is to recursively split the array into halves, sum each half, then add the two partial sums. This can be implemented as:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;text&amp;quot;&amp;gt;&lt;br /&gt;
function pairwise_sum(x[1..n]):&lt;br /&gt;
    if n == 0:&lt;br /&gt;
        return 0&lt;br /&gt;
    if n == 1:&lt;br /&gt;
        return x[1]&lt;br /&gt;
    m = floor(n/2)&lt;br /&gt;
    left_sum  = pairwise_sum(x[1..m])&lt;br /&gt;
    right_sum = pairwise_sum(x[m+1..n])&lt;br /&gt;
    return left_sum + right_sum&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
By always adding numbers of similar magnitudes first (small subsets are reduced to subtotals, then those are added, etc.), pairwise summation avoids the situation of a very large running total absorbing small additions until the final steps. The error of pairwise summation grows logarithmically with &amp;lt;math&amp;gt;N&amp;lt;/math&amp;gt; in the worst case (roughly &amp;lt;math&amp;gt;O(\varepsilon \log_2 N)&amp;lt;/math&amp;gt;)&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;, instead of linearly. Intuitively, at each level of the tree, rounding error is constrained, and there are about &amp;lt;math&amp;gt;\log_2 N&amp;lt;/math&amp;gt; levels. In fact, the worst-case relative error bound for pairwise summation (base case of 1 element) is:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;|E_N| \le \frac{\varepsilon \log_2 N}{1 - \varepsilon \log_2 N} \sum_{i=1}^N |x_i|,&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
which for practical &amp;lt;math&amp;gt;N&amp;lt;/math&amp;gt; simplifies to on the order of &amp;lt;math&amp;gt;\varepsilon \log_2 N&amp;lt;/math&amp;gt; times the condition number of the sum&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;. In typical random-error scenarios, the error behaves even better (on the order of &amp;lt;math&amp;gt;\sqrt{\log N}&amp;lt;/math&amp;gt;)&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;. The key is that &#039;&#039;&#039;pairwise (tree) reduction balances the accumulations&#039;&#039;&#039;, preventing one sum from overwhelming the others too early.&lt;br /&gt;
&lt;br /&gt;
From a performance standpoint, pairwise summation has the same &amp;lt;math&amp;gt;O(N)&amp;lt;/math&amp;gt; total additions as linear summation, just a different order. It is also highly amenable to parallelization: independent partial sums can be computed in parallel at each level of the tree. Many numerical libraries and BLAS implementations use a form of blocked or pairwise summation in dot products for improved accuracy at negligible performance cost. For example, NumPy and Julia perform partial pairwise summation by default for better numerical behavior&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=== 3.3. Compensated Summation (Kahan, Neumaier) ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Compensated summation&#039;&#039;&#039; is an alternative technique to improve summation accuracy by tracking the small “lost” quantities. The most famous algorithm is the &#039;&#039;&#039;Kahan summation algorithm&#039;&#039;&#039; (also called Kahan–Babuška)&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. Kahan summation uses an extra variable to accumulate the round-off error. In pseudocode:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;c&amp;quot;&amp;gt;&lt;br /&gt;
double sum = 0.0;&lt;br /&gt;
double c = 0.0;    // compensation for lost low-order bits&lt;br /&gt;
for (i = 1; i &amp;lt;= N; ++i) {&lt;br /&gt;
    double y = x[i] - c;    // recover low-order bits by subtracting compensation&lt;br /&gt;
    double t = sum + y;     // perform the addition&lt;br /&gt;
    c = (t - sum) - y;      // compute new compensation (the error in t)&lt;br /&gt;
    sum = t;&lt;br /&gt;
}&lt;br /&gt;
return sum;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Here &amp;lt;code&amp;gt;c&amp;lt;/code&amp;gt; accumulates the tiny leftovers that would otherwise be dropped&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. Each iteration essentially subtracts the last error from the new addend (&amp;lt;code&amp;gt;y = x[i] - c&amp;lt;/code&amp;gt;) so that &amp;lt;code&amp;gt;t = sum + y&amp;lt;/code&amp;gt; includes the small bits from the previous operation. The new error is then stored in &amp;lt;code&amp;gt;c&amp;lt;/code&amp;gt; for the next iteration. The result is that the final &amp;lt;code&amp;gt;sum&amp;lt;/code&amp;gt; is as if computed with higher precision than the machine’s: Kahan’s algorithm can &#039;&#039;&#039;greatly reduce the error&#039;&#039;&#039; in summing a sequence of floats. In fact, with exact arithmetic for the compensation, Kahan summation yields a result accurate to within 1 or 2 ULP of the true sum, regardless of N (assuming the sequence of partial sums doesn’t overflow)&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. More precisely, Kahan summation achieves a &#039;&#039;&#039;worst-case error independent of N&#039;&#039;&#039; (error bound depends only on machine precision, not the number of terms)&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. In practice, using a double accumulator for summing single-precision numbers via Kahan can give near double-precision accuracy.&lt;br /&gt;
&lt;br /&gt;
An important subtlety is that the compensation variable &amp;lt;code&amp;gt;c&amp;lt;/code&amp;gt; should be kept in &#039;&#039;full precision&#039;&#039;. In the implementation above, we used &amp;lt;code&amp;gt;double&amp;lt;/code&amp;gt; for both sum and c. If summing FP32 values, one might use &amp;lt;code&amp;gt;float sum&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;float c&amp;lt;/code&amp;gt; (both 32-bit), which still improves accuracy, but using a higher precision for the accumulator and compensation (i.e. double) yields even better results. Many implementations of Kahan summation use the same type for sum and c, but it&#039;s assumed to be higher precision than the inputs when possible.&lt;br /&gt;
&lt;br /&gt;
Kahan’s algorithm carries a performance cost: each iteration does five arithmetic operations (two adds, one subtract, etc.) and has a data dependency, which can hinder parallelism or pipelining&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. Thus, it runs slower than naive summation (roughly 2–3× slower or more, depending on CPU pipeline). In loops where summation is a bottleneck, this may be significant. Nonetheless, for modest N or where maximum accuracy is required, compensated summation is extremely useful. In fact, Python’s &amp;lt;code&amp;gt;math.fsum&amp;lt;/code&amp;gt; function uses a variant of Kahan’s algorithm (actually a more advanced multi-part compensation) to sum iterables with high precision.&lt;br /&gt;
&lt;br /&gt;
There are variations and improvements on basic Kahan summation. &#039;&#039;&#039;Neumaier’s algorithm&#039;&#039;&#039; (sometimes called &#039;&#039;Kahan-Babuška-Neumaier&#039;&#039;) improves Kahan’s method by also handling cases where the next input is larger in magnitude than the running sum&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. In such cases, the original Kahan formula could suffer error; Neumaier’s version effectively swaps roles when a huge value comes in. Its pseudocode is:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;text&amp;quot;&amp;gt;&lt;br /&gt;
function NeumaierSum(array):&lt;br /&gt;
    sum = 0.0&lt;br /&gt;
    compensation = 0.0&lt;br /&gt;
    for each x in array:&lt;br /&gt;
        t = sum + x&lt;br /&gt;
        if (fabs(sum) &amp;gt;= fabs(x))&lt;br /&gt;
            compensation += (sum - t) + x;&lt;br /&gt;
        else&lt;br /&gt;
            compensation += (x - t) + sum;&lt;br /&gt;
        sum = t;&lt;br /&gt;
    return sum + compensation;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This algorithm ensures the compensation covers both scenarios: when the sum is the bigger addend or when the new term is bigger&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. In the end, it adds the final &amp;lt;code&amp;gt;compensation&amp;lt;/code&amp;gt; once to the result. Neumaier’s method can produce the correct result in cases where basic Kahan fails. For instance, summing [1.0, &amp;lt;math&amp;gt;+10^{100}&amp;lt;/math&amp;gt;, 1.0, &amp;lt;math&amp;gt;-10^{100}&amp;lt;/math&amp;gt;] in double precision: regular Kahan summation might give 0.0 (losing the small terms), whereas Neumaier’s algorithm yields 2.0 exactly&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. There are also higher-order compensation methods (carrying multiple compensation terms, e.g., the &#039;&#039;&#039;Klein summation&#039;&#039;&#039; variant&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt; or using arbitrary precision for intermediate sums) that further reduce error at increased computational cost&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
In summary, compensated summation algorithms trade extra computation for greatly improved numerical accuracy. They are especially valuable when summing large arrays of numbers with varying magnitudes, where naive summation could lose most of the low-order bits. Many scientific computing environments provide compensated sum routines for this reason. In deep learning, however, a different approach to improved precision is often taken: &#039;&#039;&#039;using a wider accumulator type&#039;&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
=== 3.4. Mixed-Precision Accumulation ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Mixed-precision accumulation&#039;&#039;&#039; refers to performing arithmetic in a higher precision than the input data. A common scenario is accumulating sums in 32-bit floats when the operands are 16-bit floats. Rather than implementing a compensation algorithm, one can simply use a wider accumulator so that intermediate rounding error is smaller. This is effectively what modern GPUs do for tensor operations: for example, NVIDIA &#039;&#039;&#039;Tensor Cores operate on FP16 input data with an FP32 accumulator&#039;&#039;&#039; (the 16-bit products are accumulated in 32-bit)&amp;lt;ref name=&amp;quot;ref6&amp;quot;/&amp;gt;. In this way, summing 2048 products in a dot product has far less precision loss than it would with a 16-bit accumulator. Hardware that supports mixed precision can give the best of both worlds: fast low-precision multiplies and high-precision accumulation.&lt;br /&gt;
&lt;br /&gt;
Software can also leverage mixed precision. For instance, one can sum an array of &amp;lt;code&amp;gt;float&amp;lt;/code&amp;gt; values into a &amp;lt;code&amp;gt;double&amp;lt;/code&amp;gt; variable, accumulating the result in double precision and then converting back to float at the end:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;c&amp;quot;&amp;gt;&lt;br /&gt;
double sum = 0.0;&lt;br /&gt;
for (int i=0; i&amp;lt;N; ++i) {&lt;br /&gt;
    sum += (double) arr_float[i];&lt;br /&gt;
}&lt;br /&gt;
float result = (float) sum;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This approach often yields a more accurate &amp;lt;code&amp;gt;result&amp;lt;/code&amp;gt; than summing in single precision directly, because the intermediate sum carries 52-bit precision. The final rounding to float happens only once at the end (minimizing overall rounding error). In fact, the above is one way to implement &amp;lt;code&amp;gt;math.fsum&amp;lt;/code&amp;gt; in Python (it internally does something similar with multiple partial sums in double precision).&lt;br /&gt;
&lt;br /&gt;
In machine learning training and inference, &#039;&#039;&#039;mixed precision&#039;&#039;&#039; is widely used. When using FP16 for speed, it&#039;s common to accumulate with FP32. For example, &#039;&#039;&#039;Google&#039;s TPUs use BFloat16 for multiplication but accumulate in FP32&#039;&#039;&#039; for matrix ops&amp;lt;ref name=&amp;quot;ref6&amp;quot;/&amp;gt;. AWS’s Inferentia (Neuron) hardware similarly supports FP16/BF16 matrix multiplication with FP32 accumulation&amp;lt;ref name=&amp;quot;ref7&amp;quot;/&amp;gt;. The motivation is that summing hundreds or thousands of half-precision values would otherwise introduce intolerable error or risk overflow. With an FP32 accumulator, the reduction error is drastically reduced (by about &amp;lt;math&amp;gt;2^{13}&amp;lt;/math&amp;gt; times, since FP32 has 13 more precision bits than FP16). Mixed-precision accumulation thus provides a simple and effective form of compensation: &#039;&#039;&#039;use a larger bucket to collect many small contributions&#039;&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
There is a spectrum of options: summing int8 or int16 integers might use 32-bit integers as accumulators for the same reason (to avoid overflow). Some BLAS libraries even offer &#039;&#039;extended precision accumulators&#039;&#039; for single precision dot products (accumulating in double). The downside of mixed precision is the extra memory and maybe throughput cost of the wider type. But on many platforms, the add latency is dominated by hardware design rather than bit-width, so using a 32-bit adder vs 16-bit has negligible speed impact if supported natively. The key is whether data movement or register pressure increases.&lt;br /&gt;
&lt;br /&gt;
In summary, &#039;&#039;&#039;using a higher precision accumulator is often the easiest way to improve numerical accuracy&#039;&#039;&#039; for reductions. It is a primary strategy in deep learning hardware to maintain stability while exploiting low precision for storage and initial computations.&lt;br /&gt;
&lt;br /&gt;
== Precision Formats in AI and Machine Learning ==&lt;br /&gt;
&lt;br /&gt;
Modern AI systems use a variety of numeric formats to balance speed, memory, and accuracy. We discuss common precisions and their roles:&lt;br /&gt;
&lt;br /&gt;
=== 4.1. FP32 and FP64 in Training and Inference ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;FP32 (single precision)&#039;&#039;&#039; has been the workhorse of deep learning for years. Most training scripts in frameworks like TensorFlow and PyTorch used FP32 by default, as it offers sufficient precision to propagate gradients and accumulate weight updates over millions of iterations without excessive error. With ~7 decimal digits of precision and a wide exponent range, FP32 can handle the dynamic range of activations and gradients in typical neural networks. In inference, FP32 is often more precision than strictly necessary for final predictions, but it has remained common because of its ease and the fact that many GPU operations were optimized for FP32.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;FP64 (double precision)&#039;&#039;&#039; is rarely used in deep learning model training or inference, except in specific scenarios requiring extreme precision. FP64 is common in scientific computing and certain types of numerical simulations. In neural networks, however, the extra precision is usually not needed for model accuracy—using doubles tends to yield negligible improvements in model metrics but at a large performance cost (since on GPUs FP64 throughput can be 1/2, 1/8, or even 1/32 of FP32 throughput depending on hardware). One exception might be when integrating neural nets into larger physics simulations or when solving ill-conditioned problems as part of the model. Some researchers use FP64 to diagnose numerical issues or ensure reproducibility. Generally though, deep learning has favored lower precision for efficiency, and FP64 is considered overkill for most networks&amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
That said, certain classical machine learning algorithms benefit from double precision. For example, computing the inverse of a matrix (as in a Gaussian Process regression or Kalman filter embedded in a model) can incur large rounding errors in single precision, so using FP64 can significantly improve the result&amp;lt;ref name=&amp;quot;ref1&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref1&amp;quot;/&amp;gt;. The ONNX standard supports a float64 tensor type for such needs. In ONNX Runtime, however, emphasis has been on FP32 performance, and some pipelines default to float even if the original model was double&amp;lt;ref name=&amp;quot;ref1&amp;quot;/&amp;gt;. If a model’s prediction computation involves operations that are numerically sensitive (like matrix inversion, Cholesky decomposition, or summing extremely large counts), then FP64 may be required to avoid noticeable discrepancies&amp;lt;ref name=&amp;quot;ref1&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref1&amp;quot;/&amp;gt;. A good practice is to test a model in both float and double if you suspect precision issues; significant differences indicate an ill-conditioned calculation that might need higher precision.&lt;br /&gt;
&lt;br /&gt;
In summary, FP32 remains the &#039;&#039;&#039;default&#039;&#039;&#039; format for most training and inference due to its balance of accuracy and efficiency&amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;, while FP64 is reserved for niche cases or precision-critical subroutines. FP32 typically achieves the same accuracy as FP64 for end-to-end deep learning tasks, because network training can often compensate for small noise, and other sources of error (like generalization error) dominate. Therefore, the community trend has been moving to &#039;&#039;&#039;lower&#039;&#039;&#039; precisions, not higher, to gain speed — provided the models still converge and perform well.&lt;br /&gt;
&lt;br /&gt;
=== 4.2. FP16 and BFloat16 in Deep Learning ===&lt;br /&gt;
&lt;br /&gt;
To further speed up neural network training and inference, &#039;&#039;&#039;half-precision (FP16)&#039;&#039;&#039; was adopted, especially with the advent of GPUs supporting FP16 math at twice the rate of FP32. Using FP16 reduces memory storage and bandwidth by 50% compared to FP32, which is significant for large models. However, FP16’s small exponent range and precision posed challenges: training a large network purely in FP16 can fail due to gradient underflow/overflow or weight updates not registering (because changes are below the 0.001 quantization step). To address this, frameworks introduced &#039;&#039;&#039;mixed-precision training&#039;&#039;&#039;, where compute-intensive parts (matrix multiplies, convolutions) run in FP16, but certain accumulations and model parameters remain in FP32. For instance, NVIDIA’s &#039;&#039;&#039;Tensor Core&#039;&#039;&#039; units require FP16 (or BF16) inputs but accumulate results in FP32, effectively ensuring that reductions are done in higher precision&amp;lt;ref name=&amp;quot;ref6&amp;quot;/&amp;gt;. Research showed that with proper loss scaling and mixed precision, models can train to FP32-level accuracy using mostly FP16 arithmetic, achieving significant speedups.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;BFloat16 (BF16)&#039;&#039;&#039; emerged as an alternative 16-bit format primarily from Google’s TPU training systems. BF16 has a larger exponent (8 bits, same as FP32) but only 7 fraction bits&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. This means BF16 can handle the &#039;&#039;&#039;same dynamic range&#039;&#039;&#039; as FP32 (no overflows where FP32 wouldn’t) but at the cost of even lower precision than FP16 (BF16 has about 2–3 decimal digits of precision vs FP16’s ~3–4). Despite its coarse precision, BF16 turns out to be effective for training deep networks because the algorithms (stochastic gradient descent, etc.) are somewhat robust to that noise, and having the wide range prevents catastrophic loss of signal (no gradient will overflow to Inf unless it was going to in FP32 anyway). Many training frameworks (PyTorch, TensorFlow) support BF16 training on hardware like &#039;&#039;&#039;TPU, NVIDIA A100+, Intel CPUs with AVX-512 BF16, etc&#039;&#039;&#039;. Typically, when using BF16, &#039;&#039;&#039;accumulations are still done in FP32&#039;&#039;&#039; (for example, a TPU v2/v3 multiplies BF16 matrices and accumulates into an FP32 result for the dot product&amp;lt;ref name=&amp;quot;ref6&amp;quot;/&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
In inference, both FP16 and BF16 are used to deploy models for faster speed or lower memory. FP16 is popular on NVIDIA GPUs (which have specialized FP16 units). BF16 is advantageous on TPUs and some CPUs that support it, and increasingly on GPUs (Ampere and newer GPUs also support BF16 arithmetic). The choice between FP16 and BF16 can depend on hardware: BF16’s advantage is easier software conversion (you can often take an FP32-trained model and cast weights to BF16 with minimal loss, since the dynamic range is intact), whereas FP16 might require adjusting the model or ensuring values fit in range (sometimes needing techniques like maintaining a master copy of weights in FP32 during training, or handling out-of-range values).&lt;br /&gt;
&lt;br /&gt;
For ONNX, both Float16 and BFloat16 types are part of the standard. ONNX models can be converted to FP16 (truncating weights and inserting cast nodes for computations) using tools like &amp;lt;code&amp;gt;float16_converter&amp;lt;/code&amp;gt; in ONNX Runtime&amp;lt;ref name=&amp;quot;ref8&amp;quot;/&amp;gt;. ONNX Runtime on GPUs will typically execute those FP16 ops with mixed precision on tensor cores (FP16 input, FP32 accumulate), whereas on CPU it may emulate FP16 or upcast to FP32 (as many CPU implementations do not natively support half-precision math)&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;. In fact, as an example, running an ONNX model in FP16 on a CPU might insert hidden Cast operations to FP32 behind the scenes, because the CPU kernel for that op only exists in FP32&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;. This ensures correctness (the computation is done in higher precision) but can reduce performance. We will revisit such backend-dependent behavior in Section 5.3.&lt;br /&gt;
&lt;br /&gt;
In summary, &#039;&#039;&#039;FP16 and BF16&#039;&#039;&#039; are now established as important low-precision formats in deep learning. FP16 offers more precision bits, which can matter for e.g. small differences, but has a limited range (care needed to avoid overflow). BF16 offers safety in range but less precision (which can slightly affect convergence or final accuracy in some cases). Both typically rely on FP32 for accumulation to achieve acceptable accuracy. When deploying models, one should choose a format supported efficiently by the target hardware and verify that the accuracy is not significantly degraded. Often, CNNs and transformers can run in FP16/BF16 with virtually no drop in score, but certain operations (like softmax, see Section 6.3) might need special handling.&lt;br /&gt;
&lt;br /&gt;
=== 4.3. FP8 Formats (E4M3, E5M2) and Scaling ===&lt;br /&gt;
&lt;br /&gt;
The push for ever lower precision has led to experimental &#039;&#039;&#039;8-bit floating point (FP8)&#039;&#039;&#039; formats for deep learning. In 2022, researchers from NVIDIA, Intel, and Arm proposed an FP8 standard with two variants: &#039;&#039;&#039;E4M3&#039;&#039;&#039; (4 exponent bits, 3 fraction bits) and &#039;&#039;&#039;E5M2&#039;&#039;&#039; (5 exponent bits, 2 fraction bits)&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;. Both have 1 sign bit, for a total of 8 bits. E5M2 has a wider exponent range (5 bits gives bias 15) but one fewer mantissa bit than E4M3. The idea is to use E4M3 for numbers that need more precision (like weights or activations around 1.0) and E5M2 for numbers that need more dynamic range (like gradient updates which can be very small or large). In fact, the study found that using E4M3 for forward activations/weights and E5M2 for backward gradients can allow training of large networks with FP8 to accuracy on par with FP16 training&amp;lt;ref name=&amp;quot;ref11&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
FP8 formats are &#039;&#039;not&#039;&#039; IEEE 754 standard (though IEEE is considering lower precision in its 2019 revision). Notably, the E4M3 format as used by NVIDIA does not allocate any bit-pattern for &amp;lt;math&amp;gt;+\infty&amp;lt;/math&amp;gt; or &amp;lt;math&amp;gt;-\infty&amp;lt;/math&amp;gt; – it has only NaN for invalid results, using that freed bit pattern to extend the representable range by one exponent value&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;. Essentially, E4M3 can represent numbers up to ~&amp;lt;math&amp;gt;2^{15}&amp;lt;/math&amp;gt; (around 3e4) instead of capping at infinity at &amp;lt;math&amp;gt;2^{16}&amp;lt;/math&amp;gt;, by foregoing infinities&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;. Variations also exist regarding negative zero and how NaNs are handled&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;. For instance, GraphCore’s IPU uses FP8 formats that disallow negative zero and infinities, to maximize useable range&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Using 8-bit floats for neural networks requires additional techniques, chiefly &#039;&#039;&#039;scaling&#039;&#039;&#039;. Because 8-bit has very limited precision and range, one typically applies a power-of-two scale factor to groups of values to normalize them into an FP8-friendly range. This is akin to block floating-point or “microscaling” per layer or per tensor. For example, one might keep a FP32 scale for each channel or tensor, and represent values as &amp;lt;math&amp;gt;(FP8\_value) \times 2^{scale}&amp;lt;/math&amp;gt;. The ONNX standard has introduced support for FP8 in version 1.15, including types for E4M3 and E5M2 (with variations FNUZ: finite-only and no negative zero)&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;, and even a type called &#039;&#039;&#039;E8M0&#039;&#039;&#039; which is an 8-bit exponent-only number used purely as a scale factor&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;. The E8M0 type has 8 exponent bits, no mantissa, effectively representing powers of two from &amp;lt;math&amp;gt;2^{-127}&amp;lt;/math&amp;gt; to &amp;lt;math&amp;gt;2^{128}&amp;lt;/math&amp;gt; for scaling purposes&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;. This indicates an approach where an FP8 tensor would be accompanied by an E8M0 scale (or scales) to interpret its value correctly.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Rounding&#039;&#039;&#039; becomes critical when casting values to FP8. The quantization error is much larger relative to FP16 or FP32. Studies have shown that choosing the right rounding mode (e.g., round-to-nearest, stochastic rounding, etc.) and clamping behavior (saturating to max instead of inf) can affect model accuracy&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;. ONNX added a &amp;lt;code&amp;gt;round_mode&amp;lt;/code&amp;gt; attribute to its Cast operator to allow controlling this for FP8&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;. In particular, one paper found that &#039;&#039;&#039;rounding up with saturation&#039;&#039;&#039; (round-to-nearest ties away from 0, and clamp overflows to max finite rather than inf) gave better accuracy in large language model training than other rounding modes&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
At present, FP8 is on the cutting edge. NVIDIA’s H100 GPU supports FP8 tensor cores (for both E4M3 and E5M2), and early results show minimal accuracy loss on some vision and NLP tasks when properly tuned. However, using FP8 requires fine-tuning hyperparameters like scaling factors for each layer and perhaps tweaking training procedures. ONNX’s inclusion of FP8 types means you can represent and transport models using FP8 weights or activations, but actual hardware support is limited and typically paired with calibration or dynamic scaling logic outside of ONNX graph (or using custom ONNX functions for scaling). It remains an &#039;&#039;&#039;open research area&#039;&#039;&#039; how to fully automate FP8 quantization for arbitrary models without accuracy loss.&lt;br /&gt;
&lt;br /&gt;
In summary, FP8 formats promise further compression of neural network computation and data. They are an order of magnitude more error-prone than FP16, but when combined with careful scaling, mixed usage of E4M3/E5M2, and maybe modified training techniques, they have shown surprising success. ONNX is actively evolving to support these ultra-low precision types, signaling their potential importance in future AI inference deployments.&lt;br /&gt;
&lt;br /&gt;
== Accumulation Semantics in ONNX ==&lt;br /&gt;
&lt;br /&gt;
One of the roles of the ONNX specification is to define the mathematical behavior of operators (Conv, MatMul, Sum, etc.) in a framework-agnostic way. However, ONNX mostly defines &#039;&#039;what&#039;&#039; an operator computes in exact arithmetic, not &#039;&#039;how&#039;&#039; the computation is carried out in finite precision. This leaves certain details – like accumulator precision or summation order – up to the implementation (runtime or hardware). We&#039;ll examine how ONNX treats accumulation and what guarantees (or lack thereof) are provided.&lt;br /&gt;
&lt;br /&gt;
=== 5.1. Operator Definitions and Numerical Guarantees ===&lt;br /&gt;
&lt;br /&gt;
Each ONNX operator is described as performing an ideal mathematical function on its inputs. For example, &amp;lt;code&amp;gt;MatMul&amp;lt;/code&amp;gt; is defined as the standard matrix product of two tensors, and &amp;lt;code&amp;gt;ReduceSum&amp;lt;/code&amp;gt; computes a summation along given axes. The ONNX spec, as of current versions, &#039;&#039;&#039;does not mandate the precision of intermediate calculations&#039;&#039;&#039; beyond the types of the inputs and outputs. If an operator’s inputs and outputs are float16, the spec implies the computation is done in float16 in a sense, but it does not forbid an implementation from using higher precision internally – it simply doesn&#039;t mention it. There is &#039;&#039;&#039;no explicit guarantee of identical bit-for-bit results&#039;&#039;&#039; across different ONNX backends for floating-point ops. Instead, results are expected to be numerically close, within tolerances one would normally expect from floating-point differences (due to different hardware or algorithms). The ONNX Backend Test suite typically uses a relative or absolute error tolerance when validating outputs for this reason.&lt;br /&gt;
&lt;br /&gt;
Because of this, important details like the use of extended precision or accumulation in higher precision are left ambiguous. This has led to discussions in the ONNX community about making such behavior more explicit. For example, a proposal was raised to allow specification of accumulation precision for certain ops like MatMul&amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;. The reasoning is that as lower-precision types (float16, bfloat16, float8) become more common, knowing or controlling whether an op internally accumulates in a higher precision is important for both accuracy and reproducibility&amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;. In June 2025, an issue on the ONNX GitHub suggested adding an attribute to MatMul (and similarly Gemm, Softmax, etc.) to specify the accumulator type&amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;. By default it would use input precision (status quo), but an attribute could request (for instance) 32-bit accumulation even if inputs are 16-bit&amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;. As of this writing, however, no such attribute exists in the official spec – it&#039;s an open issue. Thus, the numerical semantics are effectively: &#039;&#039;“compute the result as if in the same type as inputs, but actual implementations may use more precision internally as an optimization.”&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
From a guarantee standpoint, ONNX operators promise the mathematically correct result assuming real-number arithmetic, but they &#039;&#039;&#039;do not guarantee bit-exact results&#039;&#039;&#039; due to floating-point effects. There is an understanding that differences can arise from different summation orders, fused operations (e.g., an implementation might use an FMA (fused multiply-add) which has one rounding instead of two), or use of extended precision registers. ONNX does not formally define rounding modes either – it relies on IEEE 754 defaults, but if a backend used something like deterministic summation or alternative rounding, ONNX has no mechanism to express that.&lt;br /&gt;
&lt;br /&gt;
In summary, ONNX&#039;s current stance is largely &#039;&#039;&#039;implementation-dependent&#039;&#039;&#039; for numerical precision. It provides minimal explicit numerical guarantees beyond type constraints. This is why an ONNX model run on two different hardware or runtime libraries can have slightly different results, especially in reduced precision. The burden is on backends to produce results &amp;quot;close enough&amp;quot; to each other that they don&#039;t affect the model&#039;s decisions in any significant way.&lt;br /&gt;
&lt;br /&gt;
=== 5.2. MatMul, Conv, and Reduction Operators ===&lt;br /&gt;
&lt;br /&gt;
Consider ONNX &amp;lt;code&amp;gt;MatMul&amp;lt;/code&amp;gt;: it takes two tensors (often two matrices) and produces their matrix product. In pure math, &amp;lt;math&amp;gt;C_{ij} = \sum_k A_{ik} &#039;&#039; B_{kj}&amp;lt;/math&amp;gt;. If A and B are float16, should that sum be computed in float16 or float32? As discussed, ONNX doesn’t specify – it simply says the output is a float16 matrix containing the product. In practice, many backends will &#039;&#039;promote&#039;&#039; the computation: for example, NVIDIA GPUs will do the multiplication and addition in a mixed FP16/FP32 mode by default (because of tensor cores), then convert the final result back to float16. ONNX Runtime on CPU might not support float16 multiplication natively, so it will insert cast ops to float32, perform the MatMul in float32, then cast back to float16&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;. From ONNX’s perspective, as long as the final result is within acceptable error of the “ideal” float16 computation, it’s fine. But note, an “ideal” float16 computation (where every multiply and add is rounded to FP16) might actually be &#039;&#039;less accurate* than what the backend did with FP32. This means that a backend using higher precision can produce a result that is not bit-equal to a pure FP16 implementation – it&#039;s actually closer to the true real number result. This is usually considered a good thing (improved accuracy), but it does highlight a lack of consistency: another backend might do everything in pure FP16 and get a slightly different answer.&lt;br /&gt;
&lt;br /&gt;
The same situation occurs for &#039;&#039;&#039;convolution&#039;&#039;&#039; (&amp;lt;code&amp;gt;Conv&amp;lt;/code&amp;gt; op) which is essentially a batched sum of products (like MatMul in sliding window), and for explicit reduction ops like &amp;lt;code&amp;gt;ReduceSum&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;ReduceMean&amp;lt;/code&amp;gt;, etc. If you have an ONNX &amp;lt;code&amp;gt;ReduceSum&amp;lt;/code&amp;gt; that sums 1000 float16 values, the spec doesn’t say how to sum them. A GPU kernel might sum in a tree using FP32 accumulators; a naive implementation might sum in order with FP16 and possibly overflow or underflow intermediate steps. Both are allowed as long as they output a float16 result that is “reasonable.” In practice, ONNX Runtime and others take care to avoid obviously bad behavior (like overflow) by using higher precision or by splitting the sum. But again, it’s not mandated by the ONNX standard explicitly.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Softmax&#039;&#039;&#039; is an interesting case: Softmax involves an internal sum of exponentials. ONNX defines the Softmax operator as &amp;lt;math&amp;gt;$\mathrm{Softmax}(x)_i = \frac{\exp(x_i)}{\sum_j \exp(x_j)},&amp;lt;/math&amp;gt;&amp;lt;math&amp;gt; typically applied along a specified axis. Directly computing this in float16 can be problematic for large vectors, as discussed in Section 6.3. Indeed, the issue of accumulation precision arises: should the sum &amp;lt;/math&amp;gt;\sum_j \exp(x_j)&amp;lt;math&amp;gt; be done in FP16 or can it be in FP32? Many implementations will do it in FP32 even if &amp;lt;/math&amp;gt;x&amp;lt;math&amp;gt; is FP16, because the dynamic range of &amp;lt;/math&amp;gt;\exp(x)$ is large. The ONNX Softmax spec doesn’t detail this, but it expects the result to be a proper probability distribution (summing to 1.0 within normal floating error). A pure FP16 softmax on, say, 1000 elements could sum to something like 0.996 or 1.004 due to rounding, whereas FP32 accumulation would be much closer to 1.000. ONNX likely allows both; the small difference is usually inconsequential, but if it were large (say the FP16 sum overflowed to inf), that would be a correctness issue. A robust backend will prevent that (by using the common max-subtraction trick and higher precision accumulation). The ONNX issue 7072 indeed calls out Softmax as another op where explicit control of accumulation precision would be useful&amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
In summary, &#039;&#039;&#039;MatMul, Conv, and reduction ops&#039;&#039;&#039; in ONNX are defined mathematically but with &#039;&#039;implicit&#039;&#039; numerical behavior. Implementations commonly use higher precision for intermediate sums especially when input is low precision, but this is not standardized. Consequently, results can differ slightly between implementations. For critical applications, one must be aware of these differences. A model exported to ONNX with float16 weights might perform slightly differently on one runtime vs another if one does all float16 math and the other uses float32 accumulations. Both conform to ONNX. As a user, if you require a certain precision for accumulation, currently you have to ensure it by the way you export or run the model (for example, insert manual cast nodes in the ONNX graph to force certain ops to FP32). We may see future ONNX versions address this with attributes or metadata as mentioned, but for now these ops are effectively “compute with best effort accuracy”.&lt;br /&gt;
&lt;br /&gt;
=== 5.3. Backend and Hardware-Dependent Behavior ===&lt;br /&gt;
&lt;br /&gt;
Because ONNX is implemented by many backends (ONNX Runtime, TensorRT, CoreML, PyTorch JIT, etc.), and these in turn run on varied hardware (CPU, GPU, accelerators), the numeric behavior can be backend-dependent. Some examples:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;CPU backends:&#039;&#039;&#039; Many CPUs do not have native float16 vector arithmetic (with a few exceptions in recent CPUs). ONNX Runtime’s default CPU execution provider will often upcast float16 to float32, compute, then downcast&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;. This ensures accuracy but means that if you thought you were getting a full float16 pipeline, you’re not – the CPU is essentially ignoring the low precision in compute. On the other hand, x86 CPUs have extended 80-bit precision in x87 registers (though less used now) and 64-bit SIMD registers for doubles; some compiler optimizations might carry intermediate results in higher precision. This could make CPU results slightly different from a GPU’s if, say, a sum is done in 80-bit on CPU versus 32-bit on GPU. Generally, modern compilers avoid such extended precision unless explicitly asked, to improve reproducibility.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;GPU backends:&#039;&#039;&#039; GPUs often perform summations in parallel using tree reductions. The order of operations can change depending on how threads are scheduled. This means even on the same GPU, summing an array in one launch versus another might not give bit-identical results if the internal parallel reduction algorithm differs (though it should be deterministically the same given the same library version and input, but if you change GPU model or BLAS library, it might differ). Also, GPUs have fused multiply-add (FMA) instructions that do &amp;lt;math&amp;gt;(a*b + c)&amp;lt;/math&amp;gt; in one step with one rounding, which can produce slightly different results than doing the multiply and add separately (which would round twice). If an ONNX backend uses a fused kernel (like a GEMM library) it might yield different low-order bits than a simple loop.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Vendor libraries:&#039;&#039;&#039; ONNX Runtime can use vendor BLAS or DNN libraries (MKL, cuDNN, TensorRT, etc.). These libraries often implement their own strategies for numerical stability. For example, NVIDIA’s cuDNN might use FP32 accumulation for FP16 inputs by default. TensorRT (NVIDIA’s inference engine) will sometimes automatically promote precisions or insert scaling to maintain stability&amp;lt;ref name=&amp;quot;ref13&amp;quot;/&amp;gt;. On the other hand, if you explicitly request FP16 in TensorRT with no FP32 fallback, it might do the entire op in FP16 and you’d see more error. Some libraries offer knobs: TensorRT allows choosing FP32, FP16, or INT8 precision for each layer, and will use FP32 internally for safety in some cases unless forced otherwise.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Hardware-specific quirks:&#039;&#039;&#039; As another example, NVIDIA’s &#039;&#039;&#039;TensorFloat-32 (TF32)&#039;&#039;&#039; on Ampere GPUs is a precision mode where matrix multiplications use 10-bit mantissa (so about FP16 precision for the multiply) but keep 8-bit exponent (range like FP32). By default, CUDA libraries use TF32 for FP32 matrix multiplies on Ampere to speed them up, unless you disable it. This means if you run an ONNX model with FP32 on an A100 GPU, your matmuls might actually be lower precision than FP32 (though higher than FP16)&amp;lt;ref name=&amp;quot;ref6&amp;quot;/&amp;gt;. It’s an example of hardware choosing an intermediate precision for performance. From ONNX’s perspective, the output is still FP32 and (hopefully) within error tolerances, but a user might be surprised that it’s not a “true” FP32 computation internally. If exactness is needed, one has to turn off TF32.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Non-determinism:&#039;&#039;&#039; Some backends (especially on GPUs) allow non-deterministic execution for performance. For instance, atomic adds in parallel reductions can produce results that vary run-to-run because the order of accumulation is race-condition-dependent. ONNX’s spec doesn’t cover this explicitly, but it’s something to consider. In ONNX Runtime, there are settings for deterministic computing, but by default it may use all available performance optimizations, even if results vary at the 1e-7 level.&lt;br /&gt;
&lt;br /&gt;
In short, the numerical behavior under ONNX can depend on the combination of backend software and hardware capabilities. The lack of a strict spec for accumulation means implementations have freedom to use clever strategies (higher precision, reordering) to improve accuracy or speed. Usually this is positive (e.g. getting better accuracy than a naive approach). But it could also lead to slight inconsistencies. For example, you might find that exporting a PyTorch model to ONNX and running it with one engine yields outputs slightly different from PyTorch’s – not because ONNX is wrong, but because the summation or convolution was done differently (maybe PyTorch summed in FP32 as well, but maybe not in exactly the same way).&lt;br /&gt;
&lt;br /&gt;
The key takeaway is that ONNX &#039;&#039;by design&#039;&#039; abstracts away these details, which is good for portability but means you need to &#039;&#039;&#039;trust the backend&#039;&#039;&#039; or verify its precision behavior for sensitive models. If necessary, you can enforce certain behavior by inserting ops (for example, if you want to ensure FP32 accumulation, you could insert a cast to FP32 for inputs of a MatMul and cast the result back to FP16 – this way the ONNX graph itself mandates FP32 compute in between). This of course sacrifices some performance or device-specific magic.&lt;br /&gt;
&lt;br /&gt;
Looking forward, as mentioned, there are proposals to enrich ONNX’s specification regarding precision. This might include explicit flags for “exact summation” or “accumulate in int32” etc., which would make models more self-descriptive but also potentially less portable (if a backend doesn’t support that, what then?). For now, users should be aware of these backend differences. Most of the time, well-engineered ONNX runtimes yield results close enough that they don&#039;t affect the application (e.g., classification top-1 remains the same even if low-level sums differ by 1e-5). But for high-stakes numerical computations, testing and maybe adjusting the model or backend settings is prudent.&lt;br /&gt;
&lt;br /&gt;
== Numerical Stability in Deep Learning Models ==&lt;br /&gt;
&lt;br /&gt;
Deep learning computations involve many operations that, while theoretically well-defined, can be sensitive to numerical issues when implemented in finite precision. We&#039;ll discuss a few common scenarios in neural networks where numerical stability is a concern, and how precision and accumulation play a role.&lt;br /&gt;
&lt;br /&gt;
=== 6.1. Dot Products and Large-Scale Reductions ===&lt;br /&gt;
&lt;br /&gt;
Neural networks are full of dot products: every fully-connected layer computes &amp;lt;math&amp;gt;\mathbf{y} = W\mathbf{x}&amp;lt;/math&amp;gt;, which is a series of inner products between weight vectors and the input. Convolutional layers compute weighted sums of local regions. Attention mechanisms compute large matrix products of query/key/value matrices. All these boil down to summing a lot of terms: &amp;lt;math&amp;gt;\sum_{k=1}^K a_k b_k&amp;lt;/math&amp;gt;. When &amp;lt;math&amp;gt;K&amp;lt;/math&amp;gt; is large (it could be in the thousands or more for a single neuron in a fully connected layer of a large model), the potential for floating-point error in that sum increases.&lt;br /&gt;
&lt;br /&gt;
If using FP32, the error may still be negligible in many cases, but in lower precision like FP16, it can become significant. For example, consider a dot product of length 2048 in FP16. Each product is in FP16 (with ~3 decimal digits precision) and if accumulated in FP16, the worst-case error could be on the order of &amp;lt;math&amp;gt;2048 \times \varepsilon_{FP16} \approx 2048 \times 0.0009766 \approx 2&amp;lt;/math&amp;gt; (i.e., you could lose a couple units in the third decimal place relative accuracy in worst case). Even in random error accumulation, you might see error on the order of &amp;lt;math&amp;gt;\sqrt{2048} \times 0.0009766 \approx 0.044&amp;lt;/math&amp;gt; or 4.4% (though this is a rough estimate). That’s quite significant. Fortunately, as discussed, most hardware will accumulate such sums in a higher precision like FP32, dramatically reducing that error (~7 orders of magnitude smaller &amp;lt;math&amp;gt;\varepsilon&amp;lt;/math&amp;gt;). But if it didn’t, the network’s outputs could get noisy. A fully-connected layer with many inputs might output slightly different results depending on summation order if done in low precision.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Large-scale reductions&#039;&#039;&#039; also occur in operations like global average pooling (summing all elements of a feature map) or computing the total loss by summing loss over batch samples. If one sums 100,000 values in FP32, the worst-case error could be on the order of 100,000 &#039;&#039; 1e-7 ≈ 1e-2 (1% uncertainty), though typical error would be much less. In FP16, summing 100k values is likely impossible without overflow: as noted, adding ~10^4 moderate numbers can overflow FP16&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. So large reductions &#039;&#039;must* be handled carefully (either in chunks or in higher precision). Frameworks generally do: for example, when performing a sum reduction on GPU, libraries break it into parallel blocks that accumulate in registers (usually 32-bit registers, even if inputs are 16-bit). &lt;br /&gt;
&lt;br /&gt;
Another aspect is that many dot products in neural nets involve values of varying magnitude. If the distribution of terms is such that some are much larger than others, the smaller terms might not contribute much if summed last. Ideally, one might sort by magnitude or use pairwise summation to improve that. Most BLAS libraries effectively do a pairwise strategy due to their parallel structure. So, while worst-case analyses are grim, practical implementations mitigate a lot of it.&lt;br /&gt;
&lt;br /&gt;
However, certain layers can suffer if not handled properly. One famous example is in RNNs or LSTMs: if one uses FP16 naive summation for the gating mechanism, errors could accumulate over many time steps and cause drift or instability. In modern practice, recurrent layers are often run in FP16 with FP32 accumulators to avoid this.&lt;br /&gt;
&lt;br /&gt;
In summary, &#039;&#039;&#039;dot products and large reductions need sufficient precision&#039;&#039;&#039; or robust algorithms. If you are implementing a custom kernel (say a custom ONNX op) and you have to sum a huge array, you should consider using a technique from Section 3 (pairwise summation, Kahan, or at least double accumulator if possible). Many deep learning computations, by virtue of hardware and library design, already do something like this under the hood. But it&#039;s always good to be aware, especially if pushing into lower precision territories.&lt;br /&gt;
&lt;br /&gt;
=== 6.2. Cancellation, Dynamic Range, and Conditioning ===&lt;br /&gt;
&lt;br /&gt;
We introduced &#039;&#039;cancellation&#039;&#039; earlier in the context of summation. In deep learning, cancellation can appear in a few places. One is in the computation of certain norms or variances. For instance, Batch Normalization computes mean and variance of activations. The variance formula &amp;lt;math&amp;gt;\mathrm{Var}(x) = E[x^2] - (E[x])^2&amp;lt;/math&amp;gt; is subject to catastrophic cancellation if &amp;lt;math&amp;gt;E[x]&amp;lt;/math&amp;gt; and &amp;lt;math&amp;gt;\sqrt{E[x^2]}&amp;lt;/math&amp;gt; are close in magnitude (which can happen if the variance is small compared to the mean). If implemented naively in single precision, you might get a negative variance due to numerical error (which would then be clamped to zero or sqrt would yield NaN). Robust implementations compute variance in a single pass or increase precision. Indeed, in many frameworks, batchnorm accumulation is done in FP32 even if the activations are FP16, precisely to avoid numerical issues in computing mean and variance. ONNX itself doesn’t specify this, but an ONNX backend is likely to do batchnorm stats in higher precision for safety. The &#039;&#039;conditioning&#039;&#039; of the variance calculation is poor when variance is tiny relative to mean, so it&#039;s a spot where higher precision is needed.&lt;br /&gt;
&lt;br /&gt;
Another example is in certain loss functions or regularizations. If you subtract two large numbers to get a small difference, you lose precision. Say you have two nearly identical predictions and you subtract them as part of some operation – if done in float16, you might get zero whereas float32 would have a slight difference. This could affect, say, a gradient if that difference was supposed to propagate information. Fortunately, neural nets often avoid directly subtracting large nearly equal numbers; but it can happen. In attention mechanisms, the &#039;&#039;score normalization&#039;&#039; (softmax) subtracts a max to avoid huge exponentials, which is good for range but if multiple scores are close to the max, you subtract numbers that are close. However, those subtractions are not catastrophic in the same way; they&#039;re intentional to reduce range and are typically fine in FP32 or even FP16.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Dynamic range&#039;&#039;&#039; issues are everywhere in deep learning. Activation functions like ReLU and exponential can produce very large or very small values. If subsequent computations don’t account for that, you get infinities or zeros. For example, consider a network output that produces a logit of 100 in FP16. &amp;lt;math&amp;gt;\exp(100)&amp;lt;/math&amp;gt; in FP16 overflows to infinity (since FP16 max ~ 6e4 and &amp;lt;math&amp;gt;e^{100} \sim 3.7e43&amp;lt;/math&amp;gt;). If you then do softmax, that one infinity will dominate the sum and you might get a probability of 1 for that class (or NaNs if not handled). The standard softmax implementation subtracts the max logit, so in this case it would subtract 100 from all logits, making the largest exponent &amp;lt;math&amp;gt;\exp(0)=1&amp;lt;/math&amp;gt; and others tiny – this avoids overflow. But what about underflow? If you had a very negative logit, &amp;lt;math&amp;gt;\exp(-100)&amp;lt;/math&amp;gt; in FP16 would underflow to 0. In softmax, that just contributes ~0 probability, which is fine (it effectively is zero anyway). Underflow tends to be less dangerous than overflow in inference — underflow usually just means “this probability/neuron is effectively 0,” whereas overflow can poison the results with NaNs or Infs. &lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Conditioning&#039;&#039;&#039; refers to how sensitive a function is to input perturbations. Neural networks themselves as functions can be poorly conditioned (e.g., tiny changes in weights cause big changes in output for some architectures), but typically they are designed to be reasonably well-behaved. Certain internal computations like matrix inversion (if one does a pseudoinverse or something for a layer) can be extremely ill-conditioned if the matrix is near singular. That again would need double precision perhaps. In ML, one common ill-conditioned operation is the &#039;&#039;&#039;softmax denominator&#039;&#039;&#039;: &amp;lt;math&amp;gt;\sum_j e^{x_j}&amp;lt;/math&amp;gt; can be very large and also very sensitive to changes in the largest &amp;lt;math&amp;gt;x_j&amp;lt;/math&amp;gt;. However, the softmax is more stable if computed carefully (max trick). Another is &#039;&#039;&#039;normalization&#039;&#039;&#039;: computing &amp;lt;math&amp;gt;\frac{x - \mu}{\sigma}&amp;lt;/math&amp;gt; for layer norm or batch norm – if &amp;lt;math&amp;gt;\sigma&amp;lt;/math&amp;gt; is very small, then the division blows up noise. In those cases, a small &amp;lt;math&amp;gt;\epsilon&amp;lt;/math&amp;gt; is usually added to &amp;lt;math&amp;gt;\sigma^2&amp;lt;/math&amp;gt; to prevent instability (this is a model-level fix, not a numeric one per se, but it acknowledges numeric limits).&lt;br /&gt;
&lt;br /&gt;
In training, &#039;&#039;&#039;optimizers&#039;&#039;&#039; can suffer from precision issues. For example, the Adam optimizer keeps a moving average of squared gradients; in half precision, these small updates might flush to zero or not change at some point. Some research found that certain adaptive optimizers break in FP16 because the small differences are lost (&amp;quot;Why FP16 training breaks NAdam&amp;quot; etc.). The solution is often to keep the optimizer states in FP32 while doing the bulk of compute in FP16. This again is a form of mixed precision.&lt;br /&gt;
&lt;br /&gt;
From an inference perspective, once the model is trained, the main concerns are avoiding overflow in activations and ensuring summations (like in softmax or reductions) are done with enough precision. The model’s weights are fixed, so we don&#039;t have to worry about cumulative error over iterations as in training, just the forward pass.&lt;br /&gt;
&lt;br /&gt;
=== 6.3. Normalization Layers and Softmax ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Normalization layers&#039;&#039;&#039; (BatchNorm, LayerNorm, etc.) and &#039;&#039;&#039;Softmax&#039;&#039;&#039; are specifically worth examining for numerical stability:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Batch Normalization:&#039;&#039;&#039; During inference, batchnorm applies an affine transform using precomputed mean and variance (from training). It’s basically: &amp;lt;math&amp;gt;y = \frac{x - \mu}{\sqrt{\sigma^2 + \epsilon}} \gamma + \beta&amp;lt;/math&amp;gt;. This is a stable operation as long as &amp;lt;math&amp;gt;\epsilon&amp;lt;/math&amp;gt; is provided (to avoid division by zero). Since &amp;lt;math&amp;gt;\mu, \sigma^2&amp;lt;/math&amp;gt; are constants in inference, it reduces to linear ops which are fine. The main numerical work (computing mean/variance) happened during training or model calibration, and frameworks usually did that in FP32. So batchnorm at inference time is not a source of instability (it’s just a scale and shift of the data).&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Layer Normalization / Group Normalization:&#039;&#039;&#039; These compute mean/var on the fly for each sample (and possibly channel group). In inference, they still need to compute these statistics for each forward pass. If implemented in low precision, the variance calculation can suffer cancellation as mentioned. However, it&#039;s likely that inference engines compute the mean and variance in FP32 even if inputs are FP16 (especially since these are per-sample operations, not huge batch reductions – it&#039;s feasible to do in higher precision). If someone implemented layer norm purely in FP16, they might see issues when the variance is very small. Usually there’s an &amp;lt;math&amp;gt;\epsilon&amp;lt;/math&amp;gt; added which might dominate a tiny variance and thus mitigate catastrophic cancellation (the result might just output something mostly from &amp;lt;math&amp;gt;\epsilon&amp;lt;/math&amp;gt;). In summary, normalization layers are usually fine as long as the implementation uses enough precision for the summation of squares.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Softmax:&#039;&#039;&#039; Softmax is notorious if done improperly. The safe algorithm: subtract the maximum input from all inputs (to bring the largest exponent to 1.0), exponentiate, sum them up, divide each exponent by the sum. The max subtraction handles overflow in &amp;lt;math&amp;gt;\exp()&amp;lt;/math&amp;gt;. But what about the sum? As discussed, if there are &amp;lt;math&amp;gt;N&amp;lt;/math&amp;gt; elements in the softmax, the sum is at most &amp;lt;math&amp;gt;N&amp;lt;/math&amp;gt; (when all inputs equal the max). In classification, &amp;lt;math&amp;gt;N&amp;lt;/math&amp;gt; might be the number of classes. For ImageNet, &amp;lt;math&amp;gt;N=1000&amp;lt;/math&amp;gt;, so worst sum ~1000 (which is fine in FP16, as 1000 &amp;lt;&amp;lt; 65504). For an extreme case like a language model, &amp;lt;math&amp;gt;N&amp;lt;/math&amp;gt; could be 50,000 (vocab size). Sum could be 50,000 in worst case (everyone equal probability). 50,000 is still below 65504, so actually an FP16 sum would &#039;&#039;not&#039;&#039; overflow in that scenario, and the rounding error on a sum of 50k identical terms would be maybe a few ULPs (which at 50k magnitude might be ~0.5 difference worst-case). So maybe softmax summation in FP16 is not catastrophic for output probabilities – you’d get essentially 1.0 after normalization instead of 0.99999 in some case. Usually fine.&lt;br /&gt;
&lt;br /&gt;
:The bigger issue is if one or a few terms dominate the softmax. If one term is much larger, the others underflow to 0 after the max subtraction. That’s fine: you get a one-hot distribution essentially. If the model expected a softer distribution, that could be a problem. But if the logits were really that far apart, even FP32 would give nearly one-hot. So not really a precision issue, more of a model property.&lt;br /&gt;
&lt;br /&gt;
:However, the &#039;&#039;&#039;stability during training&#039;&#039;&#039; is a big issue. The GitHub thread we saw (ColossalAI) mentioned training a 130B model where FP16 softmax in attention caused training to occasionally blow up&amp;lt;ref name=&amp;quot;ref14&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref14&amp;quot;/&amp;gt;. They observed attention scores in some heads ranging up to 1e4 or as low as 1e-3&amp;lt;ref name=&amp;quot;ref14&amp;quot;/&amp;gt;. 1e4 exponent in FP16 is definitely Inf (because &amp;lt;math&amp;gt;\exp(10^4)&amp;lt;/math&amp;gt; is astronomically huge, but even exp(100) is inf in half). The method CogView introduced (Precision Bottleneck Relaxation) involved subtracting the max &#039;&#039;per head&#039;&#039; to reduce range&amp;lt;ref name=&amp;quot;ref14&amp;quot;/&amp;gt;—which is basically the softmax max trick at a finer granularity or something similar. In the end, they found &#039;&#039;&#039;the simplest fix was to compute softmax in FP32&#039;&#039;&#039; for those layers&amp;lt;ref name=&amp;quot;ref14&amp;quot;/&amp;gt;. They reported that it had negligible speed impact but significantly improved stability&amp;lt;ref name=&amp;quot;ref14&amp;quot;/&amp;gt;. This highlights a general principle: certain operations like softmax (and sometimes matrix multiplications in attention) benefit from higher precision, and using FP32 in a few critical spots can save a mixed-precision training from crashing. In inference, using FP32 for softmax is also a safeguard if one is uncertain. It costs little because softmax is not a heavy compute compared to matrix multiplies.&lt;br /&gt;
&lt;br /&gt;
:ONNX does have a Softmax operator; if the input is float16, a backend might do exactly what they described: internally cast to float32 for the softmax computation, then cast output back to float16. Some inference engines (like DeepSpeed’s inference or others) keep attention softmax in FP32 even if rest is FP16, for robustness&amp;lt;ref name=&amp;quot;ref14&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Apart from softmax, other places where exponentials occur are in loss functions like cross-entropy (which uses log-softmax internally), or in activation functions like Sigmoid and Tanh (which are basically scaled logistic functions). Sigmoid(&amp;lt;math&amp;gt;x)=1/(1+e^{-x})&amp;lt;/math&amp;gt;. If &amp;lt;math&amp;gt;x&amp;lt;/math&amp;gt; is moderately large positive, &amp;lt;math&amp;gt;e^{-x}&amp;lt;/math&amp;gt; underflows to 0 in FP16 if &amp;lt;math&amp;gt;x&amp;gt;~11&amp;lt;/math&amp;gt;. That gives a sigmoid output of 1.0 exactly. In FP32, &amp;lt;math&amp;gt;e^{-x}&amp;lt;/math&amp;gt; for &amp;lt;math&amp;gt;x=11&amp;lt;/math&amp;gt; is about &amp;lt;math&amp;gt;5e-5&amp;lt;/math&amp;gt;, not underflow, so sigmoid ~0.99995. So FP16 will saturate some outputs to exactly 0 or 1 sooner than FP32 would. This can slightly change behaviors (e.g., a neuron might go to full saturation a bit early). Usually, this isn’t a huge deal, but it could make gradients zero earlier. In inference, we don’t care about gradients, just the output value. Getting exactly 1.0 vs 0.99995 probably has no practical difference for a classification output. But one should be aware that &#039;&#039;&#039;activation functions can saturate faster&#039;&#039;&#039; in lower precision due to limited range or precision. ReLU has no issue (it’s linear or 0), but exponential-based activations do. In practice, again, frameworks often simply compute those in the given precision and accept the slight difference. If it were a problem, one could do those in FP32 as well.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Summary:&#039;&#039;&#039; Numerical stability in inference is maintained by using robust computation methods: subtracting maxima in softmax/LogSumExp, adding epsilon in denominators for normalization, and using higher precision for summations or critical steps. Most of these are built into the algorithms. ONNX models assume these standard practices (they don&#039;t need to explicitly do them in the graph because the backend inherently will for ops like Softmax). It&#039;s wise when building or exporting models to ONNX to test them in the target precision and ensure there are no surprises (like an output becomes NaN or Inf due to precision). Usually if training was done in mixed precision, the model will have been vetted in that environment already.&lt;br /&gt;
&lt;br /&gt;
== Performance vs. Accuracy Trade-offs ==&lt;br /&gt;
&lt;br /&gt;
There is an inherent trade-off between numerical precision and computational performance (speed, memory usage) in AI systems. Choosing lower precision can dramatically increase throughput and reduce memory, but it may introduce arithmetic errors. Engineers must balance these considerations:&lt;br /&gt;
&lt;br /&gt;
=== 7.1. Throughput, Memory Bandwidth, and Precision ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Throughput (speed)&#039;&#039;&#039; is often higher for lower-precision operations. Modern hardware can execute more low-precision ops in parallel than high-precision ops. For example, an NVIDIA V100 GPU can perform 2× more half-precision FLOPs than single-precision per clock, and newer architectures with Tensor Cores can do 8× more (since a Tensor Core warp operation might perform 64 FP16 FMAs per cycle vs 16 FP32). Similarly, int8 operations can be even faster and more energy efficient. Lower precision numbers also use less memory bandwidth – fetching 16-bit data is twice as fast as 32-bit data from memory (assuming the same bus width), which can relieve memory bottlenecks. Many deep learning workloads are memory-bound, so halving the data size can nearly double throughput if compute is not the limiting factor. This is a huge incentive to use lower precision.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Memory and storage&#039;&#039;&#039;: Using FP16 or int8 weights reduces model size, which is important for deploying large models to memory-limited environments (like edge devices or GPUs with limited VRAM). It also means you can fit larger batch sizes in memory, or more models on a chip, etc. There is also a cascade effect: smaller data means better cache utilization, which further improves performance.&lt;br /&gt;
&lt;br /&gt;
However, &#039;&#039;&#039;accuracy&#039;&#039;&#039; suffers if precision is too low. We have discussed how rounding error and limited range can cause deviations in outputs. The question is: do those deviations matter for the application? Often for neural networks, a small amount of noise or error in intermediate calculations does not change the final prediction. Neural nets are somewhat error-tolerant due to their redundant distributed representations. This is why quantization and FP16 techniques can work at all. The final model accuracy (e.g. top-1 accuracy on ImageNet) might drop only 0.5% or less when using FP16 instead of FP32, or int8 instead of FP16, if done properly. For many use-cases, this loss is acceptable given the speed gains.&lt;br /&gt;
&lt;br /&gt;
If one goes too low (like naive FP8 or int4 without special care), accuracy might drop severely – perhaps making the model unusable. Thus the trade-off: as precision decreases, performance increases but at some point accuracy degrades beyond an acceptable threshold. Engineers aim to find the lowest precision that still preserves accuracy.&lt;br /&gt;
&lt;br /&gt;
There is also the consideration of &#039;&#039;&#039;accumulation strategy&#039;&#039;&#039; affecting performance: for instance, Kahan summation (compensated) is more accurate but ~4× slower than naive summation. If you need that accuracy, you pay a performance cost. In many AI inference scenarios, the slight extra accuracy from something like Kahan isn&#039;t needed, so it’s not used. But in a scenario where you sum millions of elements for a final result that is thresholded, maybe you do need it.&lt;br /&gt;
&lt;br /&gt;
Another dimension is &#039;&#039;&#039;determinism vs speed&#039;&#039;&#039;. Enforcing a deterministic reduction order (for reproducibility) might require disabling some parallelism, which could slow things down. If exact repeatability is critical (like certain scientific or financial applications of neural nets), one might sacrifice some performance to ensure the summation happens the same way every time. ONNX allows both modes depending on the runtime.&lt;br /&gt;
&lt;br /&gt;
From a memory perspective, if precision is higher than needed, you waste memory. For example, storing intermediate activations in FP32 when they could be FP16 uses 2× memory, which might mean lower batch size or more memory traffic. So there&#039;s strong motivation to keep things as low-precision as possible &#039;&#039;except&#039;&#039; where accuracy truly demands.&lt;br /&gt;
&lt;br /&gt;
In summary, the trade-off often manifests as &#039;&#039;diminishing returns&#039;&#039;: FP64 vs FP32 – huge slowdown for usually negligible accuracy gain in ML; FP32 vs FP16 – moderate speedup for usually minor accuracy hit; FP16 vs int8 – another big speedup but maybe more accuracy loss unless calibration is done; int8 vs int4 – speed gain but often big accuracy drop that might not be worth it for current models.&lt;br /&gt;
&lt;br /&gt;
Engineers use techniques like &#039;&#039;&#039;mixed precision&#039;&#039;&#039; to target high precision only where needed. For example, run most of the model in FP16, but keep a few sensitive layers in FP32. This yields most of the performance benefits while ensuring critical parts are accurate. We already saw softmax as one example. Another might be final output layer: sometimes using FP32 for the logits and softmax can be safer.&lt;br /&gt;
&lt;br /&gt;
Hardware is also trending to support mixed modes – like FP8 Tensor Cores accumulate in FP32, etc. So hardware tries to give accuracy cheaply.&lt;br /&gt;
&lt;br /&gt;
=== 7.2. Hardware Accelerators and Tensor Cores ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Hardware accelerators&#039;&#039;&#039; (GPUs, TPUs, FPGAs, ASICs like GraphCore IPU, etc.) have specific support for certain precisions. Leveraging these can provide massive speedups:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;NVIDIA Tensor Cores:&#039;&#039;&#039; These started in Volta (V100) with support for 4x4 matrix multiply-adds on FP16 inputs with FP32 accumulation&amp;lt;ref name=&amp;quot;ref6&amp;quot;/&amp;gt;. Using Tensor Cores can give up to 8x throughput compared to using regular FP32 cores. But to use them, the data needs to be FP16 (or now BF16 or FP8 on newer GPUs) and aligned to certain dimensions (multiples of 8 etc. in matrix sizes, which frameworks handle by padding if needed). If an ONNX model is all FP32, by default it would not use Tensor Cores unless the runtime automatically converts some ops to mixed precision (which some do via a flag or environment variable enabling TF32 or FP16). The introduction of &#039;&#039;&#039;TensorFloat-32 (TF32)&#039;&#039;&#039; in Ampere GPUs is essentially a compromise: treat FP32 inputs as two 16-bit significand halves (10-bit mantissa actually) so that it can run on Tensor Cores, giving ~8x speed, while still outputting FP32. TF32 has about the precision of FP16 but range of FP32, and it was chosen because it typically doesn’t hurt convnet accuracy. ONNX Runtime or others on Ampere will use TF32 by default for conv and matmul unless disabled, giving a free speed boost at minimal accuracy cost. If exact FP32 is needed, users have to turn it off (sacrificing speed).&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;TPU (Google):&#039;&#039;&#039; TPUs have native support for BF16 matrix ops with FP32 accumulators. They were designed specifically for the mixed precision approach in training. For inference, TPUs also support int8 and other quantized ops (e.g., via Edge TPU for lower-power). If exporting ONNX to run on a TPU (via some bridge), one might get automatic BF16 usage. BF16 has the advantage of no loss in range, so TPUs can more straightforwardly run models in BF16 without modifications.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;FPGAs and ASICs:&#039;&#039;&#039; Some custom accelerators might use fixed-point or block floating-point arithmetic for efficiency. They often treat the network in quantized terms. The ONNX quantization specification (QuantizeLinear/DequantizeLinear ops) exists for representing quantized models (INT8 etc.), which is another domain beyond floating-point, but it overlaps as a precision issue. In those systems, accumulators for INT8 × INT8 multiplies are usually 32-bit (to avoid overflow for dot products up to certain lengths). The trade-off there is between integer vs float representation, which can give big speedups on CPUs/DSAs via SIMD instructions (many CPUs have vector int8 multiply-add instructions now for inference).&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Memory bandwidth on hardware:&#039;&#039;&#039; Some accelerators include special high-bandwidth memory or cache specifically for lower precision. For example, an accelerator might fetch two FP16 values in one 32-bit memory transaction – effectively doubling memory bandwidth for half precision. On some hardware, using FP16 can even reduce memory &#039;&#039;latency&#039;&#039; if multiple values can be packed.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Parallelism and Vectorization:&#039;&#039;&#039; Many hardware units process data in fixed vector widths (e.g., 256-bit registers). That could be 8 floats at once or 16 half-floats at once in the same 256 bits. If your code is in FP16, you might use the full vector width (16 values) whereas FP32 might only use half (8 values). This means FP16 can make better use of vector units. Similarly, matrix units often have fixed tile sizes, and smaller data types allow larger tiles or more tiles concurrently.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Energy efficiency:&#039;&#039;&#039; Lower precision often means less energy per operation, which on large deployments (data centers or battery-powered devices) is a significant advantage. This isn&#039;t directly performance, but it&#039;s an important trade: you might choose a slightly lower precision to save power even if accuracy drops a tiny bit.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Software overhead:&#039;&#039;&#039; Using mixed precision on hardware involves some overhead of casting and moving data between precisions. For instance, if you have to frequently cast FP16 to FP32 and back, that adds instructions and memory copies. A well-designed accelerator (like GPUs with unified memory for FP16/32 or special hardware casting) minimize this. Still, the runtime might insert extra operations (as we saw, ONNX Runtime CPU inserted many Cast nodes converting FP16 to FP32 and back&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;, which actually &#039;&#039;slowed down&#039;&#039; that FP16 model by 4x vs FP32 on a CPU, because the CPU had no native FP16). So ironically, on some hardware, using lower precision in ONNX might degrade performance if not supported (the CPU case). Always ensure the target hardware actually supports the low precision you plan to use; otherwise, you&#039;ll pay overhead for emulation.&lt;br /&gt;
&lt;br /&gt;
To illustrate trade-off: Suppose you have a model that achieves 99% accuracy in FP32 and drops to 98.5% in FP16. If FP16 runs 2x faster, many would accept the 0.5% accuracy drop for double throughput. If int8 runs 4x faster but accuracy drops to 95%, that might be too much drop for some applications, but acceptable for others that value speed. It&#039;s application-dependent.&lt;br /&gt;
&lt;br /&gt;
In critical applications, one might maintain higher precision accumulators or run certain validation steps in high precision to ensure nothing drifted. In research, sometimes a model is trained in mixed precision but validated in double to ensure any small numeric differences don’t influence the reported result (rare, but for scientific computing integration maybe).&lt;br /&gt;
&lt;br /&gt;
In the ONNX world, performance-vs-accuracy is often managed via &#039;&#039;&#039;Execution Providers&#039;&#039;&#039; (EPs) that target specific hardware. For example, the TensorRT EP in ONNX Runtime can take an ONNX model and run it using NVIDIA&#039;s high-performance inference library, which will use FP16 or INT8 if allowed, to maximize speed. The user can configure the EP with precision flags. If the accuracy drop is too high, one might restrict it to FP32. If it&#039;s fine, allow FP16. Tools exist to measure the accuracy difference (like calibrating int8 quantization).&lt;br /&gt;
&lt;br /&gt;
To conclude, the trade-offs revolve around picking the lowest precision that yields acceptable accuracy, and utilizing hardware features fully. ONNX being a high-level spec abstracts this, but it&#039;s ultimately up to the deployment to decide and sometimes up to the exporter to incorporate quantization. ONNX’s role is to be able to &#039;&#039;represent&#039;&#039; these choices (they have added quantized types, FP8 types, etc., to allow the graph to include those when needed).&lt;br /&gt;
&lt;br /&gt;
== Best Practices and Design Guidelines ==&lt;br /&gt;
&lt;br /&gt;
Given the above insights, here are some best practices and guidelines for dealing with numerical precision and accumulators in AI inference (and training) design:&lt;br /&gt;
&lt;br /&gt;
=== 8.1. Choosing an Accumulator Precision ===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Match precision to data length and range:&#039;&#039;&#039; If an operation accumulates a large number of terms, consider using a higher precision accumulator than the input data. For example, for summing &amp;gt;1000 values of type FP16, an FP32 accumulator is recommended to avoid large rounding error or overflow. If summing &amp;gt; million values of FP32, consider double (FP64) accumulation if high accuracy is needed (though this is rare in DL). &#039;&#039;Rule of thumb:&#039;&#039; if &amp;lt;math&amp;gt;N \cdot \varepsilon&amp;lt;/math&amp;gt; is not tiny (say &amp;gt;1e-3), you may benefit from higher precision. For FP16 (&amp;lt;math&amp;gt;\varepsilon \approx 1e-3&amp;lt;/math&amp;gt;), that threshold N is a few hundred. For FP32 (&amp;lt;math&amp;gt;\varepsilon \approx 1e-7&amp;lt;/math&amp;gt;), N would be in the millions before 1e-3 error.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Use higher precision in critical layers:&#039;&#039;&#039; Identify if certain layers of the model are more sensitive to precision. For instance, the output layer or a layer that feeds into a very sharp decision boundary might require more precision. If a small change there could flip the prediction, you don&#039;t want excessive numerical error. Using FP32 for logits and softmax is a common practice to maintain classification confidence stability.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Follow hardware defaults, but override if needed:&#039;&#039;&#039; Many hardware accelerators have sensible defaults (like accumulate in FP32 for tensor ops). It&#039;s usually best to use those defaults as they were chosen to balance accuracy and speed. Only override if you have evidence of a problem. For example, if using ONNX Runtime on CPU, you discovered it upcasts to float for you – great. But if using some custom accelerator that does pure FP16, you might manually insert casts in the ONNX graph around certain ops to force higher precision on those ops.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Maintain precision for gradient accumulation (training):&#039;&#039;&#039; This is more training-oriented, but worth noting: when summing gradients over batches or updating weights, use higher precision (FP32 or even 64) for those accumulations. This ensures training doesn&#039;t diverge due to numeric error. Many frameworks keep a &amp;quot;master copy&amp;quot; of weights in FP32 when training in FP16&amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;. For inference, this translates to: any running statistics or counters in the model (like if you had a running average inside the model) should probably be in higher precision to avoid drift.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Be mindful of intermediate cast costs:&#039;&#039;&#039; If you do decide to cast to higher precision for an operation in ONNX, remember it incurs memory copies and compute overhead. Make sure it&#039;s worth it. For example, if you cast a huge tensor to double, that could slow things drastically. Usually FP16-&amp;gt;FP32 is fine (hardware handles it well), but FP32-&amp;gt;FP64 might be expensive on GPUs.&lt;br /&gt;
&lt;br /&gt;
=== 8.2. Safe Reduction Lengths and Heuristics ===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Chunk long reductions:&#039;&#039;&#039; If you must do a very long sum in low precision (due to memory constraints or hardware), chunk the sum into parts, sum each to a higher precision local accumulator, then sum the partial sums. This is essentially pairwise summation. Many libraries use a fixed chunk size heuristic (e.g., sum in blocks of 256 in registers, then add those up). This bounds the error growth. If implementing manually, you can choose a chunk size such that chunk_length &#039;&#039; ε is small (like 256 &#039;&#039; 1e-3 = 0.256 for FP16, hmm maybe choose 100).&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Sort by magnitude if possible:&#039;&#039;&#039; In some cases (not typical in DL, but possible in post-processing), if you have numbers varying by orders of magnitude, summing from smallest to largest improves accuracy. It prevents small ones from being swallowed by big ones. This is more relevant if you output a sum of mixed-scale values (like some algorithm mixing big and small contributions). It’s not always feasible in real-time inference to sort, but keep in mind.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Use compensation for critical sums:&#039;&#039;&#039; If you find a particular sum or difference in your inference computation is crucial (like computing a small residual from two close numbers), consider using Kahan summation or another compensated scheme just for that part. For example, if you had an ONNX custom function that needs to sum a lot of almost cancelling terms, you might implement it with compensation internally.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Heuristics for FP8/low-bit:&#039;&#039;&#039; When using extremely low precision like FP8, adopt the recommended practices: use scaling (don&#039;t feed raw data into FP8, normalize it first), and perhaps only use FP8 on portions of the network that are empirically robust. The industry is still establishing heuristics, but one might be: use FP8 for matrix multiplications, but accumulate in FP16 or FP32; keep normalization in FP16; use FP16 for first and last layer maybe. This is evolving, so keep an eye on literature.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Keep an eye on sum length in softmax and normalization:&#039;&#039;&#039; If you have extremely large class counts or group sizes (e.g., a softmax over 1e6 items, or layer norm over tens of thousands of features), realize this is a giant reduction. You may want to ensure that reduction is done in higher precision. ONNX Softmax by default will likely not handle a million-class case in FP16 well (the sum would overflow since 1e6 &amp;gt; 65504). So either do that on CPU in FP32 or break it down (could do hierarchical softmax, etc.). This is a corner case, but relevant for huge vocab language models. In practice, vocab ~50k is fine as we said.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Test numeric differences:&#039;&#039;&#039; A practical tip: after quantizing or reducing precision, run some test inputs through both the high-precision model and low-precision model. Compare outputs (and maybe some intermediate signals if possible). If differences are consistently small relative to the application tolerance, you&#039;re good. If you see occasional large differences or instabilities (like outputs become NaN or Inf in low precision), that&#039;s a red flag. Use that testing to guide where you might need to increase precision or change algorithms.&lt;br /&gt;
&lt;br /&gt;
=== 8.3. When Higher Precision Is Required ===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Ill-conditioned calculations:&#039;&#039;&#039; If your model or pipeline performs any operation known to be ill-conditioned (matrix inversion, subtraction of nearly equal quantities, computing a tiny difference of large numbers), you likely need higher precision for that part. For example, if your ONNX model includes a custom op to solve a linear system, use double precision in that op if possible (or at least float with iterative refinement). In ML, this might occur in something like a physics-informed neural network or a differentiable simulation within the model.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Strict accuracy requirements:&#039;&#039;&#039; Some applications (medical, aerospace, finance) might have strict tolerances. If an inference model’s output needs to be correct to 5 decimal places, you probably cannot use FP16, which has only 3-4 decimal digits precision. FP32 might be barely enough (~7 digits). FP64 might be warranted to guarantee that level of accuracy in worst case. Most deep learning applications (like image classification) don&#039;t demand that level of numeric precision in outputs – as long as the top class is correct, it doesn&#039;t matter if its probability was 0.901 vs 0.899. But if you&#039;re doing something like summing probabilities to make a risk estimate or doing a cumulative sum over many predictions, be careful to accumulate precisely.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Large sums or products in post-processing:&#039;&#039;&#039; Sometimes the neural net is fine, but the &#039;&#039;post-processing&#039;&#039; of results might need precision. E.g., summing up probabilities of many events, or computing confidence intervals, etc. Do those calculations in double if needed outside the model.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Preventing drift in long sequences:&#039;&#039;&#039; For sequential models (RNNs processing thousands of steps), rounding error can accumulate over steps. If each step has a small error, after 10000 steps it could add up. In such cases, using a higher precision for the state might reduce drift. Some practitioners use FP64 for the hidden state accumulation in very long sequence processing to maintain numerical stability (though this is not common due to speed loss). Alternatively, periodically renormalize or reset small errors.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Simulations or differential equations:&#039;&#039;&#039; If your ONNX model involves simulating something (like a differential equation solver inside a model, or an ODE/RNN hybrid), those often need double precision to be stable for long durations. Recognize those situations and allocate precision accordingly.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Verification and debugging:&#039;&#039;&#039; Use higher precision as a tool to verify correctness. For example, run your model in FP64 (if possible) and compare to FP32 to see if there are differences. If not, FP32 is fine. If yes, you might find a particular calculation losing too much info in FP32, implying maybe you should keep it in FP64 in production as well. This is rare in inference but can be part of rigorous validation.&lt;br /&gt;
&lt;br /&gt;
In ONNX, using higher precision may involve casting to &amp;lt;code&amp;gt;tensor(double)&amp;lt;/code&amp;gt; for certain ops. Not all runtimes support double on GPU though (some do via emulation or just slower path). ONNX CPU does support double for general ops. So be mindful of the target.&lt;br /&gt;
&lt;br /&gt;
Finally, &#039;&#039;&#039;document your precision choices&#039;&#039;&#039;. If you deliver an ONNX model that uses mixed types (float16 inputs, float32 accumulations, int8 weights, etc.), ensure that is clear to whoever maintains or uses it. Precision issues can be sneaky, so clarity helps.&lt;br /&gt;
&lt;br /&gt;
== Limitations and Open Problems ==&lt;br /&gt;
&lt;br /&gt;
Despite advances in handling numerical precision, there remain limitations and open challenges in this domain:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Lack of Precision Specification in ONNX:&#039;&#039;&#039; As noted, ONNX currently has no way to formally specify that an op &#039;&#039;should&#039;&#039; use a higher precision internally. This can lead to nondeterministic differences across platforms&amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;. While proposals exist to add attributes for this&amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;, it is not yet standard. Thus an ONNX model might behave slightly differently on two compliant backends, and there&#039;s no built-in mechanism to ensure identical results. This is a limitation for applications requiring cross-platform consistency. Solving it may involve extending ONNX (e.g., an attribute like &amp;lt;code&amp;gt;accumulatePrecision=&amp;quot;FLOAT32&amp;quot;&amp;lt;/code&amp;gt; for MatMul) or tightening the spec, but those come with backward compatibility and implementation burden concerns.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Reproducibility and Determinism:&#039;&#039;&#039; Even with the same precision, parallel computation can introduce nondeterminism (due to different summation orders). Ensuring bit-identical results across runs (important for some sensitive domains) is non-trivial. Frameworks often provide &amp;quot;deterministic mode&amp;quot; which might use fixed ordering at cost of speed. In ONNX, there&#039;s no global flag for this; it&#039;s up to the runtime. More research could be done on algorithms that produce bit-wise identical results independent of parallel execution order (perhaps using techniques like pairwise summation or consistent partitioning). Stochastic rounding is another area: sometimes using probabilistic rounding can reduce bias in accumulated error, but it introduces randomness – reconciling that with reproducibility is tricky.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Ultra-low Precision (FP8 and below):&#039;&#039;&#039; While FP8 is being explored, going to 4-bit floats or mixed schemes is an open challenge. The OpenAI community is investigating 4-bit (quantization-wise) for LLMs with some success, but a true 4-bit float (like E3M1) would be extremely limited. Perhaps hybrid schemes (like block floating point, log scales, etc.) will come. ONNX already has some support (they mention 4-bit in documentation placeholders&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;), but it&#039;s nascent. Research into training techniques, calibration, and new number formats (like &#039;&#039;&#039;posit numbers&#039;&#039;&#039; or tapering precision floats) is ongoing. If a new format proves useful, ONNX will need to adapt to represent it.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Quantization Noise and Model Robustness:&#039;&#039;&#039; Reducing precision can sometimes expose models to issues like adversarial vulnerability or fragility – e.g., a slight perturbation might flip a decision if the margin was small and precision loss nudged it. Ensuring model robustness under quantization is an open problem. It might require re-training the model with quantization in the loop (quantization-aware training) or designing models that have large &amp;quot;margin&amp;quot; between classes to tolerate quantization noise.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Dynamic range during training vs inference:&#039;&#039;&#039; In training, activations can have wider distributions (especially early in training). Models often learn to constrain their ranges (via activation functions saturating or normalization), which makes inference more stable in lower precision. But if one tries to train in FP8 from scratch, it&#039;s very hard. This gap between what&#039;s possible in a post-trained inference model vs training is an open area – how to train directly in low precision without significant accuracy loss. Solving that could simplify workflows (no need for high-precision training then quantize; just train low-prec end to end). Techniques like loss scaling, gradient clipping, etc., are partial solutions. The development of optimizers that are resilient to low precision is ongoing.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Hardware variability:&#039;&#039;&#039; As new hardware emerges (with new numeric capabilities or quirks), ensuring that ONNX models run consistently is an ongoing task. For instance, one device might implement a fused op that reduces rounding error, another does it separately. Even something like convolution might use a different algorithm (FFT-based vs direct) which has different numeric properties. Open problem: could ONNX allow specifying numeric tolerances per op or preferred algorithms? That may be overkill for most, but for some HPC ML use cases, maybe desired.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Testing and validation of numerics:&#039;&#039;&#039; There isn&#039;t a standardized set of &amp;quot;numeric compliance&amp;quot; tests for ONNX runtimes beyond simple correctness. Perhaps in the future, test suites might include challenging numeric scenarios (large sums, nearly singular matrices) to ensure a runtime handles them gracefully (maybe by switching precision). Right now, it&#039;s mostly on the user to test their model under expected conditions.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Combining quantization with floating point:&#039;&#039;&#039; Many production models use a mix (e.g., int8 for some layers, FP16 for others). ONNX can represent this (with QuantizeLinear ops or MixedPrecision through casting), but tooling to automatically find the optimal mix is still developing. It&#039;s somewhat trial-and-error or using heuristics. An open question is how to automate precision assignment: given a model and a target hardware, how to automatically choose which layers can be int8, which should be FP16, etc., to meet an accuracy target. This crosses into NAS (neural architecture search) territory or automated quantization. &lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Numerical debugging tools:&#039;&#039;&#039; When a model goes wrong due to precision, it&#039;s often hard to pinpoint where. Better tooling to analyze numerical error layer by layer could be useful. For instance, one could run a model in double and float and diff the intermediate results to see where the largest divergence happens. Doing this systematically for big nets is an open tooling challenge. Some frameworks have hooks for this, but not standardized.&lt;br /&gt;
&lt;br /&gt;
In conclusion, while we&#039;ve come far with using lower precision in AI without sacrificing much accuracy, the landscape is always evolving. The key open problem is how to push precision lower (for efficiency gains) without crossing the threshold where accuracy falls off a cliff. Each new generation of hardware and techniques (like FP8, novel quantization schemes) extends that boundary, but also brings new questions on how to manage the numerical errors. ONNX as an interchange will need to keep pace, possibly by adding more expressive power for these numeric considerations.&lt;br /&gt;
&lt;br /&gt;
== See Also ==&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;IEEE 754 Floating-Point&#039;&#039;&#039; – The standard defining binary16, binary32, binary64 formats and rounding behavior.&lt;br /&gt;
* &#039;&#039;&#039;Floating-Point Arithmetic&#039;&#039;&#039; – General topic on how floating-point math works, common issues (overflow, underflow, cancellation).&lt;br /&gt;
* &#039;&#039;&#039;Kahan Summation Algorithm&#039;&#039;&#039; – Algorithm for compensated summation to reduce numerical error&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;.&lt;br /&gt;
* &#039;&#039;&#039;BFloat16&#039;&#039;&#039; – A short-format float with 8-bit exponent, used in TPUs and some GPUs for training networks.&lt;br /&gt;
* &#039;&#039;&#039;Mixed Precision Training&#039;&#039;&#039; – Technique of using FP16 (or BF16) along with FP32 accumulators to accelerate training&amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;.&lt;br /&gt;
* &#039;&#039;&#039;Quantization in Deep Learning&#039;&#039;&#039; – Converting networks to lower bit integer arithmetic for inference (INT8 quantization and calibration techniques).&lt;br /&gt;
* &#039;&#039;&#039;ONNX Operator Specification&#039;&#039;&#039; – Official docs for ONNX ops, which describe their mathematical function but not the required numeric precision (a point of potential extension in future).&lt;br /&gt;
* &#039;&#039;&#039;CUDA Tensor Cores &amp;amp; TF32&#039;&#039;&#039; – NVIDIA documentation on how tensor cores perform mixed precision and the TF32 format for accelerating FP32.&lt;br /&gt;
* &#039;&#039;&#039;Pairwise Summation&#039;&#039;&#039; – Summation algorithm with logN error growth&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;, used in many numerical libraries to improve accuracy of reductions.&lt;br /&gt;
* &#039;&#039;&#039;Softmax Stability&#039;&#039;&#039; – Articles on numerically stable softmax and log-sum-exp implementations to avoid overflow.&lt;br /&gt;
* &#039;&#039;&#039;Performance Tuning for Low Precision&#039;&#039;&#039; – Guides (e.g., NVIDIA or Intel whitepapers) on using low precision and the trade-offs involved in various hardware.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;references&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref1&amp;quot;&amp;gt;Accelerate and simplify Scikit-learn model inference with ONNX Runtime - Microsoft Open Source Blog — opensource.microsoft.com — https://opensource.microsoft.com/blog/2020/12/17/accelerate-simplify-scikit-learn-model-inference-onnx-runtime&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref2&amp;quot;&amp;gt;Floating Point Precision: Understanding FP64, FP32, and FP16 in Large Language Models - DEV Community — dev.to — https://dev.to/lukehinds/floating-point-precision-understanding-fp64-fp32-and-fp16-in-large-language-models-3gk6&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref3&amp;quot;&amp;gt;Floating Point Precision and Its Limitations | by Umair Akbar | Medium — akbu.medium.com — https://akbu.medium.com/floating-point-precision-and-its-limitations-cfb7247d7789&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref4&amp;quot;&amp;gt;Kahan summation algorithm - Wikipedia — en.wikipedia.org — https://en.wikipedia.org/wiki/Kahan_summation_algorithm&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref5&amp;quot;&amp;gt;Pairwise summation - Wikipedia — en.wikipedia.org — https://en.wikipedia.org/wiki/Pairwise_summation&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref6&amp;quot;&amp;gt;Numerical behavior of NVIDIA tensor cores - PMC — pmc.ncbi.nlm.nih.gov — https://pmc.ncbi.nlm.nih.gov/articles/PMC7959640/&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref7&amp;quot;&amp;gt;Mixed precision and performance-accuracy tuning (neuron-cc) — AWS Neuron Documentation — awsdocs-neuron.readthedocs-hosted.com — https://awsdocs-neuron.readthedocs-hosted.com/en/latest/about-neuron/appnotes/neuron-cc/mixed-precision.html&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref8&amp;quot;&amp;gt;Create Float16 and Mixed Precision Models - ONNX Runtime — onnxruntime.ai — https://onnxruntime.ai/docs/performance/model-optimizations/float16.html&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref9&amp;quot;&amp;gt;[Performance] ONNX FP16 model is having performance bottle neck when compared to FP32 variant · Issue #25824 · microsoft/onnxruntime · GitHub — github.com — https://github.com/microsoft/onnxruntime/issues/25824&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref10&amp;quot;&amp;gt;Float stored in 8 bits - ONNX 1.21.0 documentation — onnx.ai — https://onnx.ai/onnx/technical/float8.html&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref11&amp;quot;&amp;gt;[2209.05433] FP8 Formats for Deep Learning — arxiv.org — https://arxiv.org/abs/2209.05433&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref12&amp;quot;&amp;gt;Should MatMul op allow specification of accumulation precision? · Issue #7072 · onnx/onnx · GitHub — github.com — https://github.com/onnx/onnx/issues/7072&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref13&amp;quot;&amp;gt;Quantize ONNX models | onnxruntime — onnxruntime.ai — https://onnxruntime.ai/docs/performance/model-optimizations/quantization.html&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref14&amp;quot;&amp;gt;Keep Attention Softmax FP32 during FP16/ZeRO Training · Issue #1485 · hpcaitech/ColossalAI · GitHub — github.com — https://github.com/hpcaitech/ColossalAI/issues/1485&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;/references&amp;gt;&lt;/div&gt;</summary>
		<author><name>Timo.stripf</name></author>
	</entry>
	<entry>
		<id>https://www.emmtrix.com/w139/index.php?title=Numerical_Precision_in_ONNX_and_AI_Inference&amp;diff=2902</id>
		<title>Numerical Precision in ONNX and AI Inference</title>
		<link rel="alternate" type="text/html" href="https://www.emmtrix.com/w139/index.php?title=Numerical_Precision_in_ONNX_and_AI_Inference&amp;diff=2902"/>
		<updated>2026-01-30T15:25:22Z</updated>

		<summary type="html">&lt;p&gt;Timo.stripf: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&#039;&#039;&#039;Open Neural Network Exchange (ONNX)&#039;&#039;&#039; is an open standard format for representing machine learning models and neural network computations across different frameworks and hardware&amp;lt;ref name=&amp;quot;ref1&amp;quot;/&amp;gt;. As models are exported and deployed via ONNX, the &#039;&#039;&#039;numerical precision&#039;&#039;&#039; of computations becomes critical. Deep learning inference involves a variety of floating-point operations, and small numerical discrepancies can accumulate and affect model accuracy or reproducibility. This article provides an in-depth overview of floating-point precision issues in numerical computing, with a focus on how they manifest in AI inference and the ONNX ecosystem. We cover floating-point formats and rounding behavior, accumulation strategies for summations, the use of low-precision formats (FP16, BFloat16, FP8) in modern AI, and how ONNX defines (or leaves unspecified) the precision of accumulators in operations. Strategies for maintaining numerical stability in deep neural networks are discussed, along with the trade-offs between computational performance and arithmetic accuracy. The goal is to give engineers and researchers a comprehensive, encyclopedic reference on numerical precision in ONNX-based AI inference systems.&lt;br /&gt;
&lt;br /&gt;
== Floating-Point Precision in Numerical Computing ==&lt;br /&gt;
&lt;br /&gt;
=== 2.1. IEEE 754 Floating-Point Formats ===&lt;br /&gt;
&lt;br /&gt;
Modern computers represent real numbers using the IEEE 754 floating-point standard, which defines formats like 32-bit &#039;&#039;single precision&#039;&#039; (FP32) and 64-bit &#039;&#039;double precision&#039;&#039; (FP64). In IEEE 754 binary formats, a floating-point number is composed of three fields: 1 sign bit, several exponent bits (with a bias), and several fraction (mantissa) bits. For example, FP32 consists of 1 sign bit, 8 exponent bits, and 23 fraction bits (24 bits of significand precision including the implicit leading 1)&amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;. This provides roughly 7 decimal digits of precision and an exponent range allowing values on the order of $10^{\pm38}$. FP64 allocates 1 sign bit, 11 exponent bits, and 52 fraction bits (53-bit precision), yielding about 15–16 decimal digits of precision and a vastly larger range (~$10^{\pm308}$)&amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;. These larger formats (especially FP64) are the &amp;quot;gold standard&amp;quot; for numerical accuracy in scientific computing, as they reduce round-off error in complex calculations&amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;. However, the increased precision comes at the cost of doubled storage and typically reduced computational throughput (e.g. many GPUs execute FP64 instructions at a much lower rate than FP32&amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
In addition to single and double precision, the IEEE 754-2008 standard introduced a 16-bit &#039;&#039;half precision&#039;&#039; format (FP16, or binary16). FP16 has 1 sign bit, 5 exponent bits, and 10 fraction bits (11-bit significand including the implicit 1)&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. This format covers a much smaller dynamic range (approximately $6.1\times10^{-5}$ to $6.5\times10^4$ for normalized values&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;) and offers only around 3 decimal digits of precision. Half precision was originally used in graphics, but it has gained popularity in machine learning for its speed and memory advantages (discussed later in Section 4). Another format relevant to AI is &#039;&#039;&#039;BFloat16&#039;&#039;&#039; (Brain Float 16), a 16-bit float with &#039;&#039;&#039;8 exponent bits and 7 fraction bits&#039;&#039;&#039;&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. BFloat16 sacrifices precision (only ~7 fraction bits ≈ 2–3 decimal digits) in order to have the &#039;&#039;&#039;same exponent range as FP32&#039;&#039;&#039;&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. This means BFloat16 can represent very large or very small numbers (∼$10^{\pm38}$, similar range as FP32) but with much less precision between representable values&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. BFloat16 was introduced for deep learning by Google (for TPUs) to allow reduced precision &#039;&#039;training&#039;&#039; without frequent overflow/underflow, relying on the wide range and accepting the coarse precision.&lt;br /&gt;
&lt;br /&gt;
Each floating-point format has a &#039;&#039;&#039;unit roundoff&#039;&#039;&#039; or machine precision, denoted $\epsilon$. This is effectively the spacing between 1.0 and the next representable number. For FP32, $\epsilon \approx 2^{-23} \approx 1.19\times10^{-7}$, corresponding to about 7–8 decimal digits of precision&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. In FP64, $\epsilon = 2^{-52} \approx 2.22\times10^{-16}$ (15–16 decimal digits). Half precision is much less precise: FP16 has $\epsilon = 2^{-10} \approx 9.76\times10^{-4}$&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;, and BFloat16 has $\epsilon = 2^{-7} = 7.8125\times10^{-3}$&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. These values quantify how finely the continuum of real numbers is quantized in each format.&lt;br /&gt;
&lt;br /&gt;
=== 2.2. Machine Epsilon and Rounding Error ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Machine epsilon ($\varepsilon$)&#039;&#039;&#039; is formally defined as the smallest positive number such that $1.0 + \varepsilon &amp;gt; 1.0$ in the floating-point format&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. It effectively measures the &#039;&#039;&#039;precision&#039;&#039;&#039; of the representation. In FP32, $\varepsilon = 2^{-23} \approx 1.19\times10^{-7}$, meaning any result is rounded to about 7 decimal digits. FP16’s $\varepsilon = 2^{-10} \approx 9.77\times10^{-4}$, over 1000× larger (so only ~3 decimal digits of precision), and BFloat16’s $\varepsilon = 2^{-7} \approx 7.8\times10^{-3}$, reflecting its mere ~8-bit precision&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. Rounding in IEEE 754 by default uses &#039;&#039;&#039;round-to-nearest (ties to even)&#039;&#039;&#039;, which ensures the rounding error for any single operation is at most half a unit in the last place (0.5 ULP). We can model a floating-point addition as: &lt;br /&gt;
&lt;br /&gt;
$$\text{fl}(a+b) = (a+b)\,(1 + \delta),$$ &lt;br /&gt;
&lt;br /&gt;
where $|\delta| \le \varepsilon$ for that format&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. Here $\varepsilon/2$ is the maximum &#039;&#039;&#039;relative error&#039;&#039;&#039; in one rounding (also called unit roundoff). For example, when adding two FP32 numbers, the result is accurate to within about $10^{-7}$ of the true sum (relative), whereas in BF16, the result can deviate by up to $\sim7.8\times10^{-3}$ (nearly 0.78% relative error per operation)&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. Thus, &#039;&#039;&#039;each arithmetic operation incurs a small rounding error&#039;&#039;&#039; (on the order of machine epsilon) that can propagate or accumulate through a sequence of calculations.&lt;br /&gt;
&lt;br /&gt;
Because floating-point representations have finite exponent range, we must also consider &#039;&#039;&#039;overflow&#039;&#039;&#039; and &#039;&#039;&#039;underflow&#039;&#039;&#039;. If a result’s magnitude exceeds the maximum representable value, it overflows to infinity; if it falls below the minimum normal (or minimum subnormal) value, it underflows to zero (or a denormalized tiny value). FP32 and BF16 share an exponent size (8 bits), so their range for normal numbers is roughly $[10^{-38}, 10^{38}]$ (approx $~\!3.4\times10^{38}$ max)&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. FP16’s 5-bit exponent allows max $\sim 6.55\times10^4$ and min normal $\sim 6.10\times10^{-5}$&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. In practice, &#039;&#039;&#039;underflow/overflow&#039;&#039;&#039; can occur in low-precision arithmetic if values are not appropriately scaled. For instance, summing a large number of moderately sized terms in FP16 could overflow where FP32 would not. As an example, summing on the order of $10^4$ values of magnitude ~$10^1$ (say, adding 10,000 terms around 10 each) would produce a total $\sim10^5$, which exceeds FP16’s max finite value ~$6.5\times10^4$ and would overflow to $+\infty$ in half precision&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. Underflow is usually a concern when subtracting nearly equal numbers (catastrophic cancellation yielding a tiny difference that might flush to zero in low precision).&lt;br /&gt;
&lt;br /&gt;
=== 2.3. Accumulation Error in Reductions ===&lt;br /&gt;
&lt;br /&gt;
Floating-point addition is &#039;&#039;&#039;not associative&#039;&#039;&#039;: $(a+b)+c$ can differ from $a+(b+c)$ due to rounding, and thus the order in which we sum a list of numbers can change the result slightly. When summing a large sequence of $N$ numbers $x_1, x_2, ..., x_N$, the &#039;&#039;&#039;accumulated rounding error&#039;&#039;&#039; can be significant. In the &#039;&#039;worst case&#039;&#039; (e.g., adding many positive and negative terms that cause maximal cancellation or always rounding one way), the error can grow on the order of $O(N \varepsilon)$ relative to the exact sum&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;. Simply summing $N$ numbers in sequence (the &#039;&#039;&#039;naive linear summation&#039;&#039;&#039;) has a worst-case error bound proportional to $N\,\varepsilon$&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. This worst-case occurs for adversarial arrangements of values; in more typical scenarios with rounding errors of random signs, one can model the error as a random walk, yielding an expected root-mean-square (RMS) error growth on the order of $O(\sqrt{N}\,\varepsilon)$&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;. Still, with very large $N$, even $\sqrt{N}$ times $\varepsilon$ can become large enough to matter.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Cancellation&#039;&#039;&#039; is a particular problem: if we add two nearly equal numbers of opposite sign, most leading digits cancel and the result loses significant bits of precision. For example, in single precision, subtracting two numbers that agree in the first 6 decimal digits will yield a result with only ~1 digit of accuracy left. Catastrophic cancellation can dramatically increase relative error. Summation is especially sensitive to cancellation if large positive and negative terms are summed naively. The &#039;&#039;&#039;condition number&#039;&#039;&#039; of a summation (the ratio of the sum of absolute values $\sum|x_i|$ to the absolute value of the true sum $|\sum x_i|$) indicates how sensitive the result is to perturbations&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;. If the numbers have mixed signs and nearly cancel out (making the condition number large), even an optimal summation order will have a large relative error. For instance, summing [1.0, $10^{100}$, 1.0, $-10^{100}$] in double precision has an exact sum of 2.0, but naive summation may yield 0.0 due to cancellation; an improved summation algorithm yields the correct 2.0&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
In practical terms, when summing a long list of floating-point numbers (such as accumulating a dot product or reducing a tensor), the &#039;&#039;&#039;accumulation error&#039;&#039;&#039; can become noticeable if no precautions are taken. Techniques to mitigate this error include carefully choosing the summation order or using higher precision for intermediate sums. We will explore these strategies in the next section.&lt;br /&gt;
&lt;br /&gt;
== Accumulators and Reduction Strategies ==&lt;br /&gt;
&lt;br /&gt;
When performing reductions (sums, dot products, etc.), the method of accumulation can greatly affect the accuracy of the result. Below, we discuss various strategies:&lt;br /&gt;
&lt;br /&gt;
=== 3.1. Naive Linear Accumulation ===&lt;br /&gt;
&lt;br /&gt;
The simplest accumulation strategy is to use a single accumulator and add each element sequentially. In C-like pseudocode:&lt;br /&gt;
&lt;br /&gt;
This &#039;&#039;&#039;naive linear summation&#039;&#039;&#039; adds values one by one to the running total. While straightforward and fast, it is prone to the rounding error accumulation described earlier. The partial sum &amp;lt;code&amp;gt;sum&amp;lt;/code&amp;gt; continually grows (in absolute value) as terms are added, so new small terms may fall outside its precision. For example, if &amp;lt;code&amp;gt;sum&amp;lt;/code&amp;gt; is large and positive, adding a comparatively tiny positive number may produce no change because the small term&#039;s significant digits vanish in the presence of the large sum (the increment is below the rounding threshold of &amp;lt;code&amp;gt;sum&amp;lt;/code&amp;gt;). Conversely, adding a small number of opposite sign can suffer from cancellation. The order of addition matters: summing from smallest magnitude to largest (ascending order) tends to yield a more accurate result than summing in descending order, because adding small terms first preserves their contributions before the sum becomes very large. The naive loop above implicitly sums in the given index order; if the data is unsorted, this order is essentially arbitrary.&lt;br /&gt;
&lt;br /&gt;
The performance of linear accumulation is excellent (minimal overhead), and for many applications the resulting error is within acceptable limits. However, for &#039;&#039;&#039;very large N&#039;&#039;&#039; or ill-conditioned sums, the inaccuracy can become a problem. In a reduction involving millions of elements, a single-precision sum could lose several digits of accuracy. In deep learning, this could translate to small discrepancies in layer outputs or aggregated gradients if done naively in low precision.&lt;br /&gt;
&lt;br /&gt;
=== 3.2. Pairwise and Tree-Based Reductions ===&lt;br /&gt;
&lt;br /&gt;
A better approach to summation is &#039;&#039;&#039;pairwise summation&#039;&#039;&#039;, which sums numbers in a binary tree structure rather than linearly. The idea is to recursively split the array into halves, sum each half, then add the two partial sums. This can be implemented as:&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
By always adding numbers of similar magnitudes first (small subsets are reduced to subtotals, then those are added, etc.), pairwise summation avoids the situation of a very large running total absorbing small additions until the final steps. The error of pairwise summation grows logarithmically with $N$ in the worst case (roughly $O(\varepsilon \log_2 N)$)&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;, instead of linearly. Intuitively, at each level of the tree, rounding error is constrained, and there are about $\log_2 N$ levels. In fact, the worst-case relative error bound for pairwise summation (base case of 1 element) is:&lt;br /&gt;
&lt;br /&gt;
\[ |E_N| \le \frac{\varepsilon \log_2 N}{1 - \varepsilon \log_2 N} \sum_{i=1}^N |x_i|, \]&lt;br /&gt;
&lt;br /&gt;
which for practical $N$ simplifies to on the order of $\varepsilon \log_2 N$ times the condition number of the sum&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;. In typical random-error scenarios, the error behaves even better (on the order of $\sqrt{\log N}$)&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;. The key is that &#039;&#039;&#039;pairwise (tree) reduction balances the accumulations&#039;&#039;&#039;, preventing one sum from overwhelming the others too early.&lt;br /&gt;
&lt;br /&gt;
From a performance standpoint, pairwise summation has the same $O(N)$ total additions as linear summation, just a different order. It is also highly amenable to parallelization: independent partial sums can be computed in parallel at each level of the tree. Many numerical libraries and BLAS implementations use a form of blocked or pairwise summation in dot products for improved accuracy at negligible performance cost. For example, NumPy and Julia perform partial pairwise summation by default for better numerical behavior&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=== 3.3. Compensated Summation (Kahan, Neumaier) ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Compensated summation&#039;&#039;&#039; is an alternative technique to improve summation accuracy by tracking the small “lost” quantities. The most famous algorithm is the &#039;&#039;&#039;Kahan summation algorithm&#039;&#039;&#039; (also called Kahan–Babuška)&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. Kahan summation uses an extra variable to accumulate the round-off error. In pseudocode:&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Here &amp;lt;code&amp;gt;c&amp;lt;/code&amp;gt; accumulates the tiny leftovers that would otherwise be dropped&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. Each iteration essentially subtracts the last error from the new addend (&amp;lt;code&amp;gt;y = x[i] - c&amp;lt;/code&amp;gt;) so that &amp;lt;code&amp;gt;t = sum + y&amp;lt;/code&amp;gt; includes the small bits from the previous operation. The new error is then stored in &amp;lt;code&amp;gt;c&amp;lt;/code&amp;gt; for the next iteration. The result is that the final &amp;lt;code&amp;gt;sum&amp;lt;/code&amp;gt; is as if computed with higher precision than the machine’s: Kahan’s algorithm can &#039;&#039;&#039;greatly reduce the error&#039;&#039;&#039; in summing a sequence of floats. In fact, with exact arithmetic for the compensation, Kahan summation yields a result accurate to within 1 or 2 ULP of the true sum, regardless of N (assuming the sequence of partial sums doesn’t overflow)&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. More precisely, Kahan summation achieves a &#039;&#039;&#039;worst-case error independent of N&#039;&#039;&#039; (error bound depends only on machine precision, not the number of terms)&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. In practice, using a double accumulator for summing single-precision numbers via Kahan can give near double-precision accuracy.&lt;br /&gt;
&lt;br /&gt;
An important subtlety is that the compensation variable &amp;lt;code&amp;gt;c&amp;lt;/code&amp;gt; should be kept in &#039;&#039;full precision&#039;&#039;. In the implementation above, we used &amp;lt;code&amp;gt;double&amp;lt;/code&amp;gt; for both sum and c. If summing FP32 values, one might use &amp;lt;code&amp;gt;float sum&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;float c&amp;lt;/code&amp;gt; (both 32-bit), which still improves accuracy, but using a higher precision for the accumulator and compensation (i.e. double) yields even better results. Many implementations of Kahan summation use the same type for sum and c, but it&#039;s assumed to be higher precision than the inputs when possible.&lt;br /&gt;
&lt;br /&gt;
Kahan’s algorithm carries a performance cost: each iteration does five arithmetic operations (two adds, one subtract, etc.) and has a data dependency, which can hinder parallelism or pipelining&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. Thus, it runs slower than naive summation (roughly 2–3× slower or more, depending on CPU pipeline). In loops where summation is a bottleneck, this may be significant. Nonetheless, for modest N or where maximum accuracy is required, compensated summation is extremely useful. In fact, Python’s &amp;lt;code&amp;gt;math.fsum&amp;lt;/code&amp;gt; function uses a variant of Kahan’s algorithm (actually a more advanced multi-part compensation) to sum iterables with high precision.&lt;br /&gt;
&lt;br /&gt;
There are variations and improvements on basic Kahan summation. &#039;&#039;&#039;Neumaier’s algorithm&#039;&#039;&#039; (sometimes called &#039;&#039;Kahan-Babuška-Neumaier&#039;&#039;) improves Kahan’s method by also handling cases where the next input is larger in magnitude than the running sum&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. In such cases, the original Kahan formula could suffer error; Neumaier’s version effectively swaps roles when a huge value comes in. Its pseudocode is:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;text&amp;quot;&amp;gt;&lt;br /&gt;
xyz&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This algorithm ensures the compensation covers both scenarios: when the sum is the bigger addend or when the new term is bigger&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. In the end, it adds the final &amp;lt;code&amp;gt;compensation&amp;lt;/code&amp;gt; once to the result. Neumaier’s method can produce the correct result in cases where basic Kahan fails. For instance, summing [1.0, $+10^{100}$, 1.0, $-10^{100}$] in double precision: regular Kahan summation might give 0.0 (losing the small terms), whereas Neumaier’s algorithm yields 2.0 exactly&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. There are also higher-order compensation methods (carrying multiple compensation terms, e.g., the &#039;&#039;&#039;Klein summation&#039;&#039;&#039; variant&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt; or using arbitrary precision for intermediate sums) that further reduce error at increased computational cost&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
In summary, compensated summation algorithms trade extra computation for greatly improved numerical accuracy. They are especially valuable when summing large arrays of numbers with varying magnitudes, where naive summation could lose most of the low-order bits. Many scientific computing environments provide compensated sum routines for this reason. In deep learning, however, a different approach to improved precision is often taken: &#039;&#039;&#039;using a wider accumulator type&#039;&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
=== 3.4. Mixed-Precision Accumulation ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Mixed-precision accumulation&#039;&#039;&#039; refers to performing arithmetic in a higher precision than the input data. A common scenario is accumulating sums in 32-bit floats when the operands are 16-bit floats. Rather than implementing a compensation algorithm, one can simply use a wider accumulator so that intermediate rounding error is smaller. This is effectively what modern GPUs do for tensor operations: for example, NVIDIA &#039;&#039;&#039;Tensor Cores operate on FP16 input data with an FP32 accumulator&#039;&#039;&#039; (the 16-bit products are accumulated in 32-bit)&amp;lt;ref name=&amp;quot;ref6&amp;quot;/&amp;gt;. In this way, summing 2048 products in a dot product has far less precision loss than it would with a 16-bit accumulator. Hardware that supports mixed precision can give the best of both worlds: fast low-precision multiplies and high-precision accumulation.&lt;br /&gt;
&lt;br /&gt;
Software can also leverage mixed precision. For instance, one can sum an array of &amp;lt;code&amp;gt;float&amp;lt;/code&amp;gt; values into a &amp;lt;code&amp;gt;double&amp;lt;/code&amp;gt; variable, accumulating the result in double precision and then converting back to float at the end:&lt;br /&gt;
&lt;br /&gt;
This approach often yields a more accurate &amp;lt;code&amp;gt;result&amp;lt;/code&amp;gt; than summing in single precision directly, because the intermediate sum carries 52-bit precision. The final rounding to float happens only once at the end (minimizing overall rounding error). In fact, the above is one way to implement &amp;lt;code&amp;gt;math.fsum&amp;lt;/code&amp;gt; in Python (it internally does something similar with multiple partial sums in double precision).&lt;br /&gt;
&lt;br /&gt;
In machine learning training and inference, &#039;&#039;&#039;mixed precision&#039;&#039;&#039; is widely used. When using FP16 for speed, it&#039;s common to accumulate with FP32. For example, &#039;&#039;&#039;Google&#039;s TPUs use BFloat16 for multiplication but accumulate in FP32&#039;&#039;&#039; for matrix ops&amp;lt;ref name=&amp;quot;ref6&amp;quot;/&amp;gt;. AWS’s Inferentia (Neuron) hardware similarly supports FP16/BF16 matrix multiplication with FP32 accumulation&amp;lt;ref name=&amp;quot;ref7&amp;quot;/&amp;gt;. The motivation is that summing hundreds or thousands of half-precision values would otherwise introduce intolerable error or risk overflow. With an FP32 accumulator, the reduction error is drastically reduced (by about $2^{13}$ times, since FP32 has 13 more precision bits than FP16). Mixed-precision accumulation thus provides a simple and effective form of compensation: &#039;&#039;&#039;use a larger bucket to collect many small contributions&#039;&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
There is a spectrum of options: summing int8 or int16 integers might use 32-bit integers as accumulators for the same reason (to avoid overflow). Some BLAS libraries even offer &#039;&#039;extended precision accumulators&#039;&#039; for single precision dot products (accumulating in double). The downside of mixed precision is the extra memory and maybe throughput cost of the wider type. But on many platforms, the add latency is dominated by hardware design rather than bit-width, so using a 32-bit adder vs 16-bit has negligible speed impact if supported natively. The key is whether data movement or register pressure increases.&lt;br /&gt;
&lt;br /&gt;
In summary, &#039;&#039;&#039;using a higher precision accumulator is often the easiest way to improve numerical accuracy&#039;&#039;&#039; for reductions. It is a primary strategy in deep learning hardware to maintain stability while exploiting low precision for storage and initial computations.&lt;br /&gt;
&lt;br /&gt;
== Precision Formats in AI and Machine Learning ==&lt;br /&gt;
&lt;br /&gt;
Modern AI systems use a variety of numeric formats to balance speed, memory, and accuracy. We discuss common precisions and their roles:&lt;br /&gt;
&lt;br /&gt;
=== 4.1. FP32 and FP64 in Training and Inference ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;FP32 (single precision)&#039;&#039;&#039; has been the workhorse of deep learning for years. Most training scripts in frameworks like TensorFlow and PyTorch used FP32 by default, as it offers sufficient precision to propagate gradients and accumulate weight updates over millions of iterations without excessive error. With ~7 decimal digits of precision and a wide exponent range, FP32 can handle the dynamic range of activations and gradients in typical neural networks. In inference, FP32 is often more precision than strictly necessary for final predictions, but it has remained common because of its ease and the fact that many GPU operations were optimized for FP32.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;FP64 (double precision)&#039;&#039;&#039; is rarely used in deep learning model training or inference, except in specific scenarios requiring extreme precision. FP64 is common in scientific computing and certain types of numerical simulations. In neural networks, however, the extra precision is usually not needed for model accuracy—using doubles tends to yield negligible improvements in model metrics but at a large performance cost (since on GPUs FP64 throughput can be 1/2, 1/8, or even 1/32 of FP32 throughput depending on hardware). One exception might be when integrating neural nets into larger physics simulations or when solving ill-conditioned problems as part of the model. Some researchers use FP64 to diagnose numerical issues or ensure reproducibility. Generally though, deep learning has favored lower precision for efficiency, and FP64 is considered overkill for most networks&amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
That said, certain classical machine learning algorithms benefit from double precision. For example, computing the inverse of a matrix (as in a Gaussian Process regression or Kalman filter embedded in a model) can incur large rounding errors in single precision, so using FP64 can significantly improve the result&amp;lt;ref name=&amp;quot;ref1&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref1&amp;quot;/&amp;gt;. The ONNX standard supports a float64 tensor type for such needs. In ONNX Runtime, however, emphasis has been on FP32 performance, and some pipelines default to float even if the original model was double&amp;lt;ref name=&amp;quot;ref1&amp;quot;/&amp;gt;. If a model’s prediction computation involves operations that are numerically sensitive (like matrix inversion, Cholesky decomposition, or summing extremely large counts), then FP64 may be required to avoid noticeable discrepancies&amp;lt;ref name=&amp;quot;ref1&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref1&amp;quot;/&amp;gt;. A good practice is to test a model in both float and double if you suspect precision issues; significant differences indicate an ill-conditioned calculation that might need higher precision.&lt;br /&gt;
&lt;br /&gt;
In summary, FP32 remains the &#039;&#039;&#039;default&#039;&#039;&#039; format for most training and inference due to its balance of accuracy and efficiency&amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;, while FP64 is reserved for niche cases or precision-critical subroutines. FP32 typically achieves the same accuracy as FP64 for end-to-end deep learning tasks, because network training can often compensate for small noise, and other sources of error (like generalization error) dominate. Therefore, the community trend has been moving to &#039;&#039;&#039;lower&#039;&#039;&#039; precisions, not higher, to gain speed — provided the models still converge and perform well.&lt;br /&gt;
&lt;br /&gt;
=== 4.2. FP16 and BFloat16 in Deep Learning ===&lt;br /&gt;
&lt;br /&gt;
To further speed up neural network training and inference, &#039;&#039;&#039;half-precision (FP16)&#039;&#039;&#039; was adopted, especially with the advent of GPUs supporting FP16 math at twice the rate of FP32. Using FP16 reduces memory storage and bandwidth by 50% compared to FP32, which is significant for large models. However, FP16’s small exponent range and precision posed challenges: training a large network purely in FP16 can fail due to gradient underflow/overflow or weight updates not registering (because changes are below the 0.001 quantization step). To address this, frameworks introduced &#039;&#039;&#039;mixed-precision training&#039;&#039;&#039;, where compute-intensive parts (matrix multiplies, convolutions) run in FP16, but certain accumulations and model parameters remain in FP32. For instance, NVIDIA’s &#039;&#039;&#039;Tensor Core&#039;&#039;&#039; units require FP16 (or BF16) inputs but accumulate results in FP32, effectively ensuring that reductions are done in higher precision&amp;lt;ref name=&amp;quot;ref6&amp;quot;/&amp;gt;. Research showed that with proper loss scaling and mixed precision, models can train to FP32-level accuracy using mostly FP16 arithmetic, achieving significant speedups.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;BFloat16 (BF16)&#039;&#039;&#039; emerged as an alternative 16-bit format primarily from Google’s TPU training systems. BF16 has a larger exponent (8 bits, same as FP32) but only 7 fraction bits&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. This means BF16 can handle the &#039;&#039;&#039;same dynamic range&#039;&#039;&#039; as FP32 (no overflows where FP32 wouldn’t) but at the cost of even lower precision than FP16 (BF16 has about 2–3 decimal digits of precision vs FP16’s ~3–4). Despite its coarse precision, BF16 turns out to be effective for training deep networks because the algorithms (stochastic gradient descent, etc.) are somewhat robust to that noise, and having the wide range prevents catastrophic loss of signal (no gradient will overflow to Inf unless it was going to in FP32 anyway). Many training frameworks (PyTorch, TensorFlow) support BF16 training on hardware like &#039;&#039;&#039;TPU, NVIDIA A100+, Intel CPUs with AVX-512 BF16, etc&#039;&#039;&#039;. Typically, when using BF16, &#039;&#039;&#039;accumulations are still done in FP32&#039;&#039;&#039; (for example, a TPU v2/v3 multiplies BF16 matrices and accumulates into an FP32 result for the dot product&amp;lt;ref name=&amp;quot;ref6&amp;quot;/&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
In inference, both FP16 and BF16 are used to deploy models for faster speed or lower memory. FP16 is popular on NVIDIA GPUs (which have specialized FP16 units). BF16 is advantageous on TPUs and some CPUs that support it, and increasingly on GPUs (Ampere and newer GPUs also support BF16 arithmetic). The choice between FP16 and BF16 can depend on hardware: BF16’s advantage is easier software conversion (you can often take an FP32-trained model and cast weights to BF16 with minimal loss, since the dynamic range is intact), whereas FP16 might require adjusting the model or ensuring values fit in range (sometimes needing techniques like maintaining a master copy of weights in FP32 during training, or handling out-of-range values).&lt;br /&gt;
&lt;br /&gt;
For ONNX, both Float16 and BFloat16 types are part of the standard. ONNX models can be converted to FP16 (truncating weights and inserting cast nodes for computations) using tools like &amp;lt;code&amp;gt;float16_converter&amp;lt;/code&amp;gt; in ONNX Runtime&amp;lt;ref name=&amp;quot;ref8&amp;quot;/&amp;gt;. ONNX Runtime on GPUs will typically execute those FP16 ops with mixed precision on tensor cores (FP16 input, FP32 accumulate), whereas on CPU it may emulate FP16 or upcast to FP32 (as many CPU implementations do not natively support half-precision math)&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;. In fact, as an example, running an ONNX model in FP16 on a CPU might insert hidden Cast operations to FP32 behind the scenes, because the CPU kernel for that op only exists in FP32&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;. This ensures correctness (the computation is done in higher precision) but can reduce performance. We will revisit such backend-dependent behavior in Section 5.3.&lt;br /&gt;
&lt;br /&gt;
In summary, &#039;&#039;&#039;FP16 and BF16&#039;&#039;&#039; are now established as important low-precision formats in deep learning. FP16 offers more precision bits, which can matter for e.g. small differences, but has a limited range (care needed to avoid overflow). BF16 offers safety in range but less precision (which can slightly affect convergence or final accuracy in some cases). Both typically rely on FP32 for accumulation to achieve acceptable accuracy. When deploying models, one should choose a format supported efficiently by the target hardware and verify that the accuracy is not significantly degraded. Often, CNNs and transformers can run in FP16/BF16 with virtually no drop in score, but certain operations (like softmax, see Section 6.3) might need special handling.&lt;br /&gt;
&lt;br /&gt;
=== 4.3. FP8 Formats (E4M3, E5M2) and Scaling ===&lt;br /&gt;
&lt;br /&gt;
The push for ever lower precision has led to experimental &#039;&#039;&#039;8-bit floating point (FP8)&#039;&#039;&#039; formats for deep learning. In 2022, researchers from NVIDIA, Intel, and Arm proposed an FP8 standard with two variants: &#039;&#039;&#039;E4M3&#039;&#039;&#039; (4 exponent bits, 3 fraction bits) and &#039;&#039;&#039;E5M2&#039;&#039;&#039; (5 exponent bits, 2 fraction bits)&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;. Both have 1 sign bit, for a total of 8 bits. E5M2 has a wider exponent range (5 bits gives bias 15) but one fewer mantissa bit than E4M3. The idea is to use E4M3 for numbers that need more precision (like weights or activations around 1.0) and E5M2 for numbers that need more dynamic range (like gradient updates which can be very small or large). In fact, the study found that using E4M3 for forward activations/weights and E5M2 for backward gradients can allow training of large networks with FP8 to accuracy on par with FP16 training&amp;lt;ref name=&amp;quot;ref11&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
FP8 formats are &#039;&#039;not&#039;&#039; IEEE 754 standard (though IEEE is considering lower precision in its 2019 revision). Notably, the E4M3 format as used by NVIDIA does not allocate any bit-pattern for $+\infty$ or $-\infty$ – it has only NaN for invalid results, using that freed bit pattern to extend the representable range by one exponent value&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;. Essentially, E4M3 can represent numbers up to ~$2^{15}$ (around 3e4) instead of capping at infinity at $2^{16}$, by foregoing infinities&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;. Variations also exist regarding negative zero and how NaNs are handled&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;. For instance, GraphCore’s IPU uses FP8 formats that disallow negative zero and infinities, to maximize useable range&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Using 8-bit floats for neural networks requires additional techniques, chiefly &#039;&#039;&#039;scaling&#039;&#039;&#039;. Because 8-bit has very limited precision and range, one typically applies a power-of-two scale factor to groups of values to normalize them into an FP8-friendly range. This is akin to block floating-point or “microscaling” per layer or per tensor. For example, one might keep a FP32 scale for each channel or tensor, and represent values as $(FP8\_value) \times 2^{scale}$. The ONNX standard has introduced support for FP8 in version 1.15, including types for E4M3 and E5M2 (with variations FNUZ: finite-only and no negative zero)&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;, and even a type called &#039;&#039;&#039;E8M0&#039;&#039;&#039; which is an 8-bit exponent-only number used purely as a scale factor&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;. The E8M0 type has 8 exponent bits, no mantissa, effectively representing powers of two from $2^{-127}$ to $2^{128}$ for scaling purposes&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;. This indicates an approach where an FP8 tensor would be accompanied by an E8M0 scale (or scales) to interpret its value correctly.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Rounding&#039;&#039;&#039; becomes critical when casting values to FP8. The quantization error is much larger relative to FP16 or FP32. Studies have shown that choosing the right rounding mode (e.g., round-to-nearest, stochastic rounding, etc.) and clamping behavior (saturating to max instead of inf) can affect model accuracy&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;. ONNX added a &amp;lt;code&amp;gt;round_mode&amp;lt;/code&amp;gt; attribute to its Cast operator to allow controlling this for FP8&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;. In particular, one paper found that &#039;&#039;&#039;rounding up with saturation&#039;&#039;&#039; (round-to-nearest ties away from 0, and clamp overflows to max finite rather than inf) gave better accuracy in large language model training than other rounding modes&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
At present, FP8 is on the cutting edge. NVIDIA’s H100 GPU supports FP8 tensor cores (for both E4M3 and E5M2), and early results show minimal accuracy loss on some vision and NLP tasks when properly tuned. However, using FP8 requires fine-tuning hyperparameters like scaling factors for each layer and perhaps tweaking training procedures. ONNX’s inclusion of FP8 types means you can represent and transport models using FP8 weights or activations, but actual hardware support is limited and typically paired with calibration or dynamic scaling logic outside of ONNX graph (or using custom ONNX functions for scaling). It remains an &#039;&#039;&#039;open research area&#039;&#039;&#039; how to fully automate FP8 quantization for arbitrary models without accuracy loss.&lt;br /&gt;
&lt;br /&gt;
In summary, FP8 formats promise further compression of neural network computation and data. They are an order of magnitude more error-prone than FP16, but when combined with careful scaling, mixed usage of E4M3/E5M2, and maybe modified training techniques, they have shown surprising success. ONNX is actively evolving to support these ultra-low precision types, signaling their potential importance in future AI inference deployments.&lt;br /&gt;
&lt;br /&gt;
== Accumulation Semantics in ONNX ==&lt;br /&gt;
&lt;br /&gt;
One of the roles of the ONNX specification is to define the mathematical behavior of operators (Conv, MatMul, Sum, etc.) in a framework-agnostic way. However, ONNX mostly defines &#039;&#039;what&#039;&#039; an operator computes in exact arithmetic, not &#039;&#039;how&#039;&#039; the computation is carried out in finite precision. This leaves certain details – like accumulator precision or summation order – up to the implementation (runtime or hardware). We&#039;ll examine how ONNX treats accumulation and what guarantees (or lack thereof) are provided.&lt;br /&gt;
&lt;br /&gt;
=== 5.1. Operator Definitions and Numerical Guarantees ===&lt;br /&gt;
&lt;br /&gt;
Each ONNX operator is described as performing an ideal mathematical function on its inputs. For example, &amp;lt;code&amp;gt;MatMul&amp;lt;/code&amp;gt; is defined as the standard matrix product of two tensors, and &amp;lt;code&amp;gt;ReduceSum&amp;lt;/code&amp;gt; computes a summation along given axes. The ONNX spec, as of current versions, &#039;&#039;&#039;does not mandate the precision of intermediate calculations&#039;&#039;&#039; beyond the types of the inputs and outputs. If an operator’s inputs and outputs are float16, the spec implies the computation is done in float16 in a sense, but it does not forbid an implementation from using higher precision internally – it simply doesn&#039;t mention it. There is &#039;&#039;&#039;no explicit guarantee of identical bit-for-bit results&#039;&#039;&#039; across different ONNX backends for floating-point ops. Instead, results are expected to be numerically close, within tolerances one would normally expect from floating-point differences (due to different hardware or algorithms). The ONNX Backend Test suite typically uses a relative or absolute error tolerance when validating outputs for this reason.&lt;br /&gt;
&lt;br /&gt;
Because of this, important details like the use of extended precision or accumulation in higher precision are left ambiguous. This has led to discussions in the ONNX community about making such behavior more explicit. For example, a proposal was raised to allow specification of accumulation precision for certain ops like MatMul&amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;. The reasoning is that as lower-precision types (float16, bfloat16, float8) become more common, knowing or controlling whether an op internally accumulates in a higher precision is important for both accuracy and reproducibility&amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;. In June 2025, an issue on the ONNX GitHub suggested adding an attribute to MatMul (and similarly Gemm, Softmax, etc.) to specify the accumulator type&amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;. By default it would use input precision (status quo), but an attribute could request (for instance) 32-bit accumulation even if inputs are 16-bit&amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;. As of this writing, however, no such attribute exists in the official spec – it&#039;s an open issue. Thus, the numerical semantics are effectively: &#039;&#039;“compute the result as if in the same type as inputs, but actual implementations may use more precision internally as an optimization.”&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
From a guarantee standpoint, ONNX operators promise the mathematically correct result assuming real-number arithmetic, but they &#039;&#039;&#039;do not guarantee bit-exact results&#039;&#039;&#039; due to floating-point effects. There is an understanding that differences can arise from different summation orders, fused operations (e.g., an implementation might use an FMA (fused multiply-add) which has one rounding instead of two), or use of extended precision registers. ONNX does not formally define rounding modes either – it relies on IEEE 754 defaults, but if a backend used something like deterministic summation or alternative rounding, ONNX has no mechanism to express that.&lt;br /&gt;
&lt;br /&gt;
In summary, ONNX&#039;s current stance is largely &#039;&#039;&#039;implementation-dependent&#039;&#039;&#039; for numerical precision. It provides minimal explicit numerical guarantees beyond type constraints. This is why an ONNX model run on two different hardware or runtime libraries can have slightly different results, especially in reduced precision. The burden is on backends to produce results &amp;quot;close enough&amp;quot; to each other that they don&#039;t affect the model&#039;s decisions in any significant way.&lt;br /&gt;
&lt;br /&gt;
=== 5.2. MatMul, Conv, and Reduction Operators ===&lt;br /&gt;
&lt;br /&gt;
Consider ONNX &amp;lt;code&amp;gt;MatMul&amp;lt;/code&amp;gt;: it takes two tensors (often two matrices) and produces their matrix product. In pure math, $C_{ij} = \sum_k A_{ik} &#039;&#039; B_{kj}$. If A and B are float16, should that sum be computed in float16 or float32? As discussed, ONNX doesn’t specify – it simply says the output is a float16 matrix containing the product. In practice, many backends will &#039;&#039;promote&#039;&#039; the computation: for example, NVIDIA GPUs will do the multiplication and addition in a mixed FP16/FP32 mode by default (because of tensor cores), then convert the final result back to float16. ONNX Runtime on CPU might not support float16 multiplication natively, so it will insert cast ops to float32, perform the MatMul in float32, then cast back to float16&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;. From ONNX’s perspective, as long as the final result is within acceptable error of the “ideal” float16 computation, it’s fine. But note, an “ideal” float16 computation (where every multiply and add is rounded to FP16) might actually be &#039;&#039;less accurate* than what the backend did with FP32. This means that a backend using higher precision can produce a result that is not bit-equal to a pure FP16 implementation – it&#039;s actually closer to the true real number result. This is usually considered a good thing (improved accuracy), but it does highlight a lack of consistency: another backend might do everything in pure FP16 and get a slightly different answer.&lt;br /&gt;
&lt;br /&gt;
The same situation occurs for &#039;&#039;&#039;convolution&#039;&#039;&#039; (&amp;lt;code&amp;gt;Conv&amp;lt;/code&amp;gt; op) which is essentially a batched sum of products (like MatMul in sliding window), and for explicit reduction ops like &amp;lt;code&amp;gt;ReduceSum&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;ReduceMean&amp;lt;/code&amp;gt;, etc. If you have an ONNX &amp;lt;code&amp;gt;ReduceSum&amp;lt;/code&amp;gt; that sums 1000 float16 values, the spec doesn’t say how to sum them. A GPU kernel might sum in a tree using FP32 accumulators; a naive implementation might sum in order with FP16 and possibly overflow or underflow intermediate steps. Both are allowed as long as they output a float16 result that is “reasonable.” In practice, ONNX Runtime and others take care to avoid obviously bad behavior (like overflow) by using higher precision or by splitting the sum. But again, it’s not mandated by the ONNX standard explicitly.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Softmax&#039;&#039;&#039; is an interesting case: Softmax involves an internal sum of exponentials. ONNX defines the Softmax operator as $$\mathrm{Softmax}(x)_i = \frac{\exp(x_i)}{\sum_j \exp(x_j)},$$ typically applied along a specified axis. Directly computing this in float16 can be problematic for large vectors, as discussed in Section 6.3. Indeed, the issue of accumulation precision arises: should the sum $\sum_j \exp(x_j)$ be done in FP16 or can it be in FP32? Many implementations will do it in FP32 even if $x$ is FP16, because the dynamic range of $\exp(x)$ is large. The ONNX Softmax spec doesn’t detail this, but it expects the result to be a proper probability distribution (summing to 1.0 within normal floating error). A pure FP16 softmax on, say, 1000 elements could sum to something like 0.996 or 1.004 due to rounding, whereas FP32 accumulation would be much closer to 1.000. ONNX likely allows both; the small difference is usually inconsequential, but if it were large (say the FP16 sum overflowed to inf), that would be a correctness issue. A robust backend will prevent that (by using the common max-subtraction trick and higher precision accumulation). The ONNX issue 7072 indeed calls out Softmax as another op where explicit control of accumulation precision would be useful&amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
In summary, &#039;&#039;&#039;MatMul, Conv, and reduction ops&#039;&#039;&#039; in ONNX are defined mathematically but with &#039;&#039;implicit&#039;&#039; numerical behavior. Implementations commonly use higher precision for intermediate sums especially when input is low precision, but this is not standardized. Consequently, results can differ slightly between implementations. For critical applications, one must be aware of these differences. A model exported to ONNX with float16 weights might perform slightly differently on one runtime vs another if one does all float16 math and the other uses float32 accumulations. Both conform to ONNX. As a user, if you require a certain precision for accumulation, currently you have to ensure it by the way you export or run the model (for example, insert manual cast nodes in the ONNX graph to force certain ops to FP32). We may see future ONNX versions address this with attributes or metadata as mentioned, but for now these ops are effectively “compute with best effort accuracy”.&lt;br /&gt;
&lt;br /&gt;
=== 5.3. Backend and Hardware-Dependent Behavior ===&lt;br /&gt;
&lt;br /&gt;
Because ONNX is implemented by many backends (ONNX Runtime, TensorRT, CoreML, PyTorch JIT, etc.), and these in turn run on varied hardware (CPU, GPU, accelerators), the numeric behavior can be backend-dependent. Some examples:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;CPU backends:&#039;&#039;&#039; Many CPUs do not have native float16 vector arithmetic (with a few exceptions in recent CPUs). ONNX Runtime’s default CPU execution provider will often upcast float16 to float32, compute, then downcast&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;. This ensures accuracy but means that if you thought you were getting a full float16 pipeline, you’re not – the CPU is essentially ignoring the low precision in compute. On the other hand, x86 CPUs have extended 80-bit precision in x87 registers (though less used now) and 64-bit SIMD registers for doubles; some compiler optimizations might carry intermediate results in higher precision. This could make CPU results slightly different from a GPU’s if, say, a sum is done in 80-bit on CPU versus 32-bit on GPU. Generally, modern compilers avoid such extended precision unless explicitly asked, to improve reproducibility.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;GPU backends:&#039;&#039;&#039; GPUs often perform summations in parallel using tree reductions. The order of operations can change depending on how threads are scheduled. This means even on the same GPU, summing an array in one launch versus another might not give bit-identical results if the internal parallel reduction algorithm differs (though it should be deterministically the same given the same library version and input, but if you change GPU model or BLAS library, it might differ). Also, GPUs have fused multiply-add (FMA) instructions that do $(a*b + c)$ in one step with one rounding, which can produce slightly different results than doing the multiply and add separately (which would round twice). If an ONNX backend uses a fused kernel (like a GEMM library) it might yield different low-order bits than a simple loop.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Vendor libraries:&#039;&#039;&#039; ONNX Runtime can use vendor BLAS or DNN libraries (MKL, cuDNN, TensorRT, etc.). These libraries often implement their own strategies for numerical stability. For example, NVIDIA’s cuDNN might use FP32 accumulation for FP16 inputs by default. TensorRT (NVIDIA’s inference engine) will sometimes automatically promote precisions or insert scaling to maintain stability&amp;lt;ref name=&amp;quot;ref13&amp;quot;/&amp;gt;. On the other hand, if you explicitly request FP16 in TensorRT with no FP32 fallback, it might do the entire op in FP16 and you’d see more error. Some libraries offer knobs: TensorRT allows choosing FP32, FP16, or INT8 precision for each layer, and will use FP32 internally for safety in some cases unless forced otherwise.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Hardware-specific quirks:&#039;&#039;&#039; As another example, NVIDIA’s &#039;&#039;&#039;TensorFloat-32 (TF32)&#039;&#039;&#039; on Ampere GPUs is a precision mode where matrix multiplications use 10-bit mantissa (so about FP16 precision for the multiply) but keep 8-bit exponent (range like FP32). By default, CUDA libraries use TF32 for FP32 matrix multiplies on Ampere to speed them up, unless you disable it. This means if you run an ONNX model with FP32 on an A100 GPU, your matmuls might actually be lower precision than FP32 (though higher than FP16)&amp;lt;ref name=&amp;quot;ref6&amp;quot;/&amp;gt;. It’s an example of hardware choosing an intermediate precision for performance. From ONNX’s perspective, the output is still FP32 and (hopefully) within error tolerances, but a user might be surprised that it’s not a “true” FP32 computation internally. If exactness is needed, one has to turn off TF32.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Non-determinism:&#039;&#039;&#039; Some backends (especially on GPUs) allow non-deterministic execution for performance. For instance, atomic adds in parallel reductions can produce results that vary run-to-run because the order of accumulation is race-condition-dependent. ONNX’s spec doesn’t cover this explicitly, but it’s something to consider. In ONNX Runtime, there are settings for deterministic computing, but by default it may use all available performance optimizations, even if results vary at the 1e-7 level.&lt;br /&gt;
&lt;br /&gt;
In short, the numerical behavior under ONNX can depend on the combination of backend software and hardware capabilities. The lack of a strict spec for accumulation means implementations have freedom to use clever strategies (higher precision, reordering) to improve accuracy or speed. Usually this is positive (e.g. getting better accuracy than a naive approach). But it could also lead to slight inconsistencies. For example, you might find that exporting a PyTorch model to ONNX and running it with one engine yields outputs slightly different from PyTorch’s – not because ONNX is wrong, but because the summation or convolution was done differently (maybe PyTorch summed in FP32 as well, but maybe not in exactly the same way).&lt;br /&gt;
&lt;br /&gt;
The key takeaway is that ONNX &#039;&#039;by design&#039;&#039; abstracts away these details, which is good for portability but means you need to &#039;&#039;&#039;trust the backend&#039;&#039;&#039; or verify its precision behavior for sensitive models. If necessary, you can enforce certain behavior by inserting ops (for example, if you want to ensure FP32 accumulation, you could insert a cast to FP32 for inputs of a MatMul and cast the result back to FP16 – this way the ONNX graph itself mandates FP32 compute in between). This of course sacrifices some performance or device-specific magic.&lt;br /&gt;
&lt;br /&gt;
Looking forward, as mentioned, there are proposals to enrich ONNX’s specification regarding precision. This might include explicit flags for “exact summation” or “accumulate in int32” etc., which would make models more self-descriptive but also potentially less portable (if a backend doesn’t support that, what then?). For now, users should be aware of these backend differences. Most of the time, well-engineered ONNX runtimes yield results close enough that they don&#039;t affect the application (e.g., classification top-1 remains the same even if low-level sums differ by 1e-5). But for high-stakes numerical computations, testing and maybe adjusting the model or backend settings is prudent.&lt;br /&gt;
&lt;br /&gt;
== Numerical Stability in Deep Learning Models ==&lt;br /&gt;
&lt;br /&gt;
Deep learning computations involve many operations that, while theoretically well-defined, can be sensitive to numerical issues when implemented in finite precision. We&#039;ll discuss a few common scenarios in neural networks where numerical stability is a concern, and how precision and accumulation play a role.&lt;br /&gt;
&lt;br /&gt;
=== 6.1. Dot Products and Large-Scale Reductions ===&lt;br /&gt;
&lt;br /&gt;
Neural networks are full of dot products: every fully-connected layer computes $\mathbf{y} = W\mathbf{x}$, which is a series of inner products between weight vectors and the input. Convolutional layers compute weighted sums of local regions. Attention mechanisms compute large matrix products of query/key/value matrices. All these boil down to summing a lot of terms: $\sum_{k=1}^K a_k b_k$. When $K$ is large (it could be in the thousands or more for a single neuron in a fully connected layer of a large model), the potential for floating-point error in that sum increases.&lt;br /&gt;
&lt;br /&gt;
If using FP32, the error may still be negligible in many cases, but in lower precision like FP16, it can become significant. For example, consider a dot product of length 2048 in FP16. Each product is in FP16 (with ~3 decimal digits precision) and if accumulated in FP16, the worst-case error could be on the order of $2048 \times \varepsilon_{FP16} \approx 2048 \times 0.0009766 \approx 2$ (i.e., you could lose a couple units in the third decimal place relative accuracy in worst case). Even in random error accumulation, you might see error on the order of $\sqrt{2048} \times 0.0009766 \approx 0.044$ or 4.4% (though this is a rough estimate). That’s quite significant. Fortunately, as discussed, most hardware will accumulate such sums in a higher precision like FP32, dramatically reducing that error (~7 orders of magnitude smaller $\varepsilon$). But if it didn’t, the network’s outputs could get noisy. A fully-connected layer with many inputs might output slightly different results depending on summation order if done in low precision.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Large-scale reductions&#039;&#039;&#039; also occur in operations like global average pooling (summing all elements of a feature map) or computing the total loss by summing loss over batch samples. If one sums 100,000 values in FP32, the worst-case error could be on the order of 100,000 &#039;&#039; 1e-7 ≈ 1e-2 (1% uncertainty), though typical error would be much less. In FP16, summing 100k values is likely impossible without overflow: as noted, adding ~10^4 moderate numbers can overflow FP16&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. So large reductions &#039;&#039;must* be handled carefully (either in chunks or in higher precision). Frameworks generally do: for example, when performing a sum reduction on GPU, libraries break it into parallel blocks that accumulate in registers (usually 32-bit registers, even if inputs are 16-bit). &lt;br /&gt;
&lt;br /&gt;
Another aspect is that many dot products in neural nets involve values of varying magnitude. If the distribution of terms is such that some are much larger than others, the smaller terms might not contribute much if summed last. Ideally, one might sort by magnitude or use pairwise summation to improve that. Most BLAS libraries effectively do a pairwise strategy due to their parallel structure. So, while worst-case analyses are grim, practical implementations mitigate a lot of it.&lt;br /&gt;
&lt;br /&gt;
However, certain layers can suffer if not handled properly. One famous example is in RNNs or LSTMs: if one uses FP16 naive summation for the gating mechanism, errors could accumulate over many time steps and cause drift or instability. In modern practice, recurrent layers are often run in FP16 with FP32 accumulators to avoid this.&lt;br /&gt;
&lt;br /&gt;
In summary, &#039;&#039;&#039;dot products and large reductions need sufficient precision&#039;&#039;&#039; or robust algorithms. If you are implementing a custom kernel (say a custom ONNX op) and you have to sum a huge array, you should consider using a technique from Section 3 (pairwise summation, Kahan, or at least double accumulator if possible). Many deep learning computations, by virtue of hardware and library design, already do something like this under the hood. But it&#039;s always good to be aware, especially if pushing into lower precision territories.&lt;br /&gt;
&lt;br /&gt;
=== 6.2. Cancellation, Dynamic Range, and Conditioning ===&lt;br /&gt;
&lt;br /&gt;
We introduced &#039;&#039;cancellation&#039;&#039; earlier in the context of summation. In deep learning, cancellation can appear in a few places. One is in the computation of certain norms or variances. For instance, Batch Normalization computes mean and variance of activations. The variance formula $\mathrm{Var}(x) = E[x^2] - (E[x])^2$ is subject to catastrophic cancellation if $E[x]$ and $\sqrt{E[x^2]}$ are close in magnitude (which can happen if the variance is small compared to the mean). If implemented naively in single precision, you might get a negative variance due to numerical error (which would then be clamped to zero or sqrt would yield NaN). Robust implementations compute variance in a single pass or increase precision. Indeed, in many frameworks, batchnorm accumulation is done in FP32 even if the activations are FP16, precisely to avoid numerical issues in computing mean and variance. ONNX itself doesn’t specify this, but an ONNX backend is likely to do batchnorm stats in higher precision for safety. The &#039;&#039;conditioning&#039;&#039; of the variance calculation is poor when variance is tiny relative to mean, so it&#039;s a spot where higher precision is needed.&lt;br /&gt;
&lt;br /&gt;
Another example is in certain loss functions or regularizations. If you subtract two large numbers to get a small difference, you lose precision. Say you have two nearly identical predictions and you subtract them as part of some operation – if done in float16, you might get zero whereas float32 would have a slight difference. This could affect, say, a gradient if that difference was supposed to propagate information. Fortunately, neural nets often avoid directly subtracting large nearly equal numbers; but it can happen. In attention mechanisms, the &#039;&#039;score normalization&#039;&#039; (softmax) subtracts a max to avoid huge exponentials, which is good for range but if multiple scores are close to the max, you subtract numbers that are close. However, those subtractions are not catastrophic in the same way; they&#039;re intentional to reduce range and are typically fine in FP32 or even FP16.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Dynamic range&#039;&#039;&#039; issues are everywhere in deep learning. Activation functions like ReLU and exponential can produce very large or very small values. If subsequent computations don’t account for that, you get infinities or zeros. For example, consider a network output that produces a logit of 100 in FP16. $\exp(100)$ in FP16 overflows to infinity (since FP16 max ~ 6e4 and $e^{100} \sim 3.7e43$). If you then do softmax, that one infinity will dominate the sum and you might get a probability of 1 for that class (or NaNs if not handled). The standard softmax implementation subtracts the max logit, so in this case it would subtract 100 from all logits, making the largest exponent $\exp(0)=1$ and others tiny – this avoids overflow. But what about underflow? If you had a very negative logit, $\exp(-100)$ in FP16 would underflow to 0. In softmax, that just contributes ~0 probability, which is fine (it effectively is zero anyway). Underflow tends to be less dangerous than overflow in inference — underflow usually just means “this probability/neuron is effectively 0,” whereas overflow can poison the results with NaNs or Infs. &lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Conditioning&#039;&#039;&#039; refers to how sensitive a function is to input perturbations. Neural networks themselves as functions can be poorly conditioned (e.g., tiny changes in weights cause big changes in output for some architectures), but typically they are designed to be reasonably well-behaved. Certain internal computations like matrix inversion (if one does a pseudoinverse or something for a layer) can be extremely ill-conditioned if the matrix is near singular. That again would need double precision perhaps. In ML, one common ill-conditioned operation is the &#039;&#039;&#039;softmax denominator&#039;&#039;&#039;: $\sum_j e^{x_j}$ can be very large and also very sensitive to changes in the largest $x_j$. However, the softmax is more stable if computed carefully (max trick). Another is &#039;&#039;&#039;normalization&#039;&#039;&#039;: computing $\frac{x - \mu}{\sigma}$ for layer norm or batch norm – if $\sigma$ is very small, then the division blows up noise. In those cases, a small $\epsilon$ is usually added to $\sigma^2$ to prevent instability (this is a model-level fix, not a numeric one per se, but it acknowledges numeric limits).&lt;br /&gt;
&lt;br /&gt;
In training, &#039;&#039;&#039;optimizers&#039;&#039;&#039; can suffer from precision issues. For example, the Adam optimizer keeps a moving average of squared gradients; in half precision, these small updates might flush to zero or not change at some point. Some research found that certain adaptive optimizers break in FP16 because the small differences are lost (&amp;quot;Why FP16 training breaks NAdam&amp;quot; etc.). The solution is often to keep the optimizer states in FP32 while doing the bulk of compute in FP16. This again is a form of mixed precision.&lt;br /&gt;
&lt;br /&gt;
From an inference perspective, once the model is trained, the main concerns are avoiding overflow in activations and ensuring summations (like in softmax or reductions) are done with enough precision. The model’s weights are fixed, so we don&#039;t have to worry about cumulative error over iterations as in training, just the forward pass.&lt;br /&gt;
&lt;br /&gt;
=== 6.3. Normalization Layers and Softmax ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Normalization layers&#039;&#039;&#039; (BatchNorm, LayerNorm, etc.) and &#039;&#039;&#039;Softmax&#039;&#039;&#039; are specifically worth examining for numerical stability:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Batch Normalization:&#039;&#039;&#039; During inference, batchnorm applies an affine transform using precomputed mean and variance (from training). It’s basically: $y = \frac{x - \mu}{\sqrt{\sigma^2 + \epsilon}} \gamma + \beta$. This is a stable operation as long as $\epsilon$ is provided (to avoid division by zero). Since $\mu, \sigma^2$ are constants in inference, it reduces to linear ops which are fine. The main numerical work (computing mean/variance) happened during training or model calibration, and frameworks usually did that in FP32. So batchnorm at inference time is not a source of instability (it’s just a scale and shift of the data).&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Layer Normalization / Group Normalization:&#039;&#039;&#039; These compute mean/var on the fly for each sample (and possibly channel group). In inference, they still need to compute these statistics for each forward pass. If implemented in low precision, the variance calculation can suffer cancellation as mentioned. However, it&#039;s likely that inference engines compute the mean and variance in FP32 even if inputs are FP16 (especially since these are per-sample operations, not huge batch reductions – it&#039;s feasible to do in higher precision). If someone implemented layer norm purely in FP16, they might see issues when the variance is very small. Usually there’s an $\epsilon$ added which might dominate a tiny variance and thus mitigate catastrophic cancellation (the result might just output something mostly from $\epsilon$). In summary, normalization layers are usually fine as long as the implementation uses enough precision for the summation of squares.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Softmax:&#039;&#039;&#039; Softmax is notorious if done improperly. The safe algorithm: subtract the maximum input from all inputs (to bring the largest exponent to 1.0), exponentiate, sum them up, divide each exponent by the sum. The max subtraction handles overflow in $\exp()$. But what about the sum? As discussed, if there are $N$ elements in the softmax, the sum is at most $N$ (when all inputs equal the max). In classification, $N$ might be the number of classes. For ImageNet, $N=1000$, so worst sum ~1000 (which is fine in FP16, as 1000 &amp;lt;&amp;lt; 65504). For an extreme case like a language model, $N$ could be 50,000 (vocab size). Sum could be 50,000 in worst case (everyone equal probability). 50,000 is still below 65504, so actually an FP16 sum would &#039;&#039;not&#039;&#039; overflow in that scenario, and the rounding error on a sum of 50k identical terms would be maybe a few ULPs (which at 50k magnitude might be ~0.5 difference worst-case). So maybe softmax summation in FP16 is not catastrophic for output probabilities – you’d get essentially 1.0 after normalization instead of 0.99999 in some case. Usually fine.&lt;br /&gt;
&lt;br /&gt;
:The bigger issue is if one or a few terms dominate the softmax. If one term is much larger, the others underflow to 0 after the max subtraction. That’s fine: you get a one-hot distribution essentially. If the model expected a softer distribution, that could be a problem. But if the logits were really that far apart, even FP32 would give nearly one-hot. So not really a precision issue, more of a model property.&lt;br /&gt;
&lt;br /&gt;
:However, the &#039;&#039;&#039;stability during training&#039;&#039;&#039; is a big issue. The GitHub thread we saw (ColossalAI) mentioned training a 130B model where FP16 softmax in attention caused training to occasionally blow up&amp;lt;ref name=&amp;quot;ref14&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref14&amp;quot;/&amp;gt;. They observed attention scores in some heads ranging up to 1e4 or as low as 1e-3&amp;lt;ref name=&amp;quot;ref14&amp;quot;/&amp;gt;. 1e4 exponent in FP16 is definitely Inf (because $\exp(10^4)$ is astronomically huge, but even exp(100) is inf in half). The method CogView introduced (Precision Bottleneck Relaxation) involved subtracting the max &#039;&#039;per head&#039;&#039; to reduce range&amp;lt;ref name=&amp;quot;ref14&amp;quot;/&amp;gt;—which is basically the softmax max trick at a finer granularity or something similar. In the end, they found &#039;&#039;&#039;the simplest fix was to compute softmax in FP32&#039;&#039;&#039; for those layers&amp;lt;ref name=&amp;quot;ref14&amp;quot;/&amp;gt;. They reported that it had negligible speed impact but significantly improved stability&amp;lt;ref name=&amp;quot;ref14&amp;quot;/&amp;gt;. This highlights a general principle: certain operations like softmax (and sometimes matrix multiplications in attention) benefit from higher precision, and using FP32 in a few critical spots can save a mixed-precision training from crashing. In inference, using FP32 for softmax is also a safeguard if one is uncertain. It costs little because softmax is not a heavy compute compared to matrix multiplies.&lt;br /&gt;
&lt;br /&gt;
:ONNX does have a Softmax operator; if the input is float16, a backend might do exactly what they described: internally cast to float32 for the softmax computation, then cast output back to float16. Some inference engines (like DeepSpeed’s inference or others) keep attention softmax in FP32 even if rest is FP16, for robustness&amp;lt;ref name=&amp;quot;ref14&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Apart from softmax, other places where exponentials occur are in loss functions like cross-entropy (which uses log-softmax internally), or in activation functions like Sigmoid and Tanh (which are basically scaled logistic functions). Sigmoid($x)=1/(1+e^{-x})$. If $x$ is moderately large positive, $e^{-x}$ underflows to 0 in FP16 if $x&amp;gt;~11$. That gives a sigmoid output of 1.0 exactly. In FP32, $e^{-x}$ for $x=11$ is about $5e-5$, not underflow, so sigmoid ~0.99995. So FP16 will saturate some outputs to exactly 0 or 1 sooner than FP32 would. This can slightly change behaviors (e.g., a neuron might go to full saturation a bit early). Usually, this isn’t a huge deal, but it could make gradients zero earlier. In inference, we don’t care about gradients, just the output value. Getting exactly 1.0 vs 0.99995 probably has no practical difference for a classification output. But one should be aware that &#039;&#039;&#039;activation functions can saturate faster&#039;&#039;&#039; in lower precision due to limited range or precision. ReLU has no issue (it’s linear or 0), but exponential-based activations do. In practice, again, frameworks often simply compute those in the given precision and accept the slight difference. If it were a problem, one could do those in FP32 as well.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Summary:&#039;&#039;&#039; Numerical stability in inference is maintained by using robust computation methods: subtracting maxima in softmax/LogSumExp, adding epsilon in denominators for normalization, and using higher precision for summations or critical steps. Most of these are built into the algorithms. ONNX models assume these standard practices (they don&#039;t need to explicitly do them in the graph because the backend inherently will for ops like Softmax). It&#039;s wise when building or exporting models to ONNX to test them in the target precision and ensure there are no surprises (like an output becomes NaN or Inf due to precision). Usually if training was done in mixed precision, the model will have been vetted in that environment already.&lt;br /&gt;
&lt;br /&gt;
== Performance vs. Accuracy Trade-offs ==&lt;br /&gt;
&lt;br /&gt;
There is an inherent trade-off between numerical precision and computational performance (speed, memory usage) in AI systems. Choosing lower precision can dramatically increase throughput and reduce memory, but it may introduce arithmetic errors. Engineers must balance these considerations:&lt;br /&gt;
&lt;br /&gt;
=== 7.1. Throughput, Memory Bandwidth, and Precision ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Throughput (speed)&#039;&#039;&#039; is often higher for lower-precision operations. Modern hardware can execute more low-precision ops in parallel than high-precision ops. For example, an NVIDIA V100 GPU can perform 2× more half-precision FLOPs than single-precision per clock, and newer architectures with Tensor Cores can do 8× more (since a Tensor Core warp operation might perform 64 FP16 FMAs per cycle vs 16 FP32). Similarly, int8 operations can be even faster and more energy efficient. Lower precision numbers also use less memory bandwidth – fetching 16-bit data is twice as fast as 32-bit data from memory (assuming the same bus width), which can relieve memory bottlenecks. Many deep learning workloads are memory-bound, so halving the data size can nearly double throughput if compute is not the limiting factor. This is a huge incentive to use lower precision.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Memory and storage&#039;&#039;&#039;: Using FP16 or int8 weights reduces model size, which is important for deploying large models to memory-limited environments (like edge devices or GPUs with limited VRAM). It also means you can fit larger batch sizes in memory, or more models on a chip, etc. There is also a cascade effect: smaller data means better cache utilization, which further improves performance.&lt;br /&gt;
&lt;br /&gt;
However, &#039;&#039;&#039;accuracy&#039;&#039;&#039; suffers if precision is too low. We have discussed how rounding error and limited range can cause deviations in outputs. The question is: do those deviations matter for the application? Often for neural networks, a small amount of noise or error in intermediate calculations does not change the final prediction. Neural nets are somewhat error-tolerant due to their redundant distributed representations. This is why quantization and FP16 techniques can work at all. The final model accuracy (e.g. top-1 accuracy on ImageNet) might drop only 0.5% or less when using FP16 instead of FP32, or int8 instead of FP16, if done properly. For many use-cases, this loss is acceptable given the speed gains.&lt;br /&gt;
&lt;br /&gt;
If one goes too low (like naive FP8 or int4 without special care), accuracy might drop severely – perhaps making the model unusable. Thus the trade-off: as precision decreases, performance increases but at some point accuracy degrades beyond an acceptable threshold. Engineers aim to find the lowest precision that still preserves accuracy.&lt;br /&gt;
&lt;br /&gt;
There is also the consideration of &#039;&#039;&#039;accumulation strategy&#039;&#039;&#039; affecting performance: for instance, Kahan summation (compensated) is more accurate but ~4× slower than naive summation. If you need that accuracy, you pay a performance cost. In many AI inference scenarios, the slight extra accuracy from something like Kahan isn&#039;t needed, so it’s not used. But in a scenario where you sum millions of elements for a final result that is thresholded, maybe you do need it.&lt;br /&gt;
&lt;br /&gt;
Another dimension is &#039;&#039;&#039;determinism vs speed&#039;&#039;&#039;. Enforcing a deterministic reduction order (for reproducibility) might require disabling some parallelism, which could slow things down. If exact repeatability is critical (like certain scientific or financial applications of neural nets), one might sacrifice some performance to ensure the summation happens the same way every time. ONNX allows both modes depending on the runtime.&lt;br /&gt;
&lt;br /&gt;
From a memory perspective, if precision is higher than needed, you waste memory. For example, storing intermediate activations in FP32 when they could be FP16 uses 2× memory, which might mean lower batch size or more memory traffic. So there&#039;s strong motivation to keep things as low-precision as possible &#039;&#039;except&#039;&#039; where accuracy truly demands.&lt;br /&gt;
&lt;br /&gt;
In summary, the trade-off often manifests as &#039;&#039;diminishing returns&#039;&#039;: FP64 vs FP32 – huge slowdown for usually negligible accuracy gain in ML; FP32 vs FP16 – moderate speedup for usually minor accuracy hit; FP16 vs int8 – another big speedup but maybe more accuracy loss unless calibration is done; int8 vs int4 – speed gain but often big accuracy drop that might not be worth it for current models.&lt;br /&gt;
&lt;br /&gt;
Engineers use techniques like &#039;&#039;&#039;mixed precision&#039;&#039;&#039; to target high precision only where needed. For example, run most of the model in FP16, but keep a few sensitive layers in FP32. This yields most of the performance benefits while ensuring critical parts are accurate. We already saw softmax as one example. Another might be final output layer: sometimes using FP32 for the logits and softmax can be safer.&lt;br /&gt;
&lt;br /&gt;
Hardware is also trending to support mixed modes – like FP8 Tensor Cores accumulate in FP32, etc. So hardware tries to give accuracy cheaply.&lt;br /&gt;
&lt;br /&gt;
=== 7.2. Hardware Accelerators and Tensor Cores ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Hardware accelerators&#039;&#039;&#039; (GPUs, TPUs, FPGAs, ASICs like GraphCore IPU, etc.) have specific support for certain precisions. Leveraging these can provide massive speedups:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;NVIDIA Tensor Cores:&#039;&#039;&#039; These started in Volta (V100) with support for 4x4 matrix multiply-adds on FP16 inputs with FP32 accumulation&amp;lt;ref name=&amp;quot;ref6&amp;quot;/&amp;gt;. Using Tensor Cores can give up to 8x throughput compared to using regular FP32 cores. But to use them, the data needs to be FP16 (or now BF16 or FP8 on newer GPUs) and aligned to certain dimensions (multiples of 8 etc. in matrix sizes, which frameworks handle by padding if needed). If an ONNX model is all FP32, by default it would not use Tensor Cores unless the runtime automatically converts some ops to mixed precision (which some do via a flag or environment variable enabling TF32 or FP16). The introduction of &#039;&#039;&#039;TensorFloat-32 (TF32)&#039;&#039;&#039; in Ampere GPUs is essentially a compromise: treat FP32 inputs as two 16-bit significand halves (10-bit mantissa actually) so that it can run on Tensor Cores, giving ~8x speed, while still outputting FP32. TF32 has about the precision of FP16 but range of FP32, and it was chosen because it typically doesn’t hurt convnet accuracy. ONNX Runtime or others on Ampere will use TF32 by default for conv and matmul unless disabled, giving a free speed boost at minimal accuracy cost. If exact FP32 is needed, users have to turn it off (sacrificing speed).&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;TPU (Google):&#039;&#039;&#039; TPUs have native support for BF16 matrix ops with FP32 accumulators. They were designed specifically for the mixed precision approach in training. For inference, TPUs also support int8 and other quantized ops (e.g., via Edge TPU for lower-power). If exporting ONNX to run on a TPU (via some bridge), one might get automatic BF16 usage. BF16 has the advantage of no loss in range, so TPUs can more straightforwardly run models in BF16 without modifications.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;FPGAs and ASICs:&#039;&#039;&#039; Some custom accelerators might use fixed-point or block floating-point arithmetic for efficiency. They often treat the network in quantized terms. The ONNX quantization specification (QuantizeLinear/DequantizeLinear ops) exists for representing quantized models (INT8 etc.), which is another domain beyond floating-point, but it overlaps as a precision issue. In those systems, accumulators for INT8 × INT8 multiplies are usually 32-bit (to avoid overflow for dot products up to certain lengths). The trade-off there is between integer vs float representation, which can give big speedups on CPUs/DSAs via SIMD instructions (many CPUs have vector int8 multiply-add instructions now for inference).&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Memory bandwidth on hardware:&#039;&#039;&#039; Some accelerators include special high-bandwidth memory or cache specifically for lower precision. For example, an accelerator might fetch two FP16 values in one 32-bit memory transaction – effectively doubling memory bandwidth for half precision. On some hardware, using FP16 can even reduce memory &#039;&#039;latency&#039;&#039; if multiple values can be packed.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Parallelism and Vectorization:&#039;&#039;&#039; Many hardware units process data in fixed vector widths (e.g., 256-bit registers). That could be 8 floats at once or 16 half-floats at once in the same 256 bits. If your code is in FP16, you might use the full vector width (16 values) whereas FP32 might only use half (8 values). This means FP16 can make better use of vector units. Similarly, matrix units often have fixed tile sizes, and smaller data types allow larger tiles or more tiles concurrently.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Energy efficiency:&#039;&#039;&#039; Lower precision often means less energy per operation, which on large deployments (data centers or battery-powered devices) is a significant advantage. This isn&#039;t directly performance, but it&#039;s an important trade: you might choose a slightly lower precision to save power even if accuracy drops a tiny bit.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Software overhead:&#039;&#039;&#039; Using mixed precision on hardware involves some overhead of casting and moving data between precisions. For instance, if you have to frequently cast FP16 to FP32 and back, that adds instructions and memory copies. A well-designed accelerator (like GPUs with unified memory for FP16/32 or special hardware casting) minimize this. Still, the runtime might insert extra operations (as we saw, ONNX Runtime CPU inserted many Cast nodes converting FP16 to FP32 and back&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;, which actually &#039;&#039;slowed down&#039;&#039; that FP16 model by 4x vs FP32 on a CPU, because the CPU had no native FP16). So ironically, on some hardware, using lower precision in ONNX might degrade performance if not supported (the CPU case). Always ensure the target hardware actually supports the low precision you plan to use; otherwise, you&#039;ll pay overhead for emulation.&lt;br /&gt;
&lt;br /&gt;
To illustrate trade-off: Suppose you have a model that achieves 99% accuracy in FP32 and drops to 98.5% in FP16. If FP16 runs 2x faster, many would accept the 0.5% accuracy drop for double throughput. If int8 runs 4x faster but accuracy drops to 95%, that might be too much drop for some applications, but acceptable for others that value speed. It&#039;s application-dependent.&lt;br /&gt;
&lt;br /&gt;
In critical applications, one might maintain higher precision accumulators or run certain validation steps in high precision to ensure nothing drifted. In research, sometimes a model is trained in mixed precision but validated in double to ensure any small numeric differences don’t influence the reported result (rare, but for scientific computing integration maybe).&lt;br /&gt;
&lt;br /&gt;
In the ONNX world, performance-vs-accuracy is often managed via &#039;&#039;&#039;Execution Providers&#039;&#039;&#039; (EPs) that target specific hardware. For example, the TensorRT EP in ONNX Runtime can take an ONNX model and run it using NVIDIA&#039;s high-performance inference library, which will use FP16 or INT8 if allowed, to maximize speed. The user can configure the EP with precision flags. If the accuracy drop is too high, one might restrict it to FP32. If it&#039;s fine, allow FP16. Tools exist to measure the accuracy difference (like calibrating int8 quantization).&lt;br /&gt;
&lt;br /&gt;
To conclude, the trade-offs revolve around picking the lowest precision that yields acceptable accuracy, and utilizing hardware features fully. ONNX being a high-level spec abstracts this, but it&#039;s ultimately up to the deployment to decide and sometimes up to the exporter to incorporate quantization. ONNX’s role is to be able to &#039;&#039;represent&#039;&#039; these choices (they have added quantized types, FP8 types, etc., to allow the graph to include those when needed).&lt;br /&gt;
&lt;br /&gt;
== Best Practices and Design Guidelines ==&lt;br /&gt;
&lt;br /&gt;
Given the above insights, here are some best practices and guidelines for dealing with numerical precision and accumulators in AI inference (and training) design:&lt;br /&gt;
&lt;br /&gt;
=== 8.1. Choosing an Accumulator Precision ===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Match precision to data length and range:&#039;&#039;&#039; If an operation accumulates a large number of terms, consider using a higher precision accumulator than the input data. For example, for summing &amp;gt;1000 values of type FP16, an FP32 accumulator is recommended to avoid large rounding error or overflow. If summing &amp;gt; million values of FP32, consider double (FP64) accumulation if high accuracy is needed (though this is rare in DL). &#039;&#039;Rule of thumb:&#039;&#039; if $N \cdot \varepsilon$ is not tiny (say &amp;gt;1e-3), you may benefit from higher precision. For FP16 ($\varepsilon \approx 1e-3$), that threshold N is a few hundred. For FP32 ($\varepsilon \approx 1e-7$), N would be in the millions before 1e-3 error.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Use higher precision in critical layers:&#039;&#039;&#039; Identify if certain layers of the model are more sensitive to precision. For instance, the output layer or a layer that feeds into a very sharp decision boundary might require more precision. If a small change there could flip the prediction, you don&#039;t want excessive numerical error. Using FP32 for logits and softmax is a common practice to maintain classification confidence stability.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Follow hardware defaults, but override if needed:&#039;&#039;&#039; Many hardware accelerators have sensible defaults (like accumulate in FP32 for tensor ops). It&#039;s usually best to use those defaults as they were chosen to balance accuracy and speed. Only override if you have evidence of a problem. For example, if using ONNX Runtime on CPU, you discovered it upcasts to float for you – great. But if using some custom accelerator that does pure FP16, you might manually insert casts in the ONNX graph around certain ops to force higher precision on those ops.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Maintain precision for gradient accumulation (training):&#039;&#039;&#039; This is more training-oriented, but worth noting: when summing gradients over batches or updating weights, use higher precision (FP32 or even 64) for those accumulations. This ensures training doesn&#039;t diverge due to numeric error. Many frameworks keep a &amp;quot;master copy&amp;quot; of weights in FP32 when training in FP16&amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;. For inference, this translates to: any running statistics or counters in the model (like if you had a running average inside the model) should probably be in higher precision to avoid drift.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Be mindful of intermediate cast costs:&#039;&#039;&#039; If you do decide to cast to higher precision for an operation in ONNX, remember it incurs memory copies and compute overhead. Make sure it&#039;s worth it. For example, if you cast a huge tensor to double, that could slow things drastically. Usually FP16-&amp;gt;FP32 is fine (hardware handles it well), but FP32-&amp;gt;FP64 might be expensive on GPUs.&lt;br /&gt;
&lt;br /&gt;
=== 8.2. Safe Reduction Lengths and Heuristics ===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Chunk long reductions:&#039;&#039;&#039; If you must do a very long sum in low precision (due to memory constraints or hardware), chunk the sum into parts, sum each to a higher precision local accumulator, then sum the partial sums. This is essentially pairwise summation. Many libraries use a fixed chunk size heuristic (e.g., sum in blocks of 256 in registers, then add those up). This bounds the error growth. If implementing manually, you can choose a chunk size such that chunk_length &#039;&#039; ε is small (like 256 &#039;&#039; 1e-3 = 0.256 for FP16, hmm maybe choose 100).&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Sort by magnitude if possible:&#039;&#039;&#039; In some cases (not typical in DL, but possible in post-processing), if you have numbers varying by orders of magnitude, summing from smallest to largest improves accuracy. It prevents small ones from being swallowed by big ones. This is more relevant if you output a sum of mixed-scale values (like some algorithm mixing big and small contributions). It’s not always feasible in real-time inference to sort, but keep in mind.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Use compensation for critical sums:&#039;&#039;&#039; If you find a particular sum or difference in your inference computation is crucial (like computing a small residual from two close numbers), consider using Kahan summation or another compensated scheme just for that part. For example, if you had an ONNX custom function that needs to sum a lot of almost cancelling terms, you might implement it with compensation internally.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Heuristics for FP8/low-bit:&#039;&#039;&#039; When using extremely low precision like FP8, adopt the recommended practices: use scaling (don&#039;t feed raw data into FP8, normalize it first), and perhaps only use FP8 on portions of the network that are empirically robust. The industry is still establishing heuristics, but one might be: use FP8 for matrix multiplications, but accumulate in FP16 or FP32; keep normalization in FP16; use FP16 for first and last layer maybe. This is evolving, so keep an eye on literature.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Keep an eye on sum length in softmax and normalization:&#039;&#039;&#039; If you have extremely large class counts or group sizes (e.g., a softmax over 1e6 items, or layer norm over tens of thousands of features), realize this is a giant reduction. You may want to ensure that reduction is done in higher precision. ONNX Softmax by default will likely not handle a million-class case in FP16 well (the sum would overflow since 1e6 &amp;gt; 65504). So either do that on CPU in FP32 or break it down (could do hierarchical softmax, etc.). This is a corner case, but relevant for huge vocab language models. In practice, vocab ~50k is fine as we said.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Test numeric differences:&#039;&#039;&#039; A practical tip: after quantizing or reducing precision, run some test inputs through both the high-precision model and low-precision model. Compare outputs (and maybe some intermediate signals if possible). If differences are consistently small relative to the application tolerance, you&#039;re good. If you see occasional large differences or instabilities (like outputs become NaN or Inf in low precision), that&#039;s a red flag. Use that testing to guide where you might need to increase precision or change algorithms.&lt;br /&gt;
&lt;br /&gt;
=== 8.3. When Higher Precision Is Required ===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Ill-conditioned calculations:&#039;&#039;&#039; If your model or pipeline performs any operation known to be ill-conditioned (matrix inversion, subtraction of nearly equal quantities, computing a tiny difference of large numbers), you likely need higher precision for that part. For example, if your ONNX model includes a custom op to solve a linear system, use double precision in that op if possible (or at least float with iterative refinement). In ML, this might occur in something like a physics-informed neural network or a differentiable simulation within the model.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Strict accuracy requirements:&#039;&#039;&#039; Some applications (medical, aerospace, finance) might have strict tolerances. If an inference model’s output needs to be correct to 5 decimal places, you probably cannot use FP16, which has only 3-4 decimal digits precision. FP32 might be barely enough (~7 digits). FP64 might be warranted to guarantee that level of accuracy in worst case. Most deep learning applications (like image classification) don&#039;t demand that level of numeric precision in outputs – as long as the top class is correct, it doesn&#039;t matter if its probability was 0.901 vs 0.899. But if you&#039;re doing something like summing probabilities to make a risk estimate or doing a cumulative sum over many predictions, be careful to accumulate precisely.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Large sums or products in post-processing:&#039;&#039;&#039; Sometimes the neural net is fine, but the &#039;&#039;post-processing&#039;&#039; of results might need precision. E.g., summing up probabilities of many events, or computing confidence intervals, etc. Do those calculations in double if needed outside the model.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Preventing drift in long sequences:&#039;&#039;&#039; For sequential models (RNNs processing thousands of steps), rounding error can accumulate over steps. If each step has a small error, after 10000 steps it could add up. In such cases, using a higher precision for the state might reduce drift. Some practitioners use FP64 for the hidden state accumulation in very long sequence processing to maintain numerical stability (though this is not common due to speed loss). Alternatively, periodically renormalize or reset small errors.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Simulations or differential equations:&#039;&#039;&#039; If your ONNX model involves simulating something (like a differential equation solver inside a model, or an ODE/RNN hybrid), those often need double precision to be stable for long durations. Recognize those situations and allocate precision accordingly.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Verification and debugging:&#039;&#039;&#039; Use higher precision as a tool to verify correctness. For example, run your model in FP64 (if possible) and compare to FP32 to see if there are differences. If not, FP32 is fine. If yes, you might find a particular calculation losing too much info in FP32, implying maybe you should keep it in FP64 in production as well. This is rare in inference but can be part of rigorous validation.&lt;br /&gt;
&lt;br /&gt;
In ONNX, using higher precision may involve casting to &amp;lt;code&amp;gt;tensor(double)&amp;lt;/code&amp;gt; for certain ops. Not all runtimes support double on GPU though (some do via emulation or just slower path). ONNX CPU does support double for general ops. So be mindful of the target.&lt;br /&gt;
&lt;br /&gt;
Finally, &#039;&#039;&#039;document your precision choices&#039;&#039;&#039;. If you deliver an ONNX model that uses mixed types (float16 inputs, float32 accumulations, int8 weights, etc.), ensure that is clear to whoever maintains or uses it. Precision issues can be sneaky, so clarity helps.&lt;br /&gt;
&lt;br /&gt;
== Limitations and Open Problems ==&lt;br /&gt;
&lt;br /&gt;
Despite advances in handling numerical precision, there remain limitations and open challenges in this domain:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Lack of Precision Specification in ONNX:&#039;&#039;&#039; As noted, ONNX currently has no way to formally specify that an op &#039;&#039;should&#039;&#039; use a higher precision internally. This can lead to nondeterministic differences across platforms&amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;. While proposals exist to add attributes for this&amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;, it is not yet standard. Thus an ONNX model might behave slightly differently on two compliant backends, and there&#039;s no built-in mechanism to ensure identical results. This is a limitation for applications requiring cross-platform consistency. Solving it may involve extending ONNX (e.g., an attribute like &amp;lt;code&amp;gt;accumulatePrecision=&amp;quot;FLOAT32&amp;quot;&amp;lt;/code&amp;gt; for MatMul) or tightening the spec, but those come with backward compatibility and implementation burden concerns.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Reproducibility and Determinism:&#039;&#039;&#039; Even with the same precision, parallel computation can introduce nondeterminism (due to different summation orders). Ensuring bit-identical results across runs (important for some sensitive domains) is non-trivial. Frameworks often provide &amp;quot;deterministic mode&amp;quot; which might use fixed ordering at cost of speed. In ONNX, there&#039;s no global flag for this; it&#039;s up to the runtime. More research could be done on algorithms that produce bit-wise identical results independent of parallel execution order (perhaps using techniques like pairwise summation or consistent partitioning). Stochastic rounding is another area: sometimes using probabilistic rounding can reduce bias in accumulated error, but it introduces randomness – reconciling that with reproducibility is tricky.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Ultra-low Precision (FP8 and below):&#039;&#039;&#039; While FP8 is being explored, going to 4-bit floats or mixed schemes is an open challenge. The OpenAI community is investigating 4-bit (quantization-wise) for LLMs with some success, but a true 4-bit float (like E3M1) would be extremely limited. Perhaps hybrid schemes (like block floating point, log scales, etc.) will come. ONNX already has some support (they mention 4-bit in documentation placeholders&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;), but it&#039;s nascent. Research into training techniques, calibration, and new number formats (like &#039;&#039;&#039;posit numbers&#039;&#039;&#039; or tapering precision floats) is ongoing. If a new format proves useful, ONNX will need to adapt to represent it.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Quantization Noise and Model Robustness:&#039;&#039;&#039; Reducing precision can sometimes expose models to issues like adversarial vulnerability or fragility – e.g., a slight perturbation might flip a decision if the margin was small and precision loss nudged it. Ensuring model robustness under quantization is an open problem. It might require re-training the model with quantization in the loop (quantization-aware training) or designing models that have large &amp;quot;margin&amp;quot; between classes to tolerate quantization noise.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Dynamic range during training vs inference:&#039;&#039;&#039; In training, activations can have wider distributions (especially early in training). Models often learn to constrain their ranges (via activation functions saturating or normalization), which makes inference more stable in lower precision. But if one tries to train in FP8 from scratch, it&#039;s very hard. This gap between what&#039;s possible in a post-trained inference model vs training is an open area – how to train directly in low precision without significant accuracy loss. Solving that could simplify workflows (no need for high-precision training then quantize; just train low-prec end to end). Techniques like loss scaling, gradient clipping, etc., are partial solutions. The development of optimizers that are resilient to low precision is ongoing.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Hardware variability:&#039;&#039;&#039; As new hardware emerges (with new numeric capabilities or quirks), ensuring that ONNX models run consistently is an ongoing task. For instance, one device might implement a fused op that reduces rounding error, another does it separately. Even something like convolution might use a different algorithm (FFT-based vs direct) which has different numeric properties. Open problem: could ONNX allow specifying numeric tolerances per op or preferred algorithms? That may be overkill for most, but for some HPC ML use cases, maybe desired.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Testing and validation of numerics:&#039;&#039;&#039; There isn&#039;t a standardized set of &amp;quot;numeric compliance&amp;quot; tests for ONNX runtimes beyond simple correctness. Perhaps in the future, test suites might include challenging numeric scenarios (large sums, nearly singular matrices) to ensure a runtime handles them gracefully (maybe by switching precision). Right now, it&#039;s mostly on the user to test their model under expected conditions.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Combining quantization with floating point:&#039;&#039;&#039; Many production models use a mix (e.g., int8 for some layers, FP16 for others). ONNX can represent this (with QuantizeLinear ops or MixedPrecision through casting), but tooling to automatically find the optimal mix is still developing. It&#039;s somewhat trial-and-error or using heuristics. An open question is how to automate precision assignment: given a model and a target hardware, how to automatically choose which layers can be int8, which should be FP16, etc., to meet an accuracy target. This crosses into NAS (neural architecture search) territory or automated quantization. &lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Numerical debugging tools:&#039;&#039;&#039; When a model goes wrong due to precision, it&#039;s often hard to pinpoint where. Better tooling to analyze numerical error layer by layer could be useful. For instance, one could run a model in double and float and diff the intermediate results to see where the largest divergence happens. Doing this systematically for big nets is an open tooling challenge. Some frameworks have hooks for this, but not standardized.&lt;br /&gt;
&lt;br /&gt;
In conclusion, while we&#039;ve come far with using lower precision in AI without sacrificing much accuracy, the landscape is always evolving. The key open problem is how to push precision lower (for efficiency gains) without crossing the threshold where accuracy falls off a cliff. Each new generation of hardware and techniques (like FP8, novel quantization schemes) extends that boundary, but also brings new questions on how to manage the numerical errors. ONNX as an interchange will need to keep pace, possibly by adding more expressive power for these numeric considerations.&lt;br /&gt;
&lt;br /&gt;
== See Also ==&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;IEEE 754 Floating-Point&#039;&#039;&#039; – The standard defining binary16, binary32, binary64 formats and rounding behavior.&lt;br /&gt;
* &#039;&#039;&#039;Floating-Point Arithmetic&#039;&#039;&#039; – General topic on how floating-point math works, common issues (overflow, underflow, cancellation).&lt;br /&gt;
* &#039;&#039;&#039;Kahan Summation Algorithm&#039;&#039;&#039; – Algorithm for compensated summation to reduce numerical error&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;.&lt;br /&gt;
* &#039;&#039;&#039;BFloat16&#039;&#039;&#039; – A short-format float with 8-bit exponent, used in TPUs and some GPUs for training networks.&lt;br /&gt;
* &#039;&#039;&#039;Mixed Precision Training&#039;&#039;&#039; – Technique of using FP16 (or BF16) along with FP32 accumulators to accelerate training&amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;.&lt;br /&gt;
* &#039;&#039;&#039;Quantization in Deep Learning&#039;&#039;&#039; – Converting networks to lower bit integer arithmetic for inference (INT8 quantization and calibration techniques).&lt;br /&gt;
* &#039;&#039;&#039;ONNX Operator Specification&#039;&#039;&#039; – Official docs for ONNX ops, which describe their mathematical function but not the required numeric precision (a point of potential extension in future).&lt;br /&gt;
* &#039;&#039;&#039;CUDA Tensor Cores &amp;amp; TF32&#039;&#039;&#039; – NVIDIA documentation on how tensor cores perform mixed precision and the TF32 format for accelerating FP32.&lt;br /&gt;
* &#039;&#039;&#039;Pairwise Summation&#039;&#039;&#039; – Summation algorithm with logN error growth&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;, used in many numerical libraries to improve accuracy of reductions.&lt;br /&gt;
* &#039;&#039;&#039;Softmax Stability&#039;&#039;&#039; – Articles on numerically stable softmax and log-sum-exp implementations to avoid overflow.&lt;br /&gt;
* &#039;&#039;&#039;Performance Tuning for Low Precision&#039;&#039;&#039; – Guides (e.g., NVIDIA or Intel whitepapers) on using low precision and the trade-offs involved in various hardware.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;references&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref1&amp;quot;&amp;gt;Accelerate and simplify Scikit-learn model inference with ONNX Runtime - Microsoft Open Source Blog — opensource.microsoft.com — https://opensource.microsoft.com/blog/2020/12/17/accelerate-simplify-scikit-learn-model-inference-onnx-runtime&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref2&amp;quot;&amp;gt;Floating Point Precision: Understanding FP64, FP32, and FP16 in Large Language Models - DEV Community — dev.to — https://dev.to/lukehinds/floating-point-precision-understanding-fp64-fp32-and-fp16-in-large-language-models-3gk6&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref3&amp;quot;&amp;gt;Floating Point Precision and Its Limitations | by Umair Akbar | Medium — akbu.medium.com — https://akbu.medium.com/floating-point-precision-and-its-limitations-cfb7247d7789&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref4&amp;quot;&amp;gt;Kahan summation algorithm - Wikipedia — en.wikipedia.org — https://en.wikipedia.org/wiki/Kahan_summation_algorithm&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref5&amp;quot;&amp;gt;Pairwise summation - Wikipedia — en.wikipedia.org — https://en.wikipedia.org/wiki/Pairwise_summation&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref6&amp;quot;&amp;gt;Numerical behavior of NVIDIA tensor cores - PMC — pmc.ncbi.nlm.nih.gov — https://pmc.ncbi.nlm.nih.gov/articles/PMC7959640/&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref7&amp;quot;&amp;gt;Mixed precision and performance-accuracy tuning (neuron-cc) — AWS Neuron Documentation — awsdocs-neuron.readthedocs-hosted.com — https://awsdocs-neuron.readthedocs-hosted.com/en/latest/about-neuron/appnotes/neuron-cc/mixed-precision.html&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref8&amp;quot;&amp;gt;Create Float16 and Mixed Precision Models - ONNX Runtime — onnxruntime.ai — https://onnxruntime.ai/docs/performance/model-optimizations/float16.html&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref9&amp;quot;&amp;gt;[Performance] ONNX FP16 model is having performance bottle neck when compared to FP32 variant · Issue #25824 · microsoft/onnxruntime · GitHub — github.com — https://github.com/microsoft/onnxruntime/issues/25824&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref10&amp;quot;&amp;gt;Float stored in 8 bits - ONNX 1.21.0 documentation — onnx.ai — https://onnx.ai/onnx/technical/float8.html&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref11&amp;quot;&amp;gt;[2209.05433] FP8 Formats for Deep Learning — arxiv.org — https://arxiv.org/abs/2209.05433&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref12&amp;quot;&amp;gt;Should MatMul op allow specification of accumulation precision? · Issue #7072 · onnx/onnx · GitHub — github.com — https://github.com/onnx/onnx/issues/7072&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref13&amp;quot;&amp;gt;Quantize ONNX models | onnxruntime — onnxruntime.ai — https://onnxruntime.ai/docs/performance/model-optimizations/quantization.html&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref14&amp;quot;&amp;gt;Keep Attention Softmax FP32 during FP16/ZeRO Training · Issue #1485 · hpcaitech/ColossalAI · GitHub — github.com — https://github.com/hpcaitech/ColossalAI/issues/1485&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;/references&amp;gt;&lt;/div&gt;</summary>
		<author><name>Timo.stripf</name></author>
	</entry>
	<entry>
		<id>https://www.emmtrix.com/w139/index.php?title=Numerical_Precision_in_ONNX_and_AI_Inference&amp;diff=2901</id>
		<title>Numerical Precision in ONNX and AI Inference</title>
		<link rel="alternate" type="text/html" href="https://www.emmtrix.com/w139/index.php?title=Numerical_Precision_in_ONNX_and_AI_Inference&amp;diff=2901"/>
		<updated>2026-01-30T15:24:23Z</updated>

		<summary type="html">&lt;p&gt;Timo.stripf: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&#039;&#039;&#039;Open Neural Network Exchange (ONNX)&#039;&#039;&#039; is an open standard format for representing machine learning models and neural network computations across different frameworks and hardware&amp;lt;ref name=&amp;quot;ref1&amp;quot;/&amp;gt;. As models are exported and deployed via ONNX, the &#039;&#039;&#039;numerical precision&#039;&#039;&#039; of computations becomes critical. Deep learning inference involves a variety of floating-point operations, and small numerical discrepancies can accumulate and affect model accuracy or reproducibility. This article provides an in-depth overview of floating-point precision issues in numerical computing, with a focus on how they manifest in AI inference and the ONNX ecosystem. We cover floating-point formats and rounding behavior, accumulation strategies for summations, the use of low-precision formats (FP16, BFloat16, FP8) in modern AI, and how ONNX defines (or leaves unspecified) the precision of accumulators in operations. Strategies for maintaining numerical stability in deep neural networks are discussed, along with the trade-offs between computational performance and arithmetic accuracy. The goal is to give engineers and researchers a comprehensive, encyclopedic reference on numerical precision in ONNX-based AI inference systems.&lt;br /&gt;
&lt;br /&gt;
== Floating-Point Precision in Numerical Computing ==&lt;br /&gt;
&lt;br /&gt;
=== 2.1. IEEE 754 Floating-Point Formats ===&lt;br /&gt;
&lt;br /&gt;
Modern computers represent real numbers using the IEEE 754 floating-point standard, which defines formats like 32-bit &#039;&#039;single precision&#039;&#039; (FP32) and 64-bit &#039;&#039;double precision&#039;&#039; (FP64). In IEEE 754 binary formats, a floating-point number is composed of three fields: 1 sign bit, several exponent bits (with a bias), and several fraction (mantissa) bits. For example, FP32 consists of 1 sign bit, 8 exponent bits, and 23 fraction bits (24 bits of significand precision including the implicit leading 1)&amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;. This provides roughly 7 decimal digits of precision and an exponent range allowing values on the order of $10^{\pm38}$. FP64 allocates 1 sign bit, 11 exponent bits, and 52 fraction bits (53-bit precision), yielding about 15–16 decimal digits of precision and a vastly larger range (~$10^{\pm308}$)&amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;. These larger formats (especially FP64) are the &amp;quot;gold standard&amp;quot; for numerical accuracy in scientific computing, as they reduce round-off error in complex calculations&amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;. However, the increased precision comes at the cost of doubled storage and typically reduced computational throughput (e.g. many GPUs execute FP64 instructions at a much lower rate than FP32&amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
In addition to single and double precision, the IEEE 754-2008 standard introduced a 16-bit &#039;&#039;half precision&#039;&#039; format (FP16, or binary16). FP16 has 1 sign bit, 5 exponent bits, and 10 fraction bits (11-bit significand including the implicit 1)&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. This format covers a much smaller dynamic range (approximately $6.1\times10^{-5}$ to $6.5\times10^4$ for normalized values&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;) and offers only around 3 decimal digits of precision. Half precision was originally used in graphics, but it has gained popularity in machine learning for its speed and memory advantages (discussed later in Section 4). Another format relevant to AI is &#039;&#039;&#039;BFloat16&#039;&#039;&#039; (Brain Float 16), a 16-bit float with &#039;&#039;&#039;8 exponent bits and 7 fraction bits&#039;&#039;&#039;&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. BFloat16 sacrifices precision (only ~7 fraction bits ≈ 2–3 decimal digits) in order to have the &#039;&#039;&#039;same exponent range as FP32&#039;&#039;&#039;&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. This means BFloat16 can represent very large or very small numbers (∼$10^{\pm38}$, similar range as FP32) but with much less precision between representable values&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. BFloat16 was introduced for deep learning by Google (for TPUs) to allow reduced precision &#039;&#039;training&#039;&#039; without frequent overflow/underflow, relying on the wide range and accepting the coarse precision.&lt;br /&gt;
&lt;br /&gt;
Each floating-point format has a &#039;&#039;&#039;unit roundoff&#039;&#039;&#039; or machine precision, denoted $\epsilon$. This is effectively the spacing between 1.0 and the next representable number. For FP32, $\epsilon \approx 2^{-23} \approx 1.19\times10^{-7}$, corresponding to about 7–8 decimal digits of precision&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. In FP64, $\epsilon = 2^{-52} \approx 2.22\times10^{-16}$ (15–16 decimal digits). Half precision is much less precise: FP16 has $\epsilon = 2^{-10} \approx 9.76\times10^{-4}$&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;, and BFloat16 has $\epsilon = 2^{-7} = 7.8125\times10^{-3}$&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. These values quantify how finely the continuum of real numbers is quantized in each format.&lt;br /&gt;
&lt;br /&gt;
=== 2.2. Machine Epsilon and Rounding Error ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Machine epsilon ($\varepsilon$)&#039;&#039;&#039; is formally defined as the smallest positive number such that $1.0 + \varepsilon &amp;gt; 1.0$ in the floating-point format&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. It effectively measures the &#039;&#039;&#039;precision&#039;&#039;&#039; of the representation. In FP32, $\varepsilon = 2^{-23} \approx 1.19\times10^{-7}$, meaning any result is rounded to about 7 decimal digits. FP16’s $\varepsilon = 2^{-10} \approx 9.77\times10^{-4}$, over 1000× larger (so only ~3 decimal digits of precision), and BFloat16’s $\varepsilon = 2^{-7} \approx 7.8\times10^{-3}$, reflecting its mere ~8-bit precision&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. Rounding in IEEE 754 by default uses &#039;&#039;&#039;round-to-nearest (ties to even)&#039;&#039;&#039;, which ensures the rounding error for any single operation is at most half a unit in the last place (0.5 ULP). We can model a floating-point addition as: &lt;br /&gt;
&lt;br /&gt;
$$\text{fl}(a+b) = (a+b)\,(1 + \delta),$$ &lt;br /&gt;
&lt;br /&gt;
where $|\delta| \le \varepsilon$ for that format&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. Here $\varepsilon/2$ is the maximum &#039;&#039;&#039;relative error&#039;&#039;&#039; in one rounding (also called unit roundoff). For example, when adding two FP32 numbers, the result is accurate to within about $10^{-7}$ of the true sum (relative), whereas in BF16, the result can deviate by up to $\sim7.8\times10^{-3}$ (nearly 0.78% relative error per operation)&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. Thus, &#039;&#039;&#039;each arithmetic operation incurs a small rounding error&#039;&#039;&#039; (on the order of machine epsilon) that can propagate or accumulate through a sequence of calculations.&lt;br /&gt;
&lt;br /&gt;
Because floating-point representations have finite exponent range, we must also consider &#039;&#039;&#039;overflow&#039;&#039;&#039; and &#039;&#039;&#039;underflow&#039;&#039;&#039;. If a result’s magnitude exceeds the maximum representable value, it overflows to infinity; if it falls below the minimum normal (or minimum subnormal) value, it underflows to zero (or a denormalized tiny value). FP32 and BF16 share an exponent size (8 bits), so their range for normal numbers is roughly $[10^{-38}, 10^{38}]$ (approx $~\!3.4\times10^{38}$ max)&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. FP16’s 5-bit exponent allows max $\sim 6.55\times10^4$ and min normal $\sim 6.10\times10^{-5}$&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. In practice, &#039;&#039;&#039;underflow/overflow&#039;&#039;&#039; can occur in low-precision arithmetic if values are not appropriately scaled. For instance, summing a large number of moderately sized terms in FP16 could overflow where FP32 would not. As an example, summing on the order of $10^4$ values of magnitude ~$10^1$ (say, adding 10,000 terms around 10 each) would produce a total $\sim10^5$, which exceeds FP16’s max finite value ~$6.5\times10^4$ and would overflow to $+\infty$ in half precision&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. Underflow is usually a concern when subtracting nearly equal numbers (catastrophic cancellation yielding a tiny difference that might flush to zero in low precision).&lt;br /&gt;
&lt;br /&gt;
=== 2.3. Accumulation Error in Reductions ===&lt;br /&gt;
&lt;br /&gt;
Floating-point addition is &#039;&#039;&#039;not associative&#039;&#039;&#039;: $(a+b)+c$ can differ from $a+(b+c)$ due to rounding, and thus the order in which we sum a list of numbers can change the result slightly. When summing a large sequence of $N$ numbers $x_1, x_2, ..., x_N$, the &#039;&#039;&#039;accumulated rounding error&#039;&#039;&#039; can be significant. In the &#039;&#039;worst case&#039;&#039; (e.g., adding many positive and negative terms that cause maximal cancellation or always rounding one way), the error can grow on the order of $O(N \varepsilon)$ relative to the exact sum&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;. Simply summing $N$ numbers in sequence (the &#039;&#039;&#039;naive linear summation&#039;&#039;&#039;) has a worst-case error bound proportional to $N\,\varepsilon$&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. This worst-case occurs for adversarial arrangements of values; in more typical scenarios with rounding errors of random signs, one can model the error as a random walk, yielding an expected root-mean-square (RMS) error growth on the order of $O(\sqrt{N}\,\varepsilon)$&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;. Still, with very large $N$, even $\sqrt{N}$ times $\varepsilon$ can become large enough to matter.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Cancellation&#039;&#039;&#039; is a particular problem: if we add two nearly equal numbers of opposite sign, most leading digits cancel and the result loses significant bits of precision. For example, in single precision, subtracting two numbers that agree in the first 6 decimal digits will yield a result with only ~1 digit of accuracy left. Catastrophic cancellation can dramatically increase relative error. Summation is especially sensitive to cancellation if large positive and negative terms are summed naively. The &#039;&#039;&#039;condition number&#039;&#039;&#039; of a summation (the ratio of the sum of absolute values $\sum|x_i|$ to the absolute value of the true sum $|\sum x_i|$) indicates how sensitive the result is to perturbations&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;. If the numbers have mixed signs and nearly cancel out (making the condition number large), even an optimal summation order will have a large relative error. For instance, summing [1.0, $10^{100}$, 1.0, $-10^{100}$] in double precision has an exact sum of 2.0, but naive summation may yield 0.0 due to cancellation; an improved summation algorithm yields the correct 2.0&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
In practical terms, when summing a long list of floating-point numbers (such as accumulating a dot product or reducing a tensor), the &#039;&#039;&#039;accumulation error&#039;&#039;&#039; can become noticeable if no precautions are taken. Techniques to mitigate this error include carefully choosing the summation order or using higher precision for intermediate sums. We will explore these strategies in the next section.&lt;br /&gt;
&lt;br /&gt;
== Accumulators and Reduction Strategies ==&lt;br /&gt;
&lt;br /&gt;
When performing reductions (sums, dot products, etc.), the method of accumulation can greatly affect the accuracy of the result. Below, we discuss various strategies:&lt;br /&gt;
&lt;br /&gt;
=== 3.1. Naive Linear Accumulation ===&lt;br /&gt;
&lt;br /&gt;
The simplest accumulation strategy is to use a single accumulator and add each element sequentially. In C-like pseudocode:&lt;br /&gt;
&lt;br /&gt;
This &#039;&#039;&#039;naive linear summation&#039;&#039;&#039; adds values one by one to the running total. While straightforward and fast, it is prone to the rounding error accumulation described earlier. The partial sum &amp;lt;code&amp;gt;sum&amp;lt;/code&amp;gt; continually grows (in absolute value) as terms are added, so new small terms may fall outside its precision. For example, if &amp;lt;code&amp;gt;sum&amp;lt;/code&amp;gt; is large and positive, adding a comparatively tiny positive number may produce no change because the small term&#039;s significant digits vanish in the presence of the large sum (the increment is below the rounding threshold of &amp;lt;code&amp;gt;sum&amp;lt;/code&amp;gt;). Conversely, adding a small number of opposite sign can suffer from cancellation. The order of addition matters: summing from smallest magnitude to largest (ascending order) tends to yield a more accurate result than summing in descending order, because adding small terms first preserves their contributions before the sum becomes very large. The naive loop above implicitly sums in the given index order; if the data is unsorted, this order is essentially arbitrary.&lt;br /&gt;
&lt;br /&gt;
The performance of linear accumulation is excellent (minimal overhead), and for many applications the resulting error is within acceptable limits. However, for &#039;&#039;&#039;very large N&#039;&#039;&#039; or ill-conditioned sums, the inaccuracy can become a problem. In a reduction involving millions of elements, a single-precision sum could lose several digits of accuracy. In deep learning, this could translate to small discrepancies in layer outputs or aggregated gradients if done naively in low precision.&lt;br /&gt;
&lt;br /&gt;
=== 3.2. Pairwise and Tree-Based Reductions ===&lt;br /&gt;
&lt;br /&gt;
A better approach to summation is &#039;&#039;&#039;pairwise summation&#039;&#039;&#039;, which sums numbers in a binary tree structure rather than linearly. The idea is to recursively split the array into halves, sum each half, then add the two partial sums. This can be implemented as:&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
By always adding numbers of similar magnitudes first (small subsets are reduced to subtotals, then those are added, etc.), pairwise summation avoids the situation of a very large running total absorbing small additions until the final steps. The error of pairwise summation grows logarithmically with $N$ in the worst case (roughly $O(\varepsilon \log_2 N)$)&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;, instead of linearly. Intuitively, at each level of the tree, rounding error is constrained, and there are about $\log_2 N$ levels. In fact, the worst-case relative error bound for pairwise summation (base case of 1 element) is:&lt;br /&gt;
&lt;br /&gt;
\[ |E_N| \le \frac{\varepsilon \log_2 N}{1 - \varepsilon \log_2 N} \sum_{i=1}^N |x_i|, \]&lt;br /&gt;
&lt;br /&gt;
which for practical $N$ simplifies to on the order of $\varepsilon \log_2 N$ times the condition number of the sum&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;. In typical random-error scenarios, the error behaves even better (on the order of $\sqrt{\log N}$)&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;. The key is that &#039;&#039;&#039;pairwise (tree) reduction balances the accumulations&#039;&#039;&#039;, preventing one sum from overwhelming the others too early.&lt;br /&gt;
&lt;br /&gt;
From a performance standpoint, pairwise summation has the same $O(N)$ total additions as linear summation, just a different order. It is also highly amenable to parallelization: independent partial sums can be computed in parallel at each level of the tree. Many numerical libraries and BLAS implementations use a form of blocked or pairwise summation in dot products for improved accuracy at negligible performance cost. For example, NumPy and Julia perform partial pairwise summation by default for better numerical behavior&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=== 3.3. Compensated Summation (Kahan, Neumaier) ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Compensated summation&#039;&#039;&#039; is an alternative technique to improve summation accuracy by tracking the small “lost” quantities. The most famous algorithm is the &#039;&#039;&#039;Kahan summation algorithm&#039;&#039;&#039; (also called Kahan–Babuška)&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. Kahan summation uses an extra variable to accumulate the round-off error. In pseudocode:&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Here &amp;lt;code&amp;gt;c&amp;lt;/code&amp;gt; accumulates the tiny leftovers that would otherwise be dropped&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. Each iteration essentially subtracts the last error from the new addend (&amp;lt;code&amp;gt;y = x[i] - c&amp;lt;/code&amp;gt;) so that &amp;lt;code&amp;gt;t = sum + y&amp;lt;/code&amp;gt; includes the small bits from the previous operation. The new error is then stored in &amp;lt;code&amp;gt;c&amp;lt;/code&amp;gt; for the next iteration. The result is that the final &amp;lt;code&amp;gt;sum&amp;lt;/code&amp;gt; is as if computed with higher precision than the machine’s: Kahan’s algorithm can &#039;&#039;&#039;greatly reduce the error&#039;&#039;&#039; in summing a sequence of floats. In fact, with exact arithmetic for the compensation, Kahan summation yields a result accurate to within 1 or 2 ULP of the true sum, regardless of N (assuming the sequence of partial sums doesn’t overflow)&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. More precisely, Kahan summation achieves a &#039;&#039;&#039;worst-case error independent of N&#039;&#039;&#039; (error bound depends only on machine precision, not the number of terms)&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. In practice, using a double accumulator for summing single-precision numbers via Kahan can give near double-precision accuracy.&lt;br /&gt;
&lt;br /&gt;
An important subtlety is that the compensation variable &amp;lt;code&amp;gt;c&amp;lt;/code&amp;gt; should be kept in &#039;&#039;full precision&#039;&#039;. In the implementation above, we used &amp;lt;code&amp;gt;double&amp;lt;/code&amp;gt; for both sum and c. If summing FP32 values, one might use &amp;lt;code&amp;gt;float sum&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;float c&amp;lt;/code&amp;gt; (both 32-bit), which still improves accuracy, but using a higher precision for the accumulator and compensation (i.e. double) yields even better results. Many implementations of Kahan summation use the same type for sum and c, but it&#039;s assumed to be higher precision than the inputs when possible.&lt;br /&gt;
&lt;br /&gt;
Kahan’s algorithm carries a performance cost: each iteration does five arithmetic operations (two adds, one subtract, etc.) and has a data dependency, which can hinder parallelism or pipelining&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. Thus, it runs slower than naive summation (roughly 2–3× slower or more, depending on CPU pipeline). In loops where summation is a bottleneck, this may be significant. Nonetheless, for modest N or where maximum accuracy is required, compensated summation is extremely useful. In fact, Python’s &amp;lt;code&amp;gt;math.fsum&amp;lt;/code&amp;gt; function uses a variant of Kahan’s algorithm (actually a more advanced multi-part compensation) to sum iterables with high precision.&lt;br /&gt;
&lt;br /&gt;
There are variations and improvements on basic Kahan summation. &#039;&#039;&#039;Neumaier’s algorithm&#039;&#039;&#039; (sometimes called &#039;&#039;Kahan-Babuška-Neumaier&#039;&#039;) improves Kahan’s method by also handling cases where the next input is larger in magnitude than the running sum&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. In such cases, the original Kahan formula could suffer error; Neumaier’s version effectively swaps roles when a huge value comes in. Its pseudocode is:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight&amp;gt;&lt;br /&gt;
xyz&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This algorithm ensures the compensation covers both scenarios: when the sum is the bigger addend or when the new term is bigger&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. In the end, it adds the final &amp;lt;code&amp;gt;compensation&amp;lt;/code&amp;gt; once to the result. Neumaier’s method can produce the correct result in cases where basic Kahan fails. For instance, summing [1.0, $+10^{100}$, 1.0, $-10^{100}$] in double precision: regular Kahan summation might give 0.0 (losing the small terms), whereas Neumaier’s algorithm yields 2.0 exactly&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. There are also higher-order compensation methods (carrying multiple compensation terms, e.g., the &#039;&#039;&#039;Klein summation&#039;&#039;&#039; variant&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt; or using arbitrary precision for intermediate sums) that further reduce error at increased computational cost&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
In summary, compensated summation algorithms trade extra computation for greatly improved numerical accuracy. They are especially valuable when summing large arrays of numbers with varying magnitudes, where naive summation could lose most of the low-order bits. Many scientific computing environments provide compensated sum routines for this reason. In deep learning, however, a different approach to improved precision is often taken: &#039;&#039;&#039;using a wider accumulator type&#039;&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
=== 3.4. Mixed-Precision Accumulation ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Mixed-precision accumulation&#039;&#039;&#039; refers to performing arithmetic in a higher precision than the input data. A common scenario is accumulating sums in 32-bit floats when the operands are 16-bit floats. Rather than implementing a compensation algorithm, one can simply use a wider accumulator so that intermediate rounding error is smaller. This is effectively what modern GPUs do for tensor operations: for example, NVIDIA &#039;&#039;&#039;Tensor Cores operate on FP16 input data with an FP32 accumulator&#039;&#039;&#039; (the 16-bit products are accumulated in 32-bit)&amp;lt;ref name=&amp;quot;ref6&amp;quot;/&amp;gt;. In this way, summing 2048 products in a dot product has far less precision loss than it would with a 16-bit accumulator. Hardware that supports mixed precision can give the best of both worlds: fast low-precision multiplies and high-precision accumulation.&lt;br /&gt;
&lt;br /&gt;
Software can also leverage mixed precision. For instance, one can sum an array of &amp;lt;code&amp;gt;float&amp;lt;/code&amp;gt; values into a &amp;lt;code&amp;gt;double&amp;lt;/code&amp;gt; variable, accumulating the result in double precision and then converting back to float at the end:&lt;br /&gt;
&lt;br /&gt;
This approach often yields a more accurate &amp;lt;code&amp;gt;result&amp;lt;/code&amp;gt; than summing in single precision directly, because the intermediate sum carries 52-bit precision. The final rounding to float happens only once at the end (minimizing overall rounding error). In fact, the above is one way to implement &amp;lt;code&amp;gt;math.fsum&amp;lt;/code&amp;gt; in Python (it internally does something similar with multiple partial sums in double precision).&lt;br /&gt;
&lt;br /&gt;
In machine learning training and inference, &#039;&#039;&#039;mixed precision&#039;&#039;&#039; is widely used. When using FP16 for speed, it&#039;s common to accumulate with FP32. For example, &#039;&#039;&#039;Google&#039;s TPUs use BFloat16 for multiplication but accumulate in FP32&#039;&#039;&#039; for matrix ops&amp;lt;ref name=&amp;quot;ref6&amp;quot;/&amp;gt;. AWS’s Inferentia (Neuron) hardware similarly supports FP16/BF16 matrix multiplication with FP32 accumulation&amp;lt;ref name=&amp;quot;ref7&amp;quot;/&amp;gt;. The motivation is that summing hundreds or thousands of half-precision values would otherwise introduce intolerable error or risk overflow. With an FP32 accumulator, the reduction error is drastically reduced (by about $2^{13}$ times, since FP32 has 13 more precision bits than FP16). Mixed-precision accumulation thus provides a simple and effective form of compensation: &#039;&#039;&#039;use a larger bucket to collect many small contributions&#039;&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
There is a spectrum of options: summing int8 or int16 integers might use 32-bit integers as accumulators for the same reason (to avoid overflow). Some BLAS libraries even offer &#039;&#039;extended precision accumulators&#039;&#039; for single precision dot products (accumulating in double). The downside of mixed precision is the extra memory and maybe throughput cost of the wider type. But on many platforms, the add latency is dominated by hardware design rather than bit-width, so using a 32-bit adder vs 16-bit has negligible speed impact if supported natively. The key is whether data movement or register pressure increases.&lt;br /&gt;
&lt;br /&gt;
In summary, &#039;&#039;&#039;using a higher precision accumulator is often the easiest way to improve numerical accuracy&#039;&#039;&#039; for reductions. It is a primary strategy in deep learning hardware to maintain stability while exploiting low precision for storage and initial computations.&lt;br /&gt;
&lt;br /&gt;
== Precision Formats in AI and Machine Learning ==&lt;br /&gt;
&lt;br /&gt;
Modern AI systems use a variety of numeric formats to balance speed, memory, and accuracy. We discuss common precisions and their roles:&lt;br /&gt;
&lt;br /&gt;
=== 4.1. FP32 and FP64 in Training and Inference ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;FP32 (single precision)&#039;&#039;&#039; has been the workhorse of deep learning for years. Most training scripts in frameworks like TensorFlow and PyTorch used FP32 by default, as it offers sufficient precision to propagate gradients and accumulate weight updates over millions of iterations without excessive error. With ~7 decimal digits of precision and a wide exponent range, FP32 can handle the dynamic range of activations and gradients in typical neural networks. In inference, FP32 is often more precision than strictly necessary for final predictions, but it has remained common because of its ease and the fact that many GPU operations were optimized for FP32.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;FP64 (double precision)&#039;&#039;&#039; is rarely used in deep learning model training or inference, except in specific scenarios requiring extreme precision. FP64 is common in scientific computing and certain types of numerical simulations. In neural networks, however, the extra precision is usually not needed for model accuracy—using doubles tends to yield negligible improvements in model metrics but at a large performance cost (since on GPUs FP64 throughput can be 1/2, 1/8, or even 1/32 of FP32 throughput depending on hardware). One exception might be when integrating neural nets into larger physics simulations or when solving ill-conditioned problems as part of the model. Some researchers use FP64 to diagnose numerical issues or ensure reproducibility. Generally though, deep learning has favored lower precision for efficiency, and FP64 is considered overkill for most networks&amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
That said, certain classical machine learning algorithms benefit from double precision. For example, computing the inverse of a matrix (as in a Gaussian Process regression or Kalman filter embedded in a model) can incur large rounding errors in single precision, so using FP64 can significantly improve the result&amp;lt;ref name=&amp;quot;ref1&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref1&amp;quot;/&amp;gt;. The ONNX standard supports a float64 tensor type for such needs. In ONNX Runtime, however, emphasis has been on FP32 performance, and some pipelines default to float even if the original model was double&amp;lt;ref name=&amp;quot;ref1&amp;quot;/&amp;gt;. If a model’s prediction computation involves operations that are numerically sensitive (like matrix inversion, Cholesky decomposition, or summing extremely large counts), then FP64 may be required to avoid noticeable discrepancies&amp;lt;ref name=&amp;quot;ref1&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref1&amp;quot;/&amp;gt;. A good practice is to test a model in both float and double if you suspect precision issues; significant differences indicate an ill-conditioned calculation that might need higher precision.&lt;br /&gt;
&lt;br /&gt;
In summary, FP32 remains the &#039;&#039;&#039;default&#039;&#039;&#039; format for most training and inference due to its balance of accuracy and efficiency&amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;, while FP64 is reserved for niche cases or precision-critical subroutines. FP32 typically achieves the same accuracy as FP64 for end-to-end deep learning tasks, because network training can often compensate for small noise, and other sources of error (like generalization error) dominate. Therefore, the community trend has been moving to &#039;&#039;&#039;lower&#039;&#039;&#039; precisions, not higher, to gain speed — provided the models still converge and perform well.&lt;br /&gt;
&lt;br /&gt;
=== 4.2. FP16 and BFloat16 in Deep Learning ===&lt;br /&gt;
&lt;br /&gt;
To further speed up neural network training and inference, &#039;&#039;&#039;half-precision (FP16)&#039;&#039;&#039; was adopted, especially with the advent of GPUs supporting FP16 math at twice the rate of FP32. Using FP16 reduces memory storage and bandwidth by 50% compared to FP32, which is significant for large models. However, FP16’s small exponent range and precision posed challenges: training a large network purely in FP16 can fail due to gradient underflow/overflow or weight updates not registering (because changes are below the 0.001 quantization step). To address this, frameworks introduced &#039;&#039;&#039;mixed-precision training&#039;&#039;&#039;, where compute-intensive parts (matrix multiplies, convolutions) run in FP16, but certain accumulations and model parameters remain in FP32. For instance, NVIDIA’s &#039;&#039;&#039;Tensor Core&#039;&#039;&#039; units require FP16 (or BF16) inputs but accumulate results in FP32, effectively ensuring that reductions are done in higher precision&amp;lt;ref name=&amp;quot;ref6&amp;quot;/&amp;gt;. Research showed that with proper loss scaling and mixed precision, models can train to FP32-level accuracy using mostly FP16 arithmetic, achieving significant speedups.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;BFloat16 (BF16)&#039;&#039;&#039; emerged as an alternative 16-bit format primarily from Google’s TPU training systems. BF16 has a larger exponent (8 bits, same as FP32) but only 7 fraction bits&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. This means BF16 can handle the &#039;&#039;&#039;same dynamic range&#039;&#039;&#039; as FP32 (no overflows where FP32 wouldn’t) but at the cost of even lower precision than FP16 (BF16 has about 2–3 decimal digits of precision vs FP16’s ~3–4). Despite its coarse precision, BF16 turns out to be effective for training deep networks because the algorithms (stochastic gradient descent, etc.) are somewhat robust to that noise, and having the wide range prevents catastrophic loss of signal (no gradient will overflow to Inf unless it was going to in FP32 anyway). Many training frameworks (PyTorch, TensorFlow) support BF16 training on hardware like &#039;&#039;&#039;TPU, NVIDIA A100+, Intel CPUs with AVX-512 BF16, etc&#039;&#039;&#039;. Typically, when using BF16, &#039;&#039;&#039;accumulations are still done in FP32&#039;&#039;&#039; (for example, a TPU v2/v3 multiplies BF16 matrices and accumulates into an FP32 result for the dot product&amp;lt;ref name=&amp;quot;ref6&amp;quot;/&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
In inference, both FP16 and BF16 are used to deploy models for faster speed or lower memory. FP16 is popular on NVIDIA GPUs (which have specialized FP16 units). BF16 is advantageous on TPUs and some CPUs that support it, and increasingly on GPUs (Ampere and newer GPUs also support BF16 arithmetic). The choice between FP16 and BF16 can depend on hardware: BF16’s advantage is easier software conversion (you can often take an FP32-trained model and cast weights to BF16 with minimal loss, since the dynamic range is intact), whereas FP16 might require adjusting the model or ensuring values fit in range (sometimes needing techniques like maintaining a master copy of weights in FP32 during training, or handling out-of-range values).&lt;br /&gt;
&lt;br /&gt;
For ONNX, both Float16 and BFloat16 types are part of the standard. ONNX models can be converted to FP16 (truncating weights and inserting cast nodes for computations) using tools like &amp;lt;code&amp;gt;float16_converter&amp;lt;/code&amp;gt; in ONNX Runtime&amp;lt;ref name=&amp;quot;ref8&amp;quot;/&amp;gt;. ONNX Runtime on GPUs will typically execute those FP16 ops with mixed precision on tensor cores (FP16 input, FP32 accumulate), whereas on CPU it may emulate FP16 or upcast to FP32 (as many CPU implementations do not natively support half-precision math)&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;. In fact, as an example, running an ONNX model in FP16 on a CPU might insert hidden Cast operations to FP32 behind the scenes, because the CPU kernel for that op only exists in FP32&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;. This ensures correctness (the computation is done in higher precision) but can reduce performance. We will revisit such backend-dependent behavior in Section 5.3.&lt;br /&gt;
&lt;br /&gt;
In summary, &#039;&#039;&#039;FP16 and BF16&#039;&#039;&#039; are now established as important low-precision formats in deep learning. FP16 offers more precision bits, which can matter for e.g. small differences, but has a limited range (care needed to avoid overflow). BF16 offers safety in range but less precision (which can slightly affect convergence or final accuracy in some cases). Both typically rely on FP32 for accumulation to achieve acceptable accuracy. When deploying models, one should choose a format supported efficiently by the target hardware and verify that the accuracy is not significantly degraded. Often, CNNs and transformers can run in FP16/BF16 with virtually no drop in score, but certain operations (like softmax, see Section 6.3) might need special handling.&lt;br /&gt;
&lt;br /&gt;
=== 4.3. FP8 Formats (E4M3, E5M2) and Scaling ===&lt;br /&gt;
&lt;br /&gt;
The push for ever lower precision has led to experimental &#039;&#039;&#039;8-bit floating point (FP8)&#039;&#039;&#039; formats for deep learning. In 2022, researchers from NVIDIA, Intel, and Arm proposed an FP8 standard with two variants: &#039;&#039;&#039;E4M3&#039;&#039;&#039; (4 exponent bits, 3 fraction bits) and &#039;&#039;&#039;E5M2&#039;&#039;&#039; (5 exponent bits, 2 fraction bits)&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;. Both have 1 sign bit, for a total of 8 bits. E5M2 has a wider exponent range (5 bits gives bias 15) but one fewer mantissa bit than E4M3. The idea is to use E4M3 for numbers that need more precision (like weights or activations around 1.0) and E5M2 for numbers that need more dynamic range (like gradient updates which can be very small or large). In fact, the study found that using E4M3 for forward activations/weights and E5M2 for backward gradients can allow training of large networks with FP8 to accuracy on par with FP16 training&amp;lt;ref name=&amp;quot;ref11&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
FP8 formats are &#039;&#039;not&#039;&#039; IEEE 754 standard (though IEEE is considering lower precision in its 2019 revision). Notably, the E4M3 format as used by NVIDIA does not allocate any bit-pattern for $+\infty$ or $-\infty$ – it has only NaN for invalid results, using that freed bit pattern to extend the representable range by one exponent value&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;. Essentially, E4M3 can represent numbers up to ~$2^{15}$ (around 3e4) instead of capping at infinity at $2^{16}$, by foregoing infinities&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;. Variations also exist regarding negative zero and how NaNs are handled&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;. For instance, GraphCore’s IPU uses FP8 formats that disallow negative zero and infinities, to maximize useable range&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Using 8-bit floats for neural networks requires additional techniques, chiefly &#039;&#039;&#039;scaling&#039;&#039;&#039;. Because 8-bit has very limited precision and range, one typically applies a power-of-two scale factor to groups of values to normalize them into an FP8-friendly range. This is akin to block floating-point or “microscaling” per layer or per tensor. For example, one might keep a FP32 scale for each channel or tensor, and represent values as $(FP8\_value) \times 2^{scale}$. The ONNX standard has introduced support for FP8 in version 1.15, including types for E4M3 and E5M2 (with variations FNUZ: finite-only and no negative zero)&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;, and even a type called &#039;&#039;&#039;E8M0&#039;&#039;&#039; which is an 8-bit exponent-only number used purely as a scale factor&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;. The E8M0 type has 8 exponent bits, no mantissa, effectively representing powers of two from $2^{-127}$ to $2^{128}$ for scaling purposes&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;. This indicates an approach where an FP8 tensor would be accompanied by an E8M0 scale (or scales) to interpret its value correctly.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Rounding&#039;&#039;&#039; becomes critical when casting values to FP8. The quantization error is much larger relative to FP16 or FP32. Studies have shown that choosing the right rounding mode (e.g., round-to-nearest, stochastic rounding, etc.) and clamping behavior (saturating to max instead of inf) can affect model accuracy&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;. ONNX added a &amp;lt;code&amp;gt;round_mode&amp;lt;/code&amp;gt; attribute to its Cast operator to allow controlling this for FP8&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;. In particular, one paper found that &#039;&#039;&#039;rounding up with saturation&#039;&#039;&#039; (round-to-nearest ties away from 0, and clamp overflows to max finite rather than inf) gave better accuracy in large language model training than other rounding modes&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
At present, FP8 is on the cutting edge. NVIDIA’s H100 GPU supports FP8 tensor cores (for both E4M3 and E5M2), and early results show minimal accuracy loss on some vision and NLP tasks when properly tuned. However, using FP8 requires fine-tuning hyperparameters like scaling factors for each layer and perhaps tweaking training procedures. ONNX’s inclusion of FP8 types means you can represent and transport models using FP8 weights or activations, but actual hardware support is limited and typically paired with calibration or dynamic scaling logic outside of ONNX graph (or using custom ONNX functions for scaling). It remains an &#039;&#039;&#039;open research area&#039;&#039;&#039; how to fully automate FP8 quantization for arbitrary models without accuracy loss.&lt;br /&gt;
&lt;br /&gt;
In summary, FP8 formats promise further compression of neural network computation and data. They are an order of magnitude more error-prone than FP16, but when combined with careful scaling, mixed usage of E4M3/E5M2, and maybe modified training techniques, they have shown surprising success. ONNX is actively evolving to support these ultra-low precision types, signaling their potential importance in future AI inference deployments.&lt;br /&gt;
&lt;br /&gt;
== Accumulation Semantics in ONNX ==&lt;br /&gt;
&lt;br /&gt;
One of the roles of the ONNX specification is to define the mathematical behavior of operators (Conv, MatMul, Sum, etc.) in a framework-agnostic way. However, ONNX mostly defines &#039;&#039;what&#039;&#039; an operator computes in exact arithmetic, not &#039;&#039;how&#039;&#039; the computation is carried out in finite precision. This leaves certain details – like accumulator precision or summation order – up to the implementation (runtime or hardware). We&#039;ll examine how ONNX treats accumulation and what guarantees (or lack thereof) are provided.&lt;br /&gt;
&lt;br /&gt;
=== 5.1. Operator Definitions and Numerical Guarantees ===&lt;br /&gt;
&lt;br /&gt;
Each ONNX operator is described as performing an ideal mathematical function on its inputs. For example, &amp;lt;code&amp;gt;MatMul&amp;lt;/code&amp;gt; is defined as the standard matrix product of two tensors, and &amp;lt;code&amp;gt;ReduceSum&amp;lt;/code&amp;gt; computes a summation along given axes. The ONNX spec, as of current versions, &#039;&#039;&#039;does not mandate the precision of intermediate calculations&#039;&#039;&#039; beyond the types of the inputs and outputs. If an operator’s inputs and outputs are float16, the spec implies the computation is done in float16 in a sense, but it does not forbid an implementation from using higher precision internally – it simply doesn&#039;t mention it. There is &#039;&#039;&#039;no explicit guarantee of identical bit-for-bit results&#039;&#039;&#039; across different ONNX backends for floating-point ops. Instead, results are expected to be numerically close, within tolerances one would normally expect from floating-point differences (due to different hardware or algorithms). The ONNX Backend Test suite typically uses a relative or absolute error tolerance when validating outputs for this reason.&lt;br /&gt;
&lt;br /&gt;
Because of this, important details like the use of extended precision or accumulation in higher precision are left ambiguous. This has led to discussions in the ONNX community about making such behavior more explicit. For example, a proposal was raised to allow specification of accumulation precision for certain ops like MatMul&amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;. The reasoning is that as lower-precision types (float16, bfloat16, float8) become more common, knowing or controlling whether an op internally accumulates in a higher precision is important for both accuracy and reproducibility&amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;. In June 2025, an issue on the ONNX GitHub suggested adding an attribute to MatMul (and similarly Gemm, Softmax, etc.) to specify the accumulator type&amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;. By default it would use input precision (status quo), but an attribute could request (for instance) 32-bit accumulation even if inputs are 16-bit&amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;. As of this writing, however, no such attribute exists in the official spec – it&#039;s an open issue. Thus, the numerical semantics are effectively: &#039;&#039;“compute the result as if in the same type as inputs, but actual implementations may use more precision internally as an optimization.”&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
From a guarantee standpoint, ONNX operators promise the mathematically correct result assuming real-number arithmetic, but they &#039;&#039;&#039;do not guarantee bit-exact results&#039;&#039;&#039; due to floating-point effects. There is an understanding that differences can arise from different summation orders, fused operations (e.g., an implementation might use an FMA (fused multiply-add) which has one rounding instead of two), or use of extended precision registers. ONNX does not formally define rounding modes either – it relies on IEEE 754 defaults, but if a backend used something like deterministic summation or alternative rounding, ONNX has no mechanism to express that.&lt;br /&gt;
&lt;br /&gt;
In summary, ONNX&#039;s current stance is largely &#039;&#039;&#039;implementation-dependent&#039;&#039;&#039; for numerical precision. It provides minimal explicit numerical guarantees beyond type constraints. This is why an ONNX model run on two different hardware or runtime libraries can have slightly different results, especially in reduced precision. The burden is on backends to produce results &amp;quot;close enough&amp;quot; to each other that they don&#039;t affect the model&#039;s decisions in any significant way.&lt;br /&gt;
&lt;br /&gt;
=== 5.2. MatMul, Conv, and Reduction Operators ===&lt;br /&gt;
&lt;br /&gt;
Consider ONNX &amp;lt;code&amp;gt;MatMul&amp;lt;/code&amp;gt;: it takes two tensors (often two matrices) and produces their matrix product. In pure math, $C_{ij} = \sum_k A_{ik} &#039;&#039; B_{kj}$. If A and B are float16, should that sum be computed in float16 or float32? As discussed, ONNX doesn’t specify – it simply says the output is a float16 matrix containing the product. In practice, many backends will &#039;&#039;promote&#039;&#039; the computation: for example, NVIDIA GPUs will do the multiplication and addition in a mixed FP16/FP32 mode by default (because of tensor cores), then convert the final result back to float16. ONNX Runtime on CPU might not support float16 multiplication natively, so it will insert cast ops to float32, perform the MatMul in float32, then cast back to float16&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;. From ONNX’s perspective, as long as the final result is within acceptable error of the “ideal” float16 computation, it’s fine. But note, an “ideal” float16 computation (where every multiply and add is rounded to FP16) might actually be &#039;&#039;less accurate* than what the backend did with FP32. This means that a backend using higher precision can produce a result that is not bit-equal to a pure FP16 implementation – it&#039;s actually closer to the true real number result. This is usually considered a good thing (improved accuracy), but it does highlight a lack of consistency: another backend might do everything in pure FP16 and get a slightly different answer.&lt;br /&gt;
&lt;br /&gt;
The same situation occurs for &#039;&#039;&#039;convolution&#039;&#039;&#039; (&amp;lt;code&amp;gt;Conv&amp;lt;/code&amp;gt; op) which is essentially a batched sum of products (like MatMul in sliding window), and for explicit reduction ops like &amp;lt;code&amp;gt;ReduceSum&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;ReduceMean&amp;lt;/code&amp;gt;, etc. If you have an ONNX &amp;lt;code&amp;gt;ReduceSum&amp;lt;/code&amp;gt; that sums 1000 float16 values, the spec doesn’t say how to sum them. A GPU kernel might sum in a tree using FP32 accumulators; a naive implementation might sum in order with FP16 and possibly overflow or underflow intermediate steps. Both are allowed as long as they output a float16 result that is “reasonable.” In practice, ONNX Runtime and others take care to avoid obviously bad behavior (like overflow) by using higher precision or by splitting the sum. But again, it’s not mandated by the ONNX standard explicitly.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Softmax&#039;&#039;&#039; is an interesting case: Softmax involves an internal sum of exponentials. ONNX defines the Softmax operator as $$\mathrm{Softmax}(x)_i = \frac{\exp(x_i)}{\sum_j \exp(x_j)},$$ typically applied along a specified axis. Directly computing this in float16 can be problematic for large vectors, as discussed in Section 6.3. Indeed, the issue of accumulation precision arises: should the sum $\sum_j \exp(x_j)$ be done in FP16 or can it be in FP32? Many implementations will do it in FP32 even if $x$ is FP16, because the dynamic range of $\exp(x)$ is large. The ONNX Softmax spec doesn’t detail this, but it expects the result to be a proper probability distribution (summing to 1.0 within normal floating error). A pure FP16 softmax on, say, 1000 elements could sum to something like 0.996 or 1.004 due to rounding, whereas FP32 accumulation would be much closer to 1.000. ONNX likely allows both; the small difference is usually inconsequential, but if it were large (say the FP16 sum overflowed to inf), that would be a correctness issue. A robust backend will prevent that (by using the common max-subtraction trick and higher precision accumulation). The ONNX issue 7072 indeed calls out Softmax as another op where explicit control of accumulation precision would be useful&amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
In summary, &#039;&#039;&#039;MatMul, Conv, and reduction ops&#039;&#039;&#039; in ONNX are defined mathematically but with &#039;&#039;implicit&#039;&#039; numerical behavior. Implementations commonly use higher precision for intermediate sums especially when input is low precision, but this is not standardized. Consequently, results can differ slightly between implementations. For critical applications, one must be aware of these differences. A model exported to ONNX with float16 weights might perform slightly differently on one runtime vs another if one does all float16 math and the other uses float32 accumulations. Both conform to ONNX. As a user, if you require a certain precision for accumulation, currently you have to ensure it by the way you export or run the model (for example, insert manual cast nodes in the ONNX graph to force certain ops to FP32). We may see future ONNX versions address this with attributes or metadata as mentioned, but for now these ops are effectively “compute with best effort accuracy”.&lt;br /&gt;
&lt;br /&gt;
=== 5.3. Backend and Hardware-Dependent Behavior ===&lt;br /&gt;
&lt;br /&gt;
Because ONNX is implemented by many backends (ONNX Runtime, TensorRT, CoreML, PyTorch JIT, etc.), and these in turn run on varied hardware (CPU, GPU, accelerators), the numeric behavior can be backend-dependent. Some examples:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;CPU backends:&#039;&#039;&#039; Many CPUs do not have native float16 vector arithmetic (with a few exceptions in recent CPUs). ONNX Runtime’s default CPU execution provider will often upcast float16 to float32, compute, then downcast&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;. This ensures accuracy but means that if you thought you were getting a full float16 pipeline, you’re not – the CPU is essentially ignoring the low precision in compute. On the other hand, x86 CPUs have extended 80-bit precision in x87 registers (though less used now) and 64-bit SIMD registers for doubles; some compiler optimizations might carry intermediate results in higher precision. This could make CPU results slightly different from a GPU’s if, say, a sum is done in 80-bit on CPU versus 32-bit on GPU. Generally, modern compilers avoid such extended precision unless explicitly asked, to improve reproducibility.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;GPU backends:&#039;&#039;&#039; GPUs often perform summations in parallel using tree reductions. The order of operations can change depending on how threads are scheduled. This means even on the same GPU, summing an array in one launch versus another might not give bit-identical results if the internal parallel reduction algorithm differs (though it should be deterministically the same given the same library version and input, but if you change GPU model or BLAS library, it might differ). Also, GPUs have fused multiply-add (FMA) instructions that do $(a*b + c)$ in one step with one rounding, which can produce slightly different results than doing the multiply and add separately (which would round twice). If an ONNX backend uses a fused kernel (like a GEMM library) it might yield different low-order bits than a simple loop.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Vendor libraries:&#039;&#039;&#039; ONNX Runtime can use vendor BLAS or DNN libraries (MKL, cuDNN, TensorRT, etc.). These libraries often implement their own strategies for numerical stability. For example, NVIDIA’s cuDNN might use FP32 accumulation for FP16 inputs by default. TensorRT (NVIDIA’s inference engine) will sometimes automatically promote precisions or insert scaling to maintain stability&amp;lt;ref name=&amp;quot;ref13&amp;quot;/&amp;gt;. On the other hand, if you explicitly request FP16 in TensorRT with no FP32 fallback, it might do the entire op in FP16 and you’d see more error. Some libraries offer knobs: TensorRT allows choosing FP32, FP16, or INT8 precision for each layer, and will use FP32 internally for safety in some cases unless forced otherwise.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Hardware-specific quirks:&#039;&#039;&#039; As another example, NVIDIA’s &#039;&#039;&#039;TensorFloat-32 (TF32)&#039;&#039;&#039; on Ampere GPUs is a precision mode where matrix multiplications use 10-bit mantissa (so about FP16 precision for the multiply) but keep 8-bit exponent (range like FP32). By default, CUDA libraries use TF32 for FP32 matrix multiplies on Ampere to speed them up, unless you disable it. This means if you run an ONNX model with FP32 on an A100 GPU, your matmuls might actually be lower precision than FP32 (though higher than FP16)&amp;lt;ref name=&amp;quot;ref6&amp;quot;/&amp;gt;. It’s an example of hardware choosing an intermediate precision for performance. From ONNX’s perspective, the output is still FP32 and (hopefully) within error tolerances, but a user might be surprised that it’s not a “true” FP32 computation internally. If exactness is needed, one has to turn off TF32.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Non-determinism:&#039;&#039;&#039; Some backends (especially on GPUs) allow non-deterministic execution for performance. For instance, atomic adds in parallel reductions can produce results that vary run-to-run because the order of accumulation is race-condition-dependent. ONNX’s spec doesn’t cover this explicitly, but it’s something to consider. In ONNX Runtime, there are settings for deterministic computing, but by default it may use all available performance optimizations, even if results vary at the 1e-7 level.&lt;br /&gt;
&lt;br /&gt;
In short, the numerical behavior under ONNX can depend on the combination of backend software and hardware capabilities. The lack of a strict spec for accumulation means implementations have freedom to use clever strategies (higher precision, reordering) to improve accuracy or speed. Usually this is positive (e.g. getting better accuracy than a naive approach). But it could also lead to slight inconsistencies. For example, you might find that exporting a PyTorch model to ONNX and running it with one engine yields outputs slightly different from PyTorch’s – not because ONNX is wrong, but because the summation or convolution was done differently (maybe PyTorch summed in FP32 as well, but maybe not in exactly the same way).&lt;br /&gt;
&lt;br /&gt;
The key takeaway is that ONNX &#039;&#039;by design&#039;&#039; abstracts away these details, which is good for portability but means you need to &#039;&#039;&#039;trust the backend&#039;&#039;&#039; or verify its precision behavior for sensitive models. If necessary, you can enforce certain behavior by inserting ops (for example, if you want to ensure FP32 accumulation, you could insert a cast to FP32 for inputs of a MatMul and cast the result back to FP16 – this way the ONNX graph itself mandates FP32 compute in between). This of course sacrifices some performance or device-specific magic.&lt;br /&gt;
&lt;br /&gt;
Looking forward, as mentioned, there are proposals to enrich ONNX’s specification regarding precision. This might include explicit flags for “exact summation” or “accumulate in int32” etc., which would make models more self-descriptive but also potentially less portable (if a backend doesn’t support that, what then?). For now, users should be aware of these backend differences. Most of the time, well-engineered ONNX runtimes yield results close enough that they don&#039;t affect the application (e.g., classification top-1 remains the same even if low-level sums differ by 1e-5). But for high-stakes numerical computations, testing and maybe adjusting the model or backend settings is prudent.&lt;br /&gt;
&lt;br /&gt;
== Numerical Stability in Deep Learning Models ==&lt;br /&gt;
&lt;br /&gt;
Deep learning computations involve many operations that, while theoretically well-defined, can be sensitive to numerical issues when implemented in finite precision. We&#039;ll discuss a few common scenarios in neural networks where numerical stability is a concern, and how precision and accumulation play a role.&lt;br /&gt;
&lt;br /&gt;
=== 6.1. Dot Products and Large-Scale Reductions ===&lt;br /&gt;
&lt;br /&gt;
Neural networks are full of dot products: every fully-connected layer computes $\mathbf{y} = W\mathbf{x}$, which is a series of inner products between weight vectors and the input. Convolutional layers compute weighted sums of local regions. Attention mechanisms compute large matrix products of query/key/value matrices. All these boil down to summing a lot of terms: $\sum_{k=1}^K a_k b_k$. When $K$ is large (it could be in the thousands or more for a single neuron in a fully connected layer of a large model), the potential for floating-point error in that sum increases.&lt;br /&gt;
&lt;br /&gt;
If using FP32, the error may still be negligible in many cases, but in lower precision like FP16, it can become significant. For example, consider a dot product of length 2048 in FP16. Each product is in FP16 (with ~3 decimal digits precision) and if accumulated in FP16, the worst-case error could be on the order of $2048 \times \varepsilon_{FP16} \approx 2048 \times 0.0009766 \approx 2$ (i.e., you could lose a couple units in the third decimal place relative accuracy in worst case). Even in random error accumulation, you might see error on the order of $\sqrt{2048} \times 0.0009766 \approx 0.044$ or 4.4% (though this is a rough estimate). That’s quite significant. Fortunately, as discussed, most hardware will accumulate such sums in a higher precision like FP32, dramatically reducing that error (~7 orders of magnitude smaller $\varepsilon$). But if it didn’t, the network’s outputs could get noisy. A fully-connected layer with many inputs might output slightly different results depending on summation order if done in low precision.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Large-scale reductions&#039;&#039;&#039; also occur in operations like global average pooling (summing all elements of a feature map) or computing the total loss by summing loss over batch samples. If one sums 100,000 values in FP32, the worst-case error could be on the order of 100,000 &#039;&#039; 1e-7 ≈ 1e-2 (1% uncertainty), though typical error would be much less. In FP16, summing 100k values is likely impossible without overflow: as noted, adding ~10^4 moderate numbers can overflow FP16&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. So large reductions &#039;&#039;must* be handled carefully (either in chunks or in higher precision). Frameworks generally do: for example, when performing a sum reduction on GPU, libraries break it into parallel blocks that accumulate in registers (usually 32-bit registers, even if inputs are 16-bit). &lt;br /&gt;
&lt;br /&gt;
Another aspect is that many dot products in neural nets involve values of varying magnitude. If the distribution of terms is such that some are much larger than others, the smaller terms might not contribute much if summed last. Ideally, one might sort by magnitude or use pairwise summation to improve that. Most BLAS libraries effectively do a pairwise strategy due to their parallel structure. So, while worst-case analyses are grim, practical implementations mitigate a lot of it.&lt;br /&gt;
&lt;br /&gt;
However, certain layers can suffer if not handled properly. One famous example is in RNNs or LSTMs: if one uses FP16 naive summation for the gating mechanism, errors could accumulate over many time steps and cause drift or instability. In modern practice, recurrent layers are often run in FP16 with FP32 accumulators to avoid this.&lt;br /&gt;
&lt;br /&gt;
In summary, &#039;&#039;&#039;dot products and large reductions need sufficient precision&#039;&#039;&#039; or robust algorithms. If you are implementing a custom kernel (say a custom ONNX op) and you have to sum a huge array, you should consider using a technique from Section 3 (pairwise summation, Kahan, or at least double accumulator if possible). Many deep learning computations, by virtue of hardware and library design, already do something like this under the hood. But it&#039;s always good to be aware, especially if pushing into lower precision territories.&lt;br /&gt;
&lt;br /&gt;
=== 6.2. Cancellation, Dynamic Range, and Conditioning ===&lt;br /&gt;
&lt;br /&gt;
We introduced &#039;&#039;cancellation&#039;&#039; earlier in the context of summation. In deep learning, cancellation can appear in a few places. One is in the computation of certain norms or variances. For instance, Batch Normalization computes mean and variance of activations. The variance formula $\mathrm{Var}(x) = E[x^2] - (E[x])^2$ is subject to catastrophic cancellation if $E[x]$ and $\sqrt{E[x^2]}$ are close in magnitude (which can happen if the variance is small compared to the mean). If implemented naively in single precision, you might get a negative variance due to numerical error (which would then be clamped to zero or sqrt would yield NaN). Robust implementations compute variance in a single pass or increase precision. Indeed, in many frameworks, batchnorm accumulation is done in FP32 even if the activations are FP16, precisely to avoid numerical issues in computing mean and variance. ONNX itself doesn’t specify this, but an ONNX backend is likely to do batchnorm stats in higher precision for safety. The &#039;&#039;conditioning&#039;&#039; of the variance calculation is poor when variance is tiny relative to mean, so it&#039;s a spot where higher precision is needed.&lt;br /&gt;
&lt;br /&gt;
Another example is in certain loss functions or regularizations. If you subtract two large numbers to get a small difference, you lose precision. Say you have two nearly identical predictions and you subtract them as part of some operation – if done in float16, you might get zero whereas float32 would have a slight difference. This could affect, say, a gradient if that difference was supposed to propagate information. Fortunately, neural nets often avoid directly subtracting large nearly equal numbers; but it can happen. In attention mechanisms, the &#039;&#039;score normalization&#039;&#039; (softmax) subtracts a max to avoid huge exponentials, which is good for range but if multiple scores are close to the max, you subtract numbers that are close. However, those subtractions are not catastrophic in the same way; they&#039;re intentional to reduce range and are typically fine in FP32 or even FP16.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Dynamic range&#039;&#039;&#039; issues are everywhere in deep learning. Activation functions like ReLU and exponential can produce very large or very small values. If subsequent computations don’t account for that, you get infinities or zeros. For example, consider a network output that produces a logit of 100 in FP16. $\exp(100)$ in FP16 overflows to infinity (since FP16 max ~ 6e4 and $e^{100} \sim 3.7e43$). If you then do softmax, that one infinity will dominate the sum and you might get a probability of 1 for that class (or NaNs if not handled). The standard softmax implementation subtracts the max logit, so in this case it would subtract 100 from all logits, making the largest exponent $\exp(0)=1$ and others tiny – this avoids overflow. But what about underflow? If you had a very negative logit, $\exp(-100)$ in FP16 would underflow to 0. In softmax, that just contributes ~0 probability, which is fine (it effectively is zero anyway). Underflow tends to be less dangerous than overflow in inference — underflow usually just means “this probability/neuron is effectively 0,” whereas overflow can poison the results with NaNs or Infs. &lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Conditioning&#039;&#039;&#039; refers to how sensitive a function is to input perturbations. Neural networks themselves as functions can be poorly conditioned (e.g., tiny changes in weights cause big changes in output for some architectures), but typically they are designed to be reasonably well-behaved. Certain internal computations like matrix inversion (if one does a pseudoinverse or something for a layer) can be extremely ill-conditioned if the matrix is near singular. That again would need double precision perhaps. In ML, one common ill-conditioned operation is the &#039;&#039;&#039;softmax denominator&#039;&#039;&#039;: $\sum_j e^{x_j}$ can be very large and also very sensitive to changes in the largest $x_j$. However, the softmax is more stable if computed carefully (max trick). Another is &#039;&#039;&#039;normalization&#039;&#039;&#039;: computing $\frac{x - \mu}{\sigma}$ for layer norm or batch norm – if $\sigma$ is very small, then the division blows up noise. In those cases, a small $\epsilon$ is usually added to $\sigma^2$ to prevent instability (this is a model-level fix, not a numeric one per se, but it acknowledges numeric limits).&lt;br /&gt;
&lt;br /&gt;
In training, &#039;&#039;&#039;optimizers&#039;&#039;&#039; can suffer from precision issues. For example, the Adam optimizer keeps a moving average of squared gradients; in half precision, these small updates might flush to zero or not change at some point. Some research found that certain adaptive optimizers break in FP16 because the small differences are lost (&amp;quot;Why FP16 training breaks NAdam&amp;quot; etc.). The solution is often to keep the optimizer states in FP32 while doing the bulk of compute in FP16. This again is a form of mixed precision.&lt;br /&gt;
&lt;br /&gt;
From an inference perspective, once the model is trained, the main concerns are avoiding overflow in activations and ensuring summations (like in softmax or reductions) are done with enough precision. The model’s weights are fixed, so we don&#039;t have to worry about cumulative error over iterations as in training, just the forward pass.&lt;br /&gt;
&lt;br /&gt;
=== 6.3. Normalization Layers and Softmax ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Normalization layers&#039;&#039;&#039; (BatchNorm, LayerNorm, etc.) and &#039;&#039;&#039;Softmax&#039;&#039;&#039; are specifically worth examining for numerical stability:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Batch Normalization:&#039;&#039;&#039; During inference, batchnorm applies an affine transform using precomputed mean and variance (from training). It’s basically: $y = \frac{x - \mu}{\sqrt{\sigma^2 + \epsilon}} \gamma + \beta$. This is a stable operation as long as $\epsilon$ is provided (to avoid division by zero). Since $\mu, \sigma^2$ are constants in inference, it reduces to linear ops which are fine. The main numerical work (computing mean/variance) happened during training or model calibration, and frameworks usually did that in FP32. So batchnorm at inference time is not a source of instability (it’s just a scale and shift of the data).&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Layer Normalization / Group Normalization:&#039;&#039;&#039; These compute mean/var on the fly for each sample (and possibly channel group). In inference, they still need to compute these statistics for each forward pass. If implemented in low precision, the variance calculation can suffer cancellation as mentioned. However, it&#039;s likely that inference engines compute the mean and variance in FP32 even if inputs are FP16 (especially since these are per-sample operations, not huge batch reductions – it&#039;s feasible to do in higher precision). If someone implemented layer norm purely in FP16, they might see issues when the variance is very small. Usually there’s an $\epsilon$ added which might dominate a tiny variance and thus mitigate catastrophic cancellation (the result might just output something mostly from $\epsilon$). In summary, normalization layers are usually fine as long as the implementation uses enough precision for the summation of squares.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Softmax:&#039;&#039;&#039; Softmax is notorious if done improperly. The safe algorithm: subtract the maximum input from all inputs (to bring the largest exponent to 1.0), exponentiate, sum them up, divide each exponent by the sum. The max subtraction handles overflow in $\exp()$. But what about the sum? As discussed, if there are $N$ elements in the softmax, the sum is at most $N$ (when all inputs equal the max). In classification, $N$ might be the number of classes. For ImageNet, $N=1000$, so worst sum ~1000 (which is fine in FP16, as 1000 &amp;lt;&amp;lt; 65504). For an extreme case like a language model, $N$ could be 50,000 (vocab size). Sum could be 50,000 in worst case (everyone equal probability). 50,000 is still below 65504, so actually an FP16 sum would &#039;&#039;not&#039;&#039; overflow in that scenario, and the rounding error on a sum of 50k identical terms would be maybe a few ULPs (which at 50k magnitude might be ~0.5 difference worst-case). So maybe softmax summation in FP16 is not catastrophic for output probabilities – you’d get essentially 1.0 after normalization instead of 0.99999 in some case. Usually fine.&lt;br /&gt;
&lt;br /&gt;
:The bigger issue is if one or a few terms dominate the softmax. If one term is much larger, the others underflow to 0 after the max subtraction. That’s fine: you get a one-hot distribution essentially. If the model expected a softer distribution, that could be a problem. But if the logits were really that far apart, even FP32 would give nearly one-hot. So not really a precision issue, more of a model property.&lt;br /&gt;
&lt;br /&gt;
:However, the &#039;&#039;&#039;stability during training&#039;&#039;&#039; is a big issue. The GitHub thread we saw (ColossalAI) mentioned training a 130B model where FP16 softmax in attention caused training to occasionally blow up&amp;lt;ref name=&amp;quot;ref14&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref14&amp;quot;/&amp;gt;. They observed attention scores in some heads ranging up to 1e4 or as low as 1e-3&amp;lt;ref name=&amp;quot;ref14&amp;quot;/&amp;gt;. 1e4 exponent in FP16 is definitely Inf (because $\exp(10^4)$ is astronomically huge, but even exp(100) is inf in half). The method CogView introduced (Precision Bottleneck Relaxation) involved subtracting the max &#039;&#039;per head&#039;&#039; to reduce range&amp;lt;ref name=&amp;quot;ref14&amp;quot;/&amp;gt;—which is basically the softmax max trick at a finer granularity or something similar. In the end, they found &#039;&#039;&#039;the simplest fix was to compute softmax in FP32&#039;&#039;&#039; for those layers&amp;lt;ref name=&amp;quot;ref14&amp;quot;/&amp;gt;. They reported that it had negligible speed impact but significantly improved stability&amp;lt;ref name=&amp;quot;ref14&amp;quot;/&amp;gt;. This highlights a general principle: certain operations like softmax (and sometimes matrix multiplications in attention) benefit from higher precision, and using FP32 in a few critical spots can save a mixed-precision training from crashing. In inference, using FP32 for softmax is also a safeguard if one is uncertain. It costs little because softmax is not a heavy compute compared to matrix multiplies.&lt;br /&gt;
&lt;br /&gt;
:ONNX does have a Softmax operator; if the input is float16, a backend might do exactly what they described: internally cast to float32 for the softmax computation, then cast output back to float16. Some inference engines (like DeepSpeed’s inference or others) keep attention softmax in FP32 even if rest is FP16, for robustness&amp;lt;ref name=&amp;quot;ref14&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Apart from softmax, other places where exponentials occur are in loss functions like cross-entropy (which uses log-softmax internally), or in activation functions like Sigmoid and Tanh (which are basically scaled logistic functions). Sigmoid($x)=1/(1+e^{-x})$. If $x$ is moderately large positive, $e^{-x}$ underflows to 0 in FP16 if $x&amp;gt;~11$. That gives a sigmoid output of 1.0 exactly. In FP32, $e^{-x}$ for $x=11$ is about $5e-5$, not underflow, so sigmoid ~0.99995. So FP16 will saturate some outputs to exactly 0 or 1 sooner than FP32 would. This can slightly change behaviors (e.g., a neuron might go to full saturation a bit early). Usually, this isn’t a huge deal, but it could make gradients zero earlier. In inference, we don’t care about gradients, just the output value. Getting exactly 1.0 vs 0.99995 probably has no practical difference for a classification output. But one should be aware that &#039;&#039;&#039;activation functions can saturate faster&#039;&#039;&#039; in lower precision due to limited range or precision. ReLU has no issue (it’s linear or 0), but exponential-based activations do. In practice, again, frameworks often simply compute those in the given precision and accept the slight difference. If it were a problem, one could do those in FP32 as well.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Summary:&#039;&#039;&#039; Numerical stability in inference is maintained by using robust computation methods: subtracting maxima in softmax/LogSumExp, adding epsilon in denominators for normalization, and using higher precision for summations or critical steps. Most of these are built into the algorithms. ONNX models assume these standard practices (they don&#039;t need to explicitly do them in the graph because the backend inherently will for ops like Softmax). It&#039;s wise when building or exporting models to ONNX to test them in the target precision and ensure there are no surprises (like an output becomes NaN or Inf due to precision). Usually if training was done in mixed precision, the model will have been vetted in that environment already.&lt;br /&gt;
&lt;br /&gt;
== Performance vs. Accuracy Trade-offs ==&lt;br /&gt;
&lt;br /&gt;
There is an inherent trade-off between numerical precision and computational performance (speed, memory usage) in AI systems. Choosing lower precision can dramatically increase throughput and reduce memory, but it may introduce arithmetic errors. Engineers must balance these considerations:&lt;br /&gt;
&lt;br /&gt;
=== 7.1. Throughput, Memory Bandwidth, and Precision ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Throughput (speed)&#039;&#039;&#039; is often higher for lower-precision operations. Modern hardware can execute more low-precision ops in parallel than high-precision ops. For example, an NVIDIA V100 GPU can perform 2× more half-precision FLOPs than single-precision per clock, and newer architectures with Tensor Cores can do 8× more (since a Tensor Core warp operation might perform 64 FP16 FMAs per cycle vs 16 FP32). Similarly, int8 operations can be even faster and more energy efficient. Lower precision numbers also use less memory bandwidth – fetching 16-bit data is twice as fast as 32-bit data from memory (assuming the same bus width), which can relieve memory bottlenecks. Many deep learning workloads are memory-bound, so halving the data size can nearly double throughput if compute is not the limiting factor. This is a huge incentive to use lower precision.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Memory and storage&#039;&#039;&#039;: Using FP16 or int8 weights reduces model size, which is important for deploying large models to memory-limited environments (like edge devices or GPUs with limited VRAM). It also means you can fit larger batch sizes in memory, or more models on a chip, etc. There is also a cascade effect: smaller data means better cache utilization, which further improves performance.&lt;br /&gt;
&lt;br /&gt;
However, &#039;&#039;&#039;accuracy&#039;&#039;&#039; suffers if precision is too low. We have discussed how rounding error and limited range can cause deviations in outputs. The question is: do those deviations matter for the application? Often for neural networks, a small amount of noise or error in intermediate calculations does not change the final prediction. Neural nets are somewhat error-tolerant due to their redundant distributed representations. This is why quantization and FP16 techniques can work at all. The final model accuracy (e.g. top-1 accuracy on ImageNet) might drop only 0.5% or less when using FP16 instead of FP32, or int8 instead of FP16, if done properly. For many use-cases, this loss is acceptable given the speed gains.&lt;br /&gt;
&lt;br /&gt;
If one goes too low (like naive FP8 or int4 without special care), accuracy might drop severely – perhaps making the model unusable. Thus the trade-off: as precision decreases, performance increases but at some point accuracy degrades beyond an acceptable threshold. Engineers aim to find the lowest precision that still preserves accuracy.&lt;br /&gt;
&lt;br /&gt;
There is also the consideration of &#039;&#039;&#039;accumulation strategy&#039;&#039;&#039; affecting performance: for instance, Kahan summation (compensated) is more accurate but ~4× slower than naive summation. If you need that accuracy, you pay a performance cost. In many AI inference scenarios, the slight extra accuracy from something like Kahan isn&#039;t needed, so it’s not used. But in a scenario where you sum millions of elements for a final result that is thresholded, maybe you do need it.&lt;br /&gt;
&lt;br /&gt;
Another dimension is &#039;&#039;&#039;determinism vs speed&#039;&#039;&#039;. Enforcing a deterministic reduction order (for reproducibility) might require disabling some parallelism, which could slow things down. If exact repeatability is critical (like certain scientific or financial applications of neural nets), one might sacrifice some performance to ensure the summation happens the same way every time. ONNX allows both modes depending on the runtime.&lt;br /&gt;
&lt;br /&gt;
From a memory perspective, if precision is higher than needed, you waste memory. For example, storing intermediate activations in FP32 when they could be FP16 uses 2× memory, which might mean lower batch size or more memory traffic. So there&#039;s strong motivation to keep things as low-precision as possible &#039;&#039;except&#039;&#039; where accuracy truly demands.&lt;br /&gt;
&lt;br /&gt;
In summary, the trade-off often manifests as &#039;&#039;diminishing returns&#039;&#039;: FP64 vs FP32 – huge slowdown for usually negligible accuracy gain in ML; FP32 vs FP16 – moderate speedup for usually minor accuracy hit; FP16 vs int8 – another big speedup but maybe more accuracy loss unless calibration is done; int8 vs int4 – speed gain but often big accuracy drop that might not be worth it for current models.&lt;br /&gt;
&lt;br /&gt;
Engineers use techniques like &#039;&#039;&#039;mixed precision&#039;&#039;&#039; to target high precision only where needed. For example, run most of the model in FP16, but keep a few sensitive layers in FP32. This yields most of the performance benefits while ensuring critical parts are accurate. We already saw softmax as one example. Another might be final output layer: sometimes using FP32 for the logits and softmax can be safer.&lt;br /&gt;
&lt;br /&gt;
Hardware is also trending to support mixed modes – like FP8 Tensor Cores accumulate in FP32, etc. So hardware tries to give accuracy cheaply.&lt;br /&gt;
&lt;br /&gt;
=== 7.2. Hardware Accelerators and Tensor Cores ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Hardware accelerators&#039;&#039;&#039; (GPUs, TPUs, FPGAs, ASICs like GraphCore IPU, etc.) have specific support for certain precisions. Leveraging these can provide massive speedups:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;NVIDIA Tensor Cores:&#039;&#039;&#039; These started in Volta (V100) with support for 4x4 matrix multiply-adds on FP16 inputs with FP32 accumulation&amp;lt;ref name=&amp;quot;ref6&amp;quot;/&amp;gt;. Using Tensor Cores can give up to 8x throughput compared to using regular FP32 cores. But to use them, the data needs to be FP16 (or now BF16 or FP8 on newer GPUs) and aligned to certain dimensions (multiples of 8 etc. in matrix sizes, which frameworks handle by padding if needed). If an ONNX model is all FP32, by default it would not use Tensor Cores unless the runtime automatically converts some ops to mixed precision (which some do via a flag or environment variable enabling TF32 or FP16). The introduction of &#039;&#039;&#039;TensorFloat-32 (TF32)&#039;&#039;&#039; in Ampere GPUs is essentially a compromise: treat FP32 inputs as two 16-bit significand halves (10-bit mantissa actually) so that it can run on Tensor Cores, giving ~8x speed, while still outputting FP32. TF32 has about the precision of FP16 but range of FP32, and it was chosen because it typically doesn’t hurt convnet accuracy. ONNX Runtime or others on Ampere will use TF32 by default for conv and matmul unless disabled, giving a free speed boost at minimal accuracy cost. If exact FP32 is needed, users have to turn it off (sacrificing speed).&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;TPU (Google):&#039;&#039;&#039; TPUs have native support for BF16 matrix ops with FP32 accumulators. They were designed specifically for the mixed precision approach in training. For inference, TPUs also support int8 and other quantized ops (e.g., via Edge TPU for lower-power). If exporting ONNX to run on a TPU (via some bridge), one might get automatic BF16 usage. BF16 has the advantage of no loss in range, so TPUs can more straightforwardly run models in BF16 without modifications.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;FPGAs and ASICs:&#039;&#039;&#039; Some custom accelerators might use fixed-point or block floating-point arithmetic for efficiency. They often treat the network in quantized terms. The ONNX quantization specification (QuantizeLinear/DequantizeLinear ops) exists for representing quantized models (INT8 etc.), which is another domain beyond floating-point, but it overlaps as a precision issue. In those systems, accumulators for INT8 × INT8 multiplies are usually 32-bit (to avoid overflow for dot products up to certain lengths). The trade-off there is between integer vs float representation, which can give big speedups on CPUs/DSAs via SIMD instructions (many CPUs have vector int8 multiply-add instructions now for inference).&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Memory bandwidth on hardware:&#039;&#039;&#039; Some accelerators include special high-bandwidth memory or cache specifically for lower precision. For example, an accelerator might fetch two FP16 values in one 32-bit memory transaction – effectively doubling memory bandwidth for half precision. On some hardware, using FP16 can even reduce memory &#039;&#039;latency&#039;&#039; if multiple values can be packed.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Parallelism and Vectorization:&#039;&#039;&#039; Many hardware units process data in fixed vector widths (e.g., 256-bit registers). That could be 8 floats at once or 16 half-floats at once in the same 256 bits. If your code is in FP16, you might use the full vector width (16 values) whereas FP32 might only use half (8 values). This means FP16 can make better use of vector units. Similarly, matrix units often have fixed tile sizes, and smaller data types allow larger tiles or more tiles concurrently.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Energy efficiency:&#039;&#039;&#039; Lower precision often means less energy per operation, which on large deployments (data centers or battery-powered devices) is a significant advantage. This isn&#039;t directly performance, but it&#039;s an important trade: you might choose a slightly lower precision to save power even if accuracy drops a tiny bit.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Software overhead:&#039;&#039;&#039; Using mixed precision on hardware involves some overhead of casting and moving data between precisions. For instance, if you have to frequently cast FP16 to FP32 and back, that adds instructions and memory copies. A well-designed accelerator (like GPUs with unified memory for FP16/32 or special hardware casting) minimize this. Still, the runtime might insert extra operations (as we saw, ONNX Runtime CPU inserted many Cast nodes converting FP16 to FP32 and back&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;, which actually &#039;&#039;slowed down&#039;&#039; that FP16 model by 4x vs FP32 on a CPU, because the CPU had no native FP16). So ironically, on some hardware, using lower precision in ONNX might degrade performance if not supported (the CPU case). Always ensure the target hardware actually supports the low precision you plan to use; otherwise, you&#039;ll pay overhead for emulation.&lt;br /&gt;
&lt;br /&gt;
To illustrate trade-off: Suppose you have a model that achieves 99% accuracy in FP32 and drops to 98.5% in FP16. If FP16 runs 2x faster, many would accept the 0.5% accuracy drop for double throughput. If int8 runs 4x faster but accuracy drops to 95%, that might be too much drop for some applications, but acceptable for others that value speed. It&#039;s application-dependent.&lt;br /&gt;
&lt;br /&gt;
In critical applications, one might maintain higher precision accumulators or run certain validation steps in high precision to ensure nothing drifted. In research, sometimes a model is trained in mixed precision but validated in double to ensure any small numeric differences don’t influence the reported result (rare, but for scientific computing integration maybe).&lt;br /&gt;
&lt;br /&gt;
In the ONNX world, performance-vs-accuracy is often managed via &#039;&#039;&#039;Execution Providers&#039;&#039;&#039; (EPs) that target specific hardware. For example, the TensorRT EP in ONNX Runtime can take an ONNX model and run it using NVIDIA&#039;s high-performance inference library, which will use FP16 or INT8 if allowed, to maximize speed. The user can configure the EP with precision flags. If the accuracy drop is too high, one might restrict it to FP32. If it&#039;s fine, allow FP16. Tools exist to measure the accuracy difference (like calibrating int8 quantization).&lt;br /&gt;
&lt;br /&gt;
To conclude, the trade-offs revolve around picking the lowest precision that yields acceptable accuracy, and utilizing hardware features fully. ONNX being a high-level spec abstracts this, but it&#039;s ultimately up to the deployment to decide and sometimes up to the exporter to incorporate quantization. ONNX’s role is to be able to &#039;&#039;represent&#039;&#039; these choices (they have added quantized types, FP8 types, etc., to allow the graph to include those when needed).&lt;br /&gt;
&lt;br /&gt;
== Best Practices and Design Guidelines ==&lt;br /&gt;
&lt;br /&gt;
Given the above insights, here are some best practices and guidelines for dealing with numerical precision and accumulators in AI inference (and training) design:&lt;br /&gt;
&lt;br /&gt;
=== 8.1. Choosing an Accumulator Precision ===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Match precision to data length and range:&#039;&#039;&#039; If an operation accumulates a large number of terms, consider using a higher precision accumulator than the input data. For example, for summing &amp;gt;1000 values of type FP16, an FP32 accumulator is recommended to avoid large rounding error or overflow. If summing &amp;gt; million values of FP32, consider double (FP64) accumulation if high accuracy is needed (though this is rare in DL). &#039;&#039;Rule of thumb:&#039;&#039; if $N \cdot \varepsilon$ is not tiny (say &amp;gt;1e-3), you may benefit from higher precision. For FP16 ($\varepsilon \approx 1e-3$), that threshold N is a few hundred. For FP32 ($\varepsilon \approx 1e-7$), N would be in the millions before 1e-3 error.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Use higher precision in critical layers:&#039;&#039;&#039; Identify if certain layers of the model are more sensitive to precision. For instance, the output layer or a layer that feeds into a very sharp decision boundary might require more precision. If a small change there could flip the prediction, you don&#039;t want excessive numerical error. Using FP32 for logits and softmax is a common practice to maintain classification confidence stability.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Follow hardware defaults, but override if needed:&#039;&#039;&#039; Many hardware accelerators have sensible defaults (like accumulate in FP32 for tensor ops). It&#039;s usually best to use those defaults as they were chosen to balance accuracy and speed. Only override if you have evidence of a problem. For example, if using ONNX Runtime on CPU, you discovered it upcasts to float for you – great. But if using some custom accelerator that does pure FP16, you might manually insert casts in the ONNX graph around certain ops to force higher precision on those ops.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Maintain precision for gradient accumulation (training):&#039;&#039;&#039; This is more training-oriented, but worth noting: when summing gradients over batches or updating weights, use higher precision (FP32 or even 64) for those accumulations. This ensures training doesn&#039;t diverge due to numeric error. Many frameworks keep a &amp;quot;master copy&amp;quot; of weights in FP32 when training in FP16&amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;. For inference, this translates to: any running statistics or counters in the model (like if you had a running average inside the model) should probably be in higher precision to avoid drift.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Be mindful of intermediate cast costs:&#039;&#039;&#039; If you do decide to cast to higher precision for an operation in ONNX, remember it incurs memory copies and compute overhead. Make sure it&#039;s worth it. For example, if you cast a huge tensor to double, that could slow things drastically. Usually FP16-&amp;gt;FP32 is fine (hardware handles it well), but FP32-&amp;gt;FP64 might be expensive on GPUs.&lt;br /&gt;
&lt;br /&gt;
=== 8.2. Safe Reduction Lengths and Heuristics ===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Chunk long reductions:&#039;&#039;&#039; If you must do a very long sum in low precision (due to memory constraints or hardware), chunk the sum into parts, sum each to a higher precision local accumulator, then sum the partial sums. This is essentially pairwise summation. Many libraries use a fixed chunk size heuristic (e.g., sum in blocks of 256 in registers, then add those up). This bounds the error growth. If implementing manually, you can choose a chunk size such that chunk_length &#039;&#039; ε is small (like 256 &#039;&#039; 1e-3 = 0.256 for FP16, hmm maybe choose 100).&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Sort by magnitude if possible:&#039;&#039;&#039; In some cases (not typical in DL, but possible in post-processing), if you have numbers varying by orders of magnitude, summing from smallest to largest improves accuracy. It prevents small ones from being swallowed by big ones. This is more relevant if you output a sum of mixed-scale values (like some algorithm mixing big and small contributions). It’s not always feasible in real-time inference to sort, but keep in mind.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Use compensation for critical sums:&#039;&#039;&#039; If you find a particular sum or difference in your inference computation is crucial (like computing a small residual from two close numbers), consider using Kahan summation or another compensated scheme just for that part. For example, if you had an ONNX custom function that needs to sum a lot of almost cancelling terms, you might implement it with compensation internally.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Heuristics for FP8/low-bit:&#039;&#039;&#039; When using extremely low precision like FP8, adopt the recommended practices: use scaling (don&#039;t feed raw data into FP8, normalize it first), and perhaps only use FP8 on portions of the network that are empirically robust. The industry is still establishing heuristics, but one might be: use FP8 for matrix multiplications, but accumulate in FP16 or FP32; keep normalization in FP16; use FP16 for first and last layer maybe. This is evolving, so keep an eye on literature.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Keep an eye on sum length in softmax and normalization:&#039;&#039;&#039; If you have extremely large class counts or group sizes (e.g., a softmax over 1e6 items, or layer norm over tens of thousands of features), realize this is a giant reduction. You may want to ensure that reduction is done in higher precision. ONNX Softmax by default will likely not handle a million-class case in FP16 well (the sum would overflow since 1e6 &amp;gt; 65504). So either do that on CPU in FP32 or break it down (could do hierarchical softmax, etc.). This is a corner case, but relevant for huge vocab language models. In practice, vocab ~50k is fine as we said.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Test numeric differences:&#039;&#039;&#039; A practical tip: after quantizing or reducing precision, run some test inputs through both the high-precision model and low-precision model. Compare outputs (and maybe some intermediate signals if possible). If differences are consistently small relative to the application tolerance, you&#039;re good. If you see occasional large differences or instabilities (like outputs become NaN or Inf in low precision), that&#039;s a red flag. Use that testing to guide where you might need to increase precision or change algorithms.&lt;br /&gt;
&lt;br /&gt;
=== 8.3. When Higher Precision Is Required ===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Ill-conditioned calculations:&#039;&#039;&#039; If your model or pipeline performs any operation known to be ill-conditioned (matrix inversion, subtraction of nearly equal quantities, computing a tiny difference of large numbers), you likely need higher precision for that part. For example, if your ONNX model includes a custom op to solve a linear system, use double precision in that op if possible (or at least float with iterative refinement). In ML, this might occur in something like a physics-informed neural network or a differentiable simulation within the model.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Strict accuracy requirements:&#039;&#039;&#039; Some applications (medical, aerospace, finance) might have strict tolerances. If an inference model’s output needs to be correct to 5 decimal places, you probably cannot use FP16, which has only 3-4 decimal digits precision. FP32 might be barely enough (~7 digits). FP64 might be warranted to guarantee that level of accuracy in worst case. Most deep learning applications (like image classification) don&#039;t demand that level of numeric precision in outputs – as long as the top class is correct, it doesn&#039;t matter if its probability was 0.901 vs 0.899. But if you&#039;re doing something like summing probabilities to make a risk estimate or doing a cumulative sum over many predictions, be careful to accumulate precisely.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Large sums or products in post-processing:&#039;&#039;&#039; Sometimes the neural net is fine, but the &#039;&#039;post-processing&#039;&#039; of results might need precision. E.g., summing up probabilities of many events, or computing confidence intervals, etc. Do those calculations in double if needed outside the model.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Preventing drift in long sequences:&#039;&#039;&#039; For sequential models (RNNs processing thousands of steps), rounding error can accumulate over steps. If each step has a small error, after 10000 steps it could add up. In such cases, using a higher precision for the state might reduce drift. Some practitioners use FP64 for the hidden state accumulation in very long sequence processing to maintain numerical stability (though this is not common due to speed loss). Alternatively, periodically renormalize or reset small errors.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Simulations or differential equations:&#039;&#039;&#039; If your ONNX model involves simulating something (like a differential equation solver inside a model, or an ODE/RNN hybrid), those often need double precision to be stable for long durations. Recognize those situations and allocate precision accordingly.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Verification and debugging:&#039;&#039;&#039; Use higher precision as a tool to verify correctness. For example, run your model in FP64 (if possible) and compare to FP32 to see if there are differences. If not, FP32 is fine. If yes, you might find a particular calculation losing too much info in FP32, implying maybe you should keep it in FP64 in production as well. This is rare in inference but can be part of rigorous validation.&lt;br /&gt;
&lt;br /&gt;
In ONNX, using higher precision may involve casting to &amp;lt;code&amp;gt;tensor(double)&amp;lt;/code&amp;gt; for certain ops. Not all runtimes support double on GPU though (some do via emulation or just slower path). ONNX CPU does support double for general ops. So be mindful of the target.&lt;br /&gt;
&lt;br /&gt;
Finally, &#039;&#039;&#039;document your precision choices&#039;&#039;&#039;. If you deliver an ONNX model that uses mixed types (float16 inputs, float32 accumulations, int8 weights, etc.), ensure that is clear to whoever maintains or uses it. Precision issues can be sneaky, so clarity helps.&lt;br /&gt;
&lt;br /&gt;
== Limitations and Open Problems ==&lt;br /&gt;
&lt;br /&gt;
Despite advances in handling numerical precision, there remain limitations and open challenges in this domain:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Lack of Precision Specification in ONNX:&#039;&#039;&#039; As noted, ONNX currently has no way to formally specify that an op &#039;&#039;should&#039;&#039; use a higher precision internally. This can lead to nondeterministic differences across platforms&amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;. While proposals exist to add attributes for this&amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;, it is not yet standard. Thus an ONNX model might behave slightly differently on two compliant backends, and there&#039;s no built-in mechanism to ensure identical results. This is a limitation for applications requiring cross-platform consistency. Solving it may involve extending ONNX (e.g., an attribute like &amp;lt;code&amp;gt;accumulatePrecision=&amp;quot;FLOAT32&amp;quot;&amp;lt;/code&amp;gt; for MatMul) or tightening the spec, but those come with backward compatibility and implementation burden concerns.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Reproducibility and Determinism:&#039;&#039;&#039; Even with the same precision, parallel computation can introduce nondeterminism (due to different summation orders). Ensuring bit-identical results across runs (important for some sensitive domains) is non-trivial. Frameworks often provide &amp;quot;deterministic mode&amp;quot; which might use fixed ordering at cost of speed. In ONNX, there&#039;s no global flag for this; it&#039;s up to the runtime. More research could be done on algorithms that produce bit-wise identical results independent of parallel execution order (perhaps using techniques like pairwise summation or consistent partitioning). Stochastic rounding is another area: sometimes using probabilistic rounding can reduce bias in accumulated error, but it introduces randomness – reconciling that with reproducibility is tricky.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Ultra-low Precision (FP8 and below):&#039;&#039;&#039; While FP8 is being explored, going to 4-bit floats or mixed schemes is an open challenge. The OpenAI community is investigating 4-bit (quantization-wise) for LLMs with some success, but a true 4-bit float (like E3M1) would be extremely limited. Perhaps hybrid schemes (like block floating point, log scales, etc.) will come. ONNX already has some support (they mention 4-bit in documentation placeholders&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;), but it&#039;s nascent. Research into training techniques, calibration, and new number formats (like &#039;&#039;&#039;posit numbers&#039;&#039;&#039; or tapering precision floats) is ongoing. If a new format proves useful, ONNX will need to adapt to represent it.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Quantization Noise and Model Robustness:&#039;&#039;&#039; Reducing precision can sometimes expose models to issues like adversarial vulnerability or fragility – e.g., a slight perturbation might flip a decision if the margin was small and precision loss nudged it. Ensuring model robustness under quantization is an open problem. It might require re-training the model with quantization in the loop (quantization-aware training) or designing models that have large &amp;quot;margin&amp;quot; between classes to tolerate quantization noise.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Dynamic range during training vs inference:&#039;&#039;&#039; In training, activations can have wider distributions (especially early in training). Models often learn to constrain their ranges (via activation functions saturating or normalization), which makes inference more stable in lower precision. But if one tries to train in FP8 from scratch, it&#039;s very hard. This gap between what&#039;s possible in a post-trained inference model vs training is an open area – how to train directly in low precision without significant accuracy loss. Solving that could simplify workflows (no need for high-precision training then quantize; just train low-prec end to end). Techniques like loss scaling, gradient clipping, etc., are partial solutions. The development of optimizers that are resilient to low precision is ongoing.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Hardware variability:&#039;&#039;&#039; As new hardware emerges (with new numeric capabilities or quirks), ensuring that ONNX models run consistently is an ongoing task. For instance, one device might implement a fused op that reduces rounding error, another does it separately. Even something like convolution might use a different algorithm (FFT-based vs direct) which has different numeric properties. Open problem: could ONNX allow specifying numeric tolerances per op or preferred algorithms? That may be overkill for most, but for some HPC ML use cases, maybe desired.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Testing and validation of numerics:&#039;&#039;&#039; There isn&#039;t a standardized set of &amp;quot;numeric compliance&amp;quot; tests for ONNX runtimes beyond simple correctness. Perhaps in the future, test suites might include challenging numeric scenarios (large sums, nearly singular matrices) to ensure a runtime handles them gracefully (maybe by switching precision). Right now, it&#039;s mostly on the user to test their model under expected conditions.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Combining quantization with floating point:&#039;&#039;&#039; Many production models use a mix (e.g., int8 for some layers, FP16 for others). ONNX can represent this (with QuantizeLinear ops or MixedPrecision through casting), but tooling to automatically find the optimal mix is still developing. It&#039;s somewhat trial-and-error or using heuristics. An open question is how to automate precision assignment: given a model and a target hardware, how to automatically choose which layers can be int8, which should be FP16, etc., to meet an accuracy target. This crosses into NAS (neural architecture search) territory or automated quantization. &lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Numerical debugging tools:&#039;&#039;&#039; When a model goes wrong due to precision, it&#039;s often hard to pinpoint where. Better tooling to analyze numerical error layer by layer could be useful. For instance, one could run a model in double and float and diff the intermediate results to see where the largest divergence happens. Doing this systematically for big nets is an open tooling challenge. Some frameworks have hooks for this, but not standardized.&lt;br /&gt;
&lt;br /&gt;
In conclusion, while we&#039;ve come far with using lower precision in AI without sacrificing much accuracy, the landscape is always evolving. The key open problem is how to push precision lower (for efficiency gains) without crossing the threshold where accuracy falls off a cliff. Each new generation of hardware and techniques (like FP8, novel quantization schemes) extends that boundary, but also brings new questions on how to manage the numerical errors. ONNX as an interchange will need to keep pace, possibly by adding more expressive power for these numeric considerations.&lt;br /&gt;
&lt;br /&gt;
== See Also ==&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;IEEE 754 Floating-Point&#039;&#039;&#039; – The standard defining binary16, binary32, binary64 formats and rounding behavior.&lt;br /&gt;
* &#039;&#039;&#039;Floating-Point Arithmetic&#039;&#039;&#039; – General topic on how floating-point math works, common issues (overflow, underflow, cancellation).&lt;br /&gt;
* &#039;&#039;&#039;Kahan Summation Algorithm&#039;&#039;&#039; – Algorithm for compensated summation to reduce numerical error&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;.&lt;br /&gt;
* &#039;&#039;&#039;BFloat16&#039;&#039;&#039; – A short-format float with 8-bit exponent, used in TPUs and some GPUs for training networks.&lt;br /&gt;
* &#039;&#039;&#039;Mixed Precision Training&#039;&#039;&#039; – Technique of using FP16 (or BF16) along with FP32 accumulators to accelerate training&amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;.&lt;br /&gt;
* &#039;&#039;&#039;Quantization in Deep Learning&#039;&#039;&#039; – Converting networks to lower bit integer arithmetic for inference (INT8 quantization and calibration techniques).&lt;br /&gt;
* &#039;&#039;&#039;ONNX Operator Specification&#039;&#039;&#039; – Official docs for ONNX ops, which describe their mathematical function but not the required numeric precision (a point of potential extension in future).&lt;br /&gt;
* &#039;&#039;&#039;CUDA Tensor Cores &amp;amp; TF32&#039;&#039;&#039; – NVIDIA documentation on how tensor cores perform mixed precision and the TF32 format for accelerating FP32.&lt;br /&gt;
* &#039;&#039;&#039;Pairwise Summation&#039;&#039;&#039; – Summation algorithm with logN error growth&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;, used in many numerical libraries to improve accuracy of reductions.&lt;br /&gt;
* &#039;&#039;&#039;Softmax Stability&#039;&#039;&#039; – Articles on numerically stable softmax and log-sum-exp implementations to avoid overflow.&lt;br /&gt;
* &#039;&#039;&#039;Performance Tuning for Low Precision&#039;&#039;&#039; – Guides (e.g., NVIDIA or Intel whitepapers) on using low precision and the trade-offs involved in various hardware.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;references&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref1&amp;quot;&amp;gt;Accelerate and simplify Scikit-learn model inference with ONNX Runtime - Microsoft Open Source Blog — opensource.microsoft.com — https://opensource.microsoft.com/blog/2020/12/17/accelerate-simplify-scikit-learn-model-inference-onnx-runtime&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref2&amp;quot;&amp;gt;Floating Point Precision: Understanding FP64, FP32, and FP16 in Large Language Models - DEV Community — dev.to — https://dev.to/lukehinds/floating-point-precision-understanding-fp64-fp32-and-fp16-in-large-language-models-3gk6&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref3&amp;quot;&amp;gt;Floating Point Precision and Its Limitations | by Umair Akbar | Medium — akbu.medium.com — https://akbu.medium.com/floating-point-precision-and-its-limitations-cfb7247d7789&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref4&amp;quot;&amp;gt;Kahan summation algorithm - Wikipedia — en.wikipedia.org — https://en.wikipedia.org/wiki/Kahan_summation_algorithm&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref5&amp;quot;&amp;gt;Pairwise summation - Wikipedia — en.wikipedia.org — https://en.wikipedia.org/wiki/Pairwise_summation&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref6&amp;quot;&amp;gt;Numerical behavior of NVIDIA tensor cores - PMC — pmc.ncbi.nlm.nih.gov — https://pmc.ncbi.nlm.nih.gov/articles/PMC7959640/&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref7&amp;quot;&amp;gt;Mixed precision and performance-accuracy tuning (neuron-cc) — AWS Neuron Documentation — awsdocs-neuron.readthedocs-hosted.com — https://awsdocs-neuron.readthedocs-hosted.com/en/latest/about-neuron/appnotes/neuron-cc/mixed-precision.html&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref8&amp;quot;&amp;gt;Create Float16 and Mixed Precision Models - ONNX Runtime — onnxruntime.ai — https://onnxruntime.ai/docs/performance/model-optimizations/float16.html&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref9&amp;quot;&amp;gt;[Performance] ONNX FP16 model is having performance bottle neck when compared to FP32 variant · Issue #25824 · microsoft/onnxruntime · GitHub — github.com — https://github.com/microsoft/onnxruntime/issues/25824&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref10&amp;quot;&amp;gt;Float stored in 8 bits - ONNX 1.21.0 documentation — onnx.ai — https://onnx.ai/onnx/technical/float8.html&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref11&amp;quot;&amp;gt;[2209.05433] FP8 Formats for Deep Learning — arxiv.org — https://arxiv.org/abs/2209.05433&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref12&amp;quot;&amp;gt;Should MatMul op allow specification of accumulation precision? · Issue #7072 · onnx/onnx · GitHub — github.com — https://github.com/onnx/onnx/issues/7072&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref13&amp;quot;&amp;gt;Quantize ONNX models | onnxruntime — onnxruntime.ai — https://onnxruntime.ai/docs/performance/model-optimizations/quantization.html&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref14&amp;quot;&amp;gt;Keep Attention Softmax FP32 during FP16/ZeRO Training · Issue #1485 · hpcaitech/ColossalAI · GitHub — github.com — https://github.com/hpcaitech/ColossalAI/issues/1485&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;/references&amp;gt;&lt;/div&gt;</summary>
		<author><name>Timo.stripf</name></author>
	</entry>
	<entry>
		<id>https://www.emmtrix.com/w139/index.php?title=Numerical_Precision_in_ONNX_and_AI_Inference&amp;diff=2900</id>
		<title>Numerical Precision in ONNX and AI Inference</title>
		<link rel="alternate" type="text/html" href="https://www.emmtrix.com/w139/index.php?title=Numerical_Precision_in_ONNX_and_AI_Inference&amp;diff=2900"/>
		<updated>2026-01-30T15:24:01Z</updated>

		<summary type="html">&lt;p&gt;Timo.stripf: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&#039;&#039;&#039;Open Neural Network Exchange (ONNX)&#039;&#039;&#039; is an open standard format for representing machine learning models and neural network computations across different frameworks and hardware&amp;lt;ref name=&amp;quot;ref1&amp;quot;/&amp;gt;. As models are exported and deployed via ONNX, the &#039;&#039;&#039;numerical precision&#039;&#039;&#039; of computations becomes critical. Deep learning inference involves a variety of floating-point operations, and small numerical discrepancies can accumulate and affect model accuracy or reproducibility. This article provides an in-depth overview of floating-point precision issues in numerical computing, with a focus on how they manifest in AI inference and the ONNX ecosystem. We cover floating-point formats and rounding behavior, accumulation strategies for summations, the use of low-precision formats (FP16, BFloat16, FP8) in modern AI, and how ONNX defines (or leaves unspecified) the precision of accumulators in operations. Strategies for maintaining numerical stability in deep neural networks are discussed, along with the trade-offs between computational performance and arithmetic accuracy. The goal is to give engineers and researchers a comprehensive, encyclopedic reference on numerical precision in ONNX-based AI inference systems.&lt;br /&gt;
&lt;br /&gt;
== Floating-Point Precision in Numerical Computing ==&lt;br /&gt;
&lt;br /&gt;
=== 2.1. IEEE 754 Floating-Point Formats ===&lt;br /&gt;
&lt;br /&gt;
Modern computers represent real numbers using the IEEE 754 floating-point standard, which defines formats like 32-bit &#039;&#039;single precision&#039;&#039; (FP32) and 64-bit &#039;&#039;double precision&#039;&#039; (FP64). In IEEE 754 binary formats, a floating-point number is composed of three fields: 1 sign bit, several exponent bits (with a bias), and several fraction (mantissa) bits. For example, FP32 consists of 1 sign bit, 8 exponent bits, and 23 fraction bits (24 bits of significand precision including the implicit leading 1)&amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;. This provides roughly 7 decimal digits of precision and an exponent range allowing values on the order of $10^{\pm38}$. FP64 allocates 1 sign bit, 11 exponent bits, and 52 fraction bits (53-bit precision), yielding about 15–16 decimal digits of precision and a vastly larger range (~$10^{\pm308}$)&amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;. These larger formats (especially FP64) are the &amp;quot;gold standard&amp;quot; for numerical accuracy in scientific computing, as they reduce round-off error in complex calculations&amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;. However, the increased precision comes at the cost of doubled storage and typically reduced computational throughput (e.g. many GPUs execute FP64 instructions at a much lower rate than FP32&amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
In addition to single and double precision, the IEEE 754-2008 standard introduced a 16-bit &#039;&#039;half precision&#039;&#039; format (FP16, or binary16). FP16 has 1 sign bit, 5 exponent bits, and 10 fraction bits (11-bit significand including the implicit 1)&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. This format covers a much smaller dynamic range (approximately $6.1\times10^{-5}$ to $6.5\times10^4$ for normalized values&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;) and offers only around 3 decimal digits of precision. Half precision was originally used in graphics, but it has gained popularity in machine learning for its speed and memory advantages (discussed later in Section 4). Another format relevant to AI is &#039;&#039;&#039;BFloat16&#039;&#039;&#039; (Brain Float 16), a 16-bit float with &#039;&#039;&#039;8 exponent bits and 7 fraction bits&#039;&#039;&#039;&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. BFloat16 sacrifices precision (only ~7 fraction bits ≈ 2–3 decimal digits) in order to have the &#039;&#039;&#039;same exponent range as FP32&#039;&#039;&#039;&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. This means BFloat16 can represent very large or very small numbers (∼$10^{\pm38}$, similar range as FP32) but with much less precision between representable values&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. BFloat16 was introduced for deep learning by Google (for TPUs) to allow reduced precision &#039;&#039;training&#039;&#039; without frequent overflow/underflow, relying on the wide range and accepting the coarse precision.&lt;br /&gt;
&lt;br /&gt;
Each floating-point format has a &#039;&#039;&#039;unit roundoff&#039;&#039;&#039; or machine precision, denoted $\epsilon$. This is effectively the spacing between 1.0 and the next representable number. For FP32, $\epsilon \approx 2^{-23} \approx 1.19\times10^{-7}$, corresponding to about 7–8 decimal digits of precision&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. In FP64, $\epsilon = 2^{-52} \approx 2.22\times10^{-16}$ (15–16 decimal digits). Half precision is much less precise: FP16 has $\epsilon = 2^{-10} \approx 9.76\times10^{-4}$&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;, and BFloat16 has $\epsilon = 2^{-7} = 7.8125\times10^{-3}$&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. These values quantify how finely the continuum of real numbers is quantized in each format.&lt;br /&gt;
&lt;br /&gt;
=== 2.2. Machine Epsilon and Rounding Error ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Machine epsilon ($\varepsilon$)&#039;&#039;&#039; is formally defined as the smallest positive number such that $1.0 + \varepsilon &amp;gt; 1.0$ in the floating-point format&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. It effectively measures the &#039;&#039;&#039;precision&#039;&#039;&#039; of the representation. In FP32, $\varepsilon = 2^{-23} \approx 1.19\times10^{-7}$, meaning any result is rounded to about 7 decimal digits. FP16’s $\varepsilon = 2^{-10} \approx 9.77\times10^{-4}$, over 1000× larger (so only ~3 decimal digits of precision), and BFloat16’s $\varepsilon = 2^{-7} \approx 7.8\times10^{-3}$, reflecting its mere ~8-bit precision&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. Rounding in IEEE 754 by default uses &#039;&#039;&#039;round-to-nearest (ties to even)&#039;&#039;&#039;, which ensures the rounding error for any single operation is at most half a unit in the last place (0.5 ULP). We can model a floating-point addition as: &lt;br /&gt;
&lt;br /&gt;
$$\text{fl}(a+b) = (a+b)\,(1 + \delta),$$ &lt;br /&gt;
&lt;br /&gt;
where $|\delta| \le \varepsilon$ for that format&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. Here $\varepsilon/2$ is the maximum &#039;&#039;&#039;relative error&#039;&#039;&#039; in one rounding (also called unit roundoff). For example, when adding two FP32 numbers, the result is accurate to within about $10^{-7}$ of the true sum (relative), whereas in BF16, the result can deviate by up to $\sim7.8\times10^{-3}$ (nearly 0.78% relative error per operation)&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. Thus, &#039;&#039;&#039;each arithmetic operation incurs a small rounding error&#039;&#039;&#039; (on the order of machine epsilon) that can propagate or accumulate through a sequence of calculations.&lt;br /&gt;
&lt;br /&gt;
Because floating-point representations have finite exponent range, we must also consider &#039;&#039;&#039;overflow&#039;&#039;&#039; and &#039;&#039;&#039;underflow&#039;&#039;&#039;. If a result’s magnitude exceeds the maximum representable value, it overflows to infinity; if it falls below the minimum normal (or minimum subnormal) value, it underflows to zero (or a denormalized tiny value). FP32 and BF16 share an exponent size (8 bits), so their range for normal numbers is roughly $[10^{-38}, 10^{38}]$ (approx $~\!3.4\times10^{38}$ max)&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. FP16’s 5-bit exponent allows max $\sim 6.55\times10^4$ and min normal $\sim 6.10\times10^{-5}$&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. In practice, &#039;&#039;&#039;underflow/overflow&#039;&#039;&#039; can occur in low-precision arithmetic if values are not appropriately scaled. For instance, summing a large number of moderately sized terms in FP16 could overflow where FP32 would not. As an example, summing on the order of $10^4$ values of magnitude ~$10^1$ (say, adding 10,000 terms around 10 each) would produce a total $\sim10^5$, which exceeds FP16’s max finite value ~$6.5\times10^4$ and would overflow to $+\infty$ in half precision&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. Underflow is usually a concern when subtracting nearly equal numbers (catastrophic cancellation yielding a tiny difference that might flush to zero in low precision).&lt;br /&gt;
&lt;br /&gt;
=== 2.3. Accumulation Error in Reductions ===&lt;br /&gt;
&lt;br /&gt;
Floating-point addition is &#039;&#039;&#039;not associative&#039;&#039;&#039;: $(a+b)+c$ can differ from $a+(b+c)$ due to rounding, and thus the order in which we sum a list of numbers can change the result slightly. When summing a large sequence of $N$ numbers $x_1, x_2, ..., x_N$, the &#039;&#039;&#039;accumulated rounding error&#039;&#039;&#039; can be significant. In the &#039;&#039;worst case&#039;&#039; (e.g., adding many positive and negative terms that cause maximal cancellation or always rounding one way), the error can grow on the order of $O(N \varepsilon)$ relative to the exact sum&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;. Simply summing $N$ numbers in sequence (the &#039;&#039;&#039;naive linear summation&#039;&#039;&#039;) has a worst-case error bound proportional to $N\,\varepsilon$&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. This worst-case occurs for adversarial arrangements of values; in more typical scenarios with rounding errors of random signs, one can model the error as a random walk, yielding an expected root-mean-square (RMS) error growth on the order of $O(\sqrt{N}\,\varepsilon)$&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;. Still, with very large $N$, even $\sqrt{N}$ times $\varepsilon$ can become large enough to matter.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Cancellation&#039;&#039;&#039; is a particular problem: if we add two nearly equal numbers of opposite sign, most leading digits cancel and the result loses significant bits of precision. For example, in single precision, subtracting two numbers that agree in the first 6 decimal digits will yield a result with only ~1 digit of accuracy left. Catastrophic cancellation can dramatically increase relative error. Summation is especially sensitive to cancellation if large positive and negative terms are summed naively. The &#039;&#039;&#039;condition number&#039;&#039;&#039; of a summation (the ratio of the sum of absolute values $\sum|x_i|$ to the absolute value of the true sum $|\sum x_i|$) indicates how sensitive the result is to perturbations&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;. If the numbers have mixed signs and nearly cancel out (making the condition number large), even an optimal summation order will have a large relative error. For instance, summing [1.0, $10^{100}$, 1.0, $-10^{100}$] in double precision has an exact sum of 2.0, but naive summation may yield 0.0 due to cancellation; an improved summation algorithm yields the correct 2.0&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
In practical terms, when summing a long list of floating-point numbers (such as accumulating a dot product or reducing a tensor), the &#039;&#039;&#039;accumulation error&#039;&#039;&#039; can become noticeable if no precautions are taken. Techniques to mitigate this error include carefully choosing the summation order or using higher precision for intermediate sums. We will explore these strategies in the next section.&lt;br /&gt;
&lt;br /&gt;
== Accumulators and Reduction Strategies ==&lt;br /&gt;
&lt;br /&gt;
When performing reductions (sums, dot products, etc.), the method of accumulation can greatly affect the accuracy of the result. Below, we discuss various strategies:&lt;br /&gt;
&lt;br /&gt;
=== 3.1. Naive Linear Accumulation ===&lt;br /&gt;
&lt;br /&gt;
The simplest accumulation strategy is to use a single accumulator and add each element sequentially. In C-like pseudocode:&lt;br /&gt;
&lt;br /&gt;
This &#039;&#039;&#039;naive linear summation&#039;&#039;&#039; adds values one by one to the running total. While straightforward and fast, it is prone to the rounding error accumulation described earlier. The partial sum &amp;lt;code&amp;gt;sum&amp;lt;/code&amp;gt; continually grows (in absolute value) as terms are added, so new small terms may fall outside its precision. For example, if &amp;lt;code&amp;gt;sum&amp;lt;/code&amp;gt; is large and positive, adding a comparatively tiny positive number may produce no change because the small term&#039;s significant digits vanish in the presence of the large sum (the increment is below the rounding threshold of &amp;lt;code&amp;gt;sum&amp;lt;/code&amp;gt;). Conversely, adding a small number of opposite sign can suffer from cancellation. The order of addition matters: summing from smallest magnitude to largest (ascending order) tends to yield a more accurate result than summing in descending order, because adding small terms first preserves their contributions before the sum becomes very large. The naive loop above implicitly sums in the given index order; if the data is unsorted, this order is essentially arbitrary.&lt;br /&gt;
&lt;br /&gt;
The performance of linear accumulation is excellent (minimal overhead), and for many applications the resulting error is within acceptable limits. However, for &#039;&#039;&#039;very large N&#039;&#039;&#039; or ill-conditioned sums, the inaccuracy can become a problem. In a reduction involving millions of elements, a single-precision sum could lose several digits of accuracy. In deep learning, this could translate to small discrepancies in layer outputs or aggregated gradients if done naively in low precision.&lt;br /&gt;
&lt;br /&gt;
=== 3.2. Pairwise and Tree-Based Reductions ===&lt;br /&gt;
&lt;br /&gt;
A better approach to summation is &#039;&#039;&#039;pairwise summation&#039;&#039;&#039;, which sums numbers in a binary tree structure rather than linearly. The idea is to recursively split the array into halves, sum each half, then add the two partial sums. This can be implemented as:&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
By always adding numbers of similar magnitudes first (small subsets are reduced to subtotals, then those are added, etc.), pairwise summation avoids the situation of a very large running total absorbing small additions until the final steps. The error of pairwise summation grows logarithmically with $N$ in the worst case (roughly $O(\varepsilon \log_2 N)$)&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;, instead of linearly. Intuitively, at each level of the tree, rounding error is constrained, and there are about $\log_2 N$ levels. In fact, the worst-case relative error bound for pairwise summation (base case of 1 element) is:&lt;br /&gt;
&lt;br /&gt;
\[ |E_N| \le \frac{\varepsilon \log_2 N}{1 - \varepsilon \log_2 N} \sum_{i=1}^N |x_i|, \]&lt;br /&gt;
&lt;br /&gt;
which for practical $N$ simplifies to on the order of $\varepsilon \log_2 N$ times the condition number of the sum&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;. In typical random-error scenarios, the error behaves even better (on the order of $\sqrt{\log N}$)&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;. The key is that &#039;&#039;&#039;pairwise (tree) reduction balances the accumulations&#039;&#039;&#039;, preventing one sum from overwhelming the others too early.&lt;br /&gt;
&lt;br /&gt;
From a performance standpoint, pairwise summation has the same $O(N)$ total additions as linear summation, just a different order. It is also highly amenable to parallelization: independent partial sums can be computed in parallel at each level of the tree. Many numerical libraries and BLAS implementations use a form of blocked or pairwise summation in dot products for improved accuracy at negligible performance cost. For example, NumPy and Julia perform partial pairwise summation by default for better numerical behavior&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=== 3.3. Compensated Summation (Kahan, Neumaier) ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Compensated summation&#039;&#039;&#039; is an alternative technique to improve summation accuracy by tracking the small “lost” quantities. The most famous algorithm is the &#039;&#039;&#039;Kahan summation algorithm&#039;&#039;&#039; (also called Kahan–Babuška)&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. Kahan summation uses an extra variable to accumulate the round-off error. In pseudocode:&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Here &amp;lt;code&amp;gt;c&amp;lt;/code&amp;gt; accumulates the tiny leftovers that would otherwise be dropped&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. Each iteration essentially subtracts the last error from the new addend (&amp;lt;code&amp;gt;y = x[i] - c&amp;lt;/code&amp;gt;) so that &amp;lt;code&amp;gt;t = sum + y&amp;lt;/code&amp;gt; includes the small bits from the previous operation. The new error is then stored in &amp;lt;code&amp;gt;c&amp;lt;/code&amp;gt; for the next iteration. The result is that the final &amp;lt;code&amp;gt;sum&amp;lt;/code&amp;gt; is as if computed with higher precision than the machine’s: Kahan’s algorithm can &#039;&#039;&#039;greatly reduce the error&#039;&#039;&#039; in summing a sequence of floats. In fact, with exact arithmetic for the compensation, Kahan summation yields a result accurate to within 1 or 2 ULP of the true sum, regardless of N (assuming the sequence of partial sums doesn’t overflow)&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. More precisely, Kahan summation achieves a &#039;&#039;&#039;worst-case error independent of N&#039;&#039;&#039; (error bound depends only on machine precision, not the number of terms)&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. In practice, using a double accumulator for summing single-precision numbers via Kahan can give near double-precision accuracy.&lt;br /&gt;
&lt;br /&gt;
An important subtlety is that the compensation variable &amp;lt;code&amp;gt;c&amp;lt;/code&amp;gt; should be kept in &#039;&#039;full precision&#039;&#039;. In the implementation above, we used &amp;lt;code&amp;gt;double&amp;lt;/code&amp;gt; for both sum and c. If summing FP32 values, one might use &amp;lt;code&amp;gt;float sum&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;float c&amp;lt;/code&amp;gt; (both 32-bit), which still improves accuracy, but using a higher precision for the accumulator and compensation (i.e. double) yields even better results. Many implementations of Kahan summation use the same type for sum and c, but it&#039;s assumed to be higher precision than the inputs when possible.&lt;br /&gt;
&lt;br /&gt;
Kahan’s algorithm carries a performance cost: each iteration does five arithmetic operations (two adds, one subtract, etc.) and has a data dependency, which can hinder parallelism or pipelining&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. Thus, it runs slower than naive summation (roughly 2–3× slower or more, depending on CPU pipeline). In loops where summation is a bottleneck, this may be significant. Nonetheless, for modest N or where maximum accuracy is required, compensated summation is extremely useful. In fact, Python’s &amp;lt;code&amp;gt;math.fsum&amp;lt;/code&amp;gt; function uses a variant of Kahan’s algorithm (actually a more advanced multi-part compensation) to sum iterables with high precision.&lt;br /&gt;
&lt;br /&gt;
There are variations and improvements on basic Kahan summation. &#039;&#039;&#039;Neumaier’s algorithm&#039;&#039;&#039; (sometimes called &#039;&#039;Kahan-Babuška-Neumaier&#039;&#039;) improves Kahan’s method by also handling cases where the next input is larger in magnitude than the running sum&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. In such cases, the original Kahan formula could suffer error; Neumaier’s version effectively swaps roles when a huge value comes in. Its pseudocode is:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;text&amp;quot;&amp;gt;&lt;br /&gt;
xyz&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This algorithm ensures the compensation covers both scenarios: when the sum is the bigger addend or when the new term is bigger&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. In the end, it adds the final &amp;lt;code&amp;gt;compensation&amp;lt;/code&amp;gt; once to the result. Neumaier’s method can produce the correct result in cases where basic Kahan fails. For instance, summing [1.0, $+10^{100}$, 1.0, $-10^{100}$] in double precision: regular Kahan summation might give 0.0 (losing the small terms), whereas Neumaier’s algorithm yields 2.0 exactly&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. There are also higher-order compensation methods (carrying multiple compensation terms, e.g., the &#039;&#039;&#039;Klein summation&#039;&#039;&#039; variant&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt; or using arbitrary precision for intermediate sums) that further reduce error at increased computational cost&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
In summary, compensated summation algorithms trade extra computation for greatly improved numerical accuracy. They are especially valuable when summing large arrays of numbers with varying magnitudes, where naive summation could lose most of the low-order bits. Many scientific computing environments provide compensated sum routines for this reason. In deep learning, however, a different approach to improved precision is often taken: &#039;&#039;&#039;using a wider accumulator type&#039;&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
=== 3.4. Mixed-Precision Accumulation ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Mixed-precision accumulation&#039;&#039;&#039; refers to performing arithmetic in a higher precision than the input data. A common scenario is accumulating sums in 32-bit floats when the operands are 16-bit floats. Rather than implementing a compensation algorithm, one can simply use a wider accumulator so that intermediate rounding error is smaller. This is effectively what modern GPUs do for tensor operations: for example, NVIDIA &#039;&#039;&#039;Tensor Cores operate on FP16 input data with an FP32 accumulator&#039;&#039;&#039; (the 16-bit products are accumulated in 32-bit)&amp;lt;ref name=&amp;quot;ref6&amp;quot;/&amp;gt;. In this way, summing 2048 products in a dot product has far less precision loss than it would with a 16-bit accumulator. Hardware that supports mixed precision can give the best of both worlds: fast low-precision multiplies and high-precision accumulation.&lt;br /&gt;
&lt;br /&gt;
Software can also leverage mixed precision. For instance, one can sum an array of &amp;lt;code&amp;gt;float&amp;lt;/code&amp;gt; values into a &amp;lt;code&amp;gt;double&amp;lt;/code&amp;gt; variable, accumulating the result in double precision and then converting back to float at the end:&lt;br /&gt;
&lt;br /&gt;
This approach often yields a more accurate &amp;lt;code&amp;gt;result&amp;lt;/code&amp;gt; than summing in single precision directly, because the intermediate sum carries 52-bit precision. The final rounding to float happens only once at the end (minimizing overall rounding error). In fact, the above is one way to implement &amp;lt;code&amp;gt;math.fsum&amp;lt;/code&amp;gt; in Python (it internally does something similar with multiple partial sums in double precision).&lt;br /&gt;
&lt;br /&gt;
In machine learning training and inference, &#039;&#039;&#039;mixed precision&#039;&#039;&#039; is widely used. When using FP16 for speed, it&#039;s common to accumulate with FP32. For example, &#039;&#039;&#039;Google&#039;s TPUs use BFloat16 for multiplication but accumulate in FP32&#039;&#039;&#039; for matrix ops&amp;lt;ref name=&amp;quot;ref6&amp;quot;/&amp;gt;. AWS’s Inferentia (Neuron) hardware similarly supports FP16/BF16 matrix multiplication with FP32 accumulation&amp;lt;ref name=&amp;quot;ref7&amp;quot;/&amp;gt;. The motivation is that summing hundreds or thousands of half-precision values would otherwise introduce intolerable error or risk overflow. With an FP32 accumulator, the reduction error is drastically reduced (by about $2^{13}$ times, since FP32 has 13 more precision bits than FP16). Mixed-precision accumulation thus provides a simple and effective form of compensation: &#039;&#039;&#039;use a larger bucket to collect many small contributions&#039;&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
There is a spectrum of options: summing int8 or int16 integers might use 32-bit integers as accumulators for the same reason (to avoid overflow). Some BLAS libraries even offer &#039;&#039;extended precision accumulators&#039;&#039; for single precision dot products (accumulating in double). The downside of mixed precision is the extra memory and maybe throughput cost of the wider type. But on many platforms, the add latency is dominated by hardware design rather than bit-width, so using a 32-bit adder vs 16-bit has negligible speed impact if supported natively. The key is whether data movement or register pressure increases.&lt;br /&gt;
&lt;br /&gt;
In summary, &#039;&#039;&#039;using a higher precision accumulator is often the easiest way to improve numerical accuracy&#039;&#039;&#039; for reductions. It is a primary strategy in deep learning hardware to maintain stability while exploiting low precision for storage and initial computations.&lt;br /&gt;
&lt;br /&gt;
== Precision Formats in AI and Machine Learning ==&lt;br /&gt;
&lt;br /&gt;
Modern AI systems use a variety of numeric formats to balance speed, memory, and accuracy. We discuss common precisions and their roles:&lt;br /&gt;
&lt;br /&gt;
=== 4.1. FP32 and FP64 in Training and Inference ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;FP32 (single precision)&#039;&#039;&#039; has been the workhorse of deep learning for years. Most training scripts in frameworks like TensorFlow and PyTorch used FP32 by default, as it offers sufficient precision to propagate gradients and accumulate weight updates over millions of iterations without excessive error. With ~7 decimal digits of precision and a wide exponent range, FP32 can handle the dynamic range of activations and gradients in typical neural networks. In inference, FP32 is often more precision than strictly necessary for final predictions, but it has remained common because of its ease and the fact that many GPU operations were optimized for FP32.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;FP64 (double precision)&#039;&#039;&#039; is rarely used in deep learning model training or inference, except in specific scenarios requiring extreme precision. FP64 is common in scientific computing and certain types of numerical simulations. In neural networks, however, the extra precision is usually not needed for model accuracy—using doubles tends to yield negligible improvements in model metrics but at a large performance cost (since on GPUs FP64 throughput can be 1/2, 1/8, or even 1/32 of FP32 throughput depending on hardware). One exception might be when integrating neural nets into larger physics simulations or when solving ill-conditioned problems as part of the model. Some researchers use FP64 to diagnose numerical issues or ensure reproducibility. Generally though, deep learning has favored lower precision for efficiency, and FP64 is considered overkill for most networks&amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
That said, certain classical machine learning algorithms benefit from double precision. For example, computing the inverse of a matrix (as in a Gaussian Process regression or Kalman filter embedded in a model) can incur large rounding errors in single precision, so using FP64 can significantly improve the result&amp;lt;ref name=&amp;quot;ref1&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref1&amp;quot;/&amp;gt;. The ONNX standard supports a float64 tensor type for such needs. In ONNX Runtime, however, emphasis has been on FP32 performance, and some pipelines default to float even if the original model was double&amp;lt;ref name=&amp;quot;ref1&amp;quot;/&amp;gt;. If a model’s prediction computation involves operations that are numerically sensitive (like matrix inversion, Cholesky decomposition, or summing extremely large counts), then FP64 may be required to avoid noticeable discrepancies&amp;lt;ref name=&amp;quot;ref1&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref1&amp;quot;/&amp;gt;. A good practice is to test a model in both float and double if you suspect precision issues; significant differences indicate an ill-conditioned calculation that might need higher precision.&lt;br /&gt;
&lt;br /&gt;
In summary, FP32 remains the &#039;&#039;&#039;default&#039;&#039;&#039; format for most training and inference due to its balance of accuracy and efficiency&amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;, while FP64 is reserved for niche cases or precision-critical subroutines. FP32 typically achieves the same accuracy as FP64 for end-to-end deep learning tasks, because network training can often compensate for small noise, and other sources of error (like generalization error) dominate. Therefore, the community trend has been moving to &#039;&#039;&#039;lower&#039;&#039;&#039; precisions, not higher, to gain speed — provided the models still converge and perform well.&lt;br /&gt;
&lt;br /&gt;
=== 4.2. FP16 and BFloat16 in Deep Learning ===&lt;br /&gt;
&lt;br /&gt;
To further speed up neural network training and inference, &#039;&#039;&#039;half-precision (FP16)&#039;&#039;&#039; was adopted, especially with the advent of GPUs supporting FP16 math at twice the rate of FP32. Using FP16 reduces memory storage and bandwidth by 50% compared to FP32, which is significant for large models. However, FP16’s small exponent range and precision posed challenges: training a large network purely in FP16 can fail due to gradient underflow/overflow or weight updates not registering (because changes are below the 0.001 quantization step). To address this, frameworks introduced &#039;&#039;&#039;mixed-precision training&#039;&#039;&#039;, where compute-intensive parts (matrix multiplies, convolutions) run in FP16, but certain accumulations and model parameters remain in FP32. For instance, NVIDIA’s &#039;&#039;&#039;Tensor Core&#039;&#039;&#039; units require FP16 (or BF16) inputs but accumulate results in FP32, effectively ensuring that reductions are done in higher precision&amp;lt;ref name=&amp;quot;ref6&amp;quot;/&amp;gt;. Research showed that with proper loss scaling and mixed precision, models can train to FP32-level accuracy using mostly FP16 arithmetic, achieving significant speedups.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;BFloat16 (BF16)&#039;&#039;&#039; emerged as an alternative 16-bit format primarily from Google’s TPU training systems. BF16 has a larger exponent (8 bits, same as FP32) but only 7 fraction bits&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. This means BF16 can handle the &#039;&#039;&#039;same dynamic range&#039;&#039;&#039; as FP32 (no overflows where FP32 wouldn’t) but at the cost of even lower precision than FP16 (BF16 has about 2–3 decimal digits of precision vs FP16’s ~3–4). Despite its coarse precision, BF16 turns out to be effective for training deep networks because the algorithms (stochastic gradient descent, etc.) are somewhat robust to that noise, and having the wide range prevents catastrophic loss of signal (no gradient will overflow to Inf unless it was going to in FP32 anyway). Many training frameworks (PyTorch, TensorFlow) support BF16 training on hardware like &#039;&#039;&#039;TPU, NVIDIA A100+, Intel CPUs with AVX-512 BF16, etc&#039;&#039;&#039;. Typically, when using BF16, &#039;&#039;&#039;accumulations are still done in FP32&#039;&#039;&#039; (for example, a TPU v2/v3 multiplies BF16 matrices and accumulates into an FP32 result for the dot product&amp;lt;ref name=&amp;quot;ref6&amp;quot;/&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
In inference, both FP16 and BF16 are used to deploy models for faster speed or lower memory. FP16 is popular on NVIDIA GPUs (which have specialized FP16 units). BF16 is advantageous on TPUs and some CPUs that support it, and increasingly on GPUs (Ampere and newer GPUs also support BF16 arithmetic). The choice between FP16 and BF16 can depend on hardware: BF16’s advantage is easier software conversion (you can often take an FP32-trained model and cast weights to BF16 with minimal loss, since the dynamic range is intact), whereas FP16 might require adjusting the model or ensuring values fit in range (sometimes needing techniques like maintaining a master copy of weights in FP32 during training, or handling out-of-range values).&lt;br /&gt;
&lt;br /&gt;
For ONNX, both Float16 and BFloat16 types are part of the standard. ONNX models can be converted to FP16 (truncating weights and inserting cast nodes for computations) using tools like &amp;lt;code&amp;gt;float16_converter&amp;lt;/code&amp;gt; in ONNX Runtime&amp;lt;ref name=&amp;quot;ref8&amp;quot;/&amp;gt;. ONNX Runtime on GPUs will typically execute those FP16 ops with mixed precision on tensor cores (FP16 input, FP32 accumulate), whereas on CPU it may emulate FP16 or upcast to FP32 (as many CPU implementations do not natively support half-precision math)&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;. In fact, as an example, running an ONNX model in FP16 on a CPU might insert hidden Cast operations to FP32 behind the scenes, because the CPU kernel for that op only exists in FP32&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;. This ensures correctness (the computation is done in higher precision) but can reduce performance. We will revisit such backend-dependent behavior in Section 5.3.&lt;br /&gt;
&lt;br /&gt;
In summary, &#039;&#039;&#039;FP16 and BF16&#039;&#039;&#039; are now established as important low-precision formats in deep learning. FP16 offers more precision bits, which can matter for e.g. small differences, but has a limited range (care needed to avoid overflow). BF16 offers safety in range but less precision (which can slightly affect convergence or final accuracy in some cases). Both typically rely on FP32 for accumulation to achieve acceptable accuracy. When deploying models, one should choose a format supported efficiently by the target hardware and verify that the accuracy is not significantly degraded. Often, CNNs and transformers can run in FP16/BF16 with virtually no drop in score, but certain operations (like softmax, see Section 6.3) might need special handling.&lt;br /&gt;
&lt;br /&gt;
=== 4.3. FP8 Formats (E4M3, E5M2) and Scaling ===&lt;br /&gt;
&lt;br /&gt;
The push for ever lower precision has led to experimental &#039;&#039;&#039;8-bit floating point (FP8)&#039;&#039;&#039; formats for deep learning. In 2022, researchers from NVIDIA, Intel, and Arm proposed an FP8 standard with two variants: &#039;&#039;&#039;E4M3&#039;&#039;&#039; (4 exponent bits, 3 fraction bits) and &#039;&#039;&#039;E5M2&#039;&#039;&#039; (5 exponent bits, 2 fraction bits)&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;. Both have 1 sign bit, for a total of 8 bits. E5M2 has a wider exponent range (5 bits gives bias 15) but one fewer mantissa bit than E4M3. The idea is to use E4M3 for numbers that need more precision (like weights or activations around 1.0) and E5M2 for numbers that need more dynamic range (like gradient updates which can be very small or large). In fact, the study found that using E4M3 for forward activations/weights and E5M2 for backward gradients can allow training of large networks with FP8 to accuracy on par with FP16 training&amp;lt;ref name=&amp;quot;ref11&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
FP8 formats are &#039;&#039;not&#039;&#039; IEEE 754 standard (though IEEE is considering lower precision in its 2019 revision). Notably, the E4M3 format as used by NVIDIA does not allocate any bit-pattern for $+\infty$ or $-\infty$ – it has only NaN for invalid results, using that freed bit pattern to extend the representable range by one exponent value&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;. Essentially, E4M3 can represent numbers up to ~$2^{15}$ (around 3e4) instead of capping at infinity at $2^{16}$, by foregoing infinities&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;. Variations also exist regarding negative zero and how NaNs are handled&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;. For instance, GraphCore’s IPU uses FP8 formats that disallow negative zero and infinities, to maximize useable range&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Using 8-bit floats for neural networks requires additional techniques, chiefly &#039;&#039;&#039;scaling&#039;&#039;&#039;. Because 8-bit has very limited precision and range, one typically applies a power-of-two scale factor to groups of values to normalize them into an FP8-friendly range. This is akin to block floating-point or “microscaling” per layer or per tensor. For example, one might keep a FP32 scale for each channel or tensor, and represent values as $(FP8\_value) \times 2^{scale}$. The ONNX standard has introduced support for FP8 in version 1.15, including types for E4M3 and E5M2 (with variations FNUZ: finite-only and no negative zero)&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;, and even a type called &#039;&#039;&#039;E8M0&#039;&#039;&#039; which is an 8-bit exponent-only number used purely as a scale factor&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;. The E8M0 type has 8 exponent bits, no mantissa, effectively representing powers of two from $2^{-127}$ to $2^{128}$ for scaling purposes&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;. This indicates an approach where an FP8 tensor would be accompanied by an E8M0 scale (or scales) to interpret its value correctly.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Rounding&#039;&#039;&#039; becomes critical when casting values to FP8. The quantization error is much larger relative to FP16 or FP32. Studies have shown that choosing the right rounding mode (e.g., round-to-nearest, stochastic rounding, etc.) and clamping behavior (saturating to max instead of inf) can affect model accuracy&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;. ONNX added a &amp;lt;code&amp;gt;round_mode&amp;lt;/code&amp;gt; attribute to its Cast operator to allow controlling this for FP8&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;. In particular, one paper found that &#039;&#039;&#039;rounding up with saturation&#039;&#039;&#039; (round-to-nearest ties away from 0, and clamp overflows to max finite rather than inf) gave better accuracy in large language model training than other rounding modes&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
At present, FP8 is on the cutting edge. NVIDIA’s H100 GPU supports FP8 tensor cores (for both E4M3 and E5M2), and early results show minimal accuracy loss on some vision and NLP tasks when properly tuned. However, using FP8 requires fine-tuning hyperparameters like scaling factors for each layer and perhaps tweaking training procedures. ONNX’s inclusion of FP8 types means you can represent and transport models using FP8 weights or activations, but actual hardware support is limited and typically paired with calibration or dynamic scaling logic outside of ONNX graph (or using custom ONNX functions for scaling). It remains an &#039;&#039;&#039;open research area&#039;&#039;&#039; how to fully automate FP8 quantization for arbitrary models without accuracy loss.&lt;br /&gt;
&lt;br /&gt;
In summary, FP8 formats promise further compression of neural network computation and data. They are an order of magnitude more error-prone than FP16, but when combined with careful scaling, mixed usage of E4M3/E5M2, and maybe modified training techniques, they have shown surprising success. ONNX is actively evolving to support these ultra-low precision types, signaling their potential importance in future AI inference deployments.&lt;br /&gt;
&lt;br /&gt;
== Accumulation Semantics in ONNX ==&lt;br /&gt;
&lt;br /&gt;
One of the roles of the ONNX specification is to define the mathematical behavior of operators (Conv, MatMul, Sum, etc.) in a framework-agnostic way. However, ONNX mostly defines &#039;&#039;what&#039;&#039; an operator computes in exact arithmetic, not &#039;&#039;how&#039;&#039; the computation is carried out in finite precision. This leaves certain details – like accumulator precision or summation order – up to the implementation (runtime or hardware). We&#039;ll examine how ONNX treats accumulation and what guarantees (or lack thereof) are provided.&lt;br /&gt;
&lt;br /&gt;
=== 5.1. Operator Definitions and Numerical Guarantees ===&lt;br /&gt;
&lt;br /&gt;
Each ONNX operator is described as performing an ideal mathematical function on its inputs. For example, &amp;lt;code&amp;gt;MatMul&amp;lt;/code&amp;gt; is defined as the standard matrix product of two tensors, and &amp;lt;code&amp;gt;ReduceSum&amp;lt;/code&amp;gt; computes a summation along given axes. The ONNX spec, as of current versions, &#039;&#039;&#039;does not mandate the precision of intermediate calculations&#039;&#039;&#039; beyond the types of the inputs and outputs. If an operator’s inputs and outputs are float16, the spec implies the computation is done in float16 in a sense, but it does not forbid an implementation from using higher precision internally – it simply doesn&#039;t mention it. There is &#039;&#039;&#039;no explicit guarantee of identical bit-for-bit results&#039;&#039;&#039; across different ONNX backends for floating-point ops. Instead, results are expected to be numerically close, within tolerances one would normally expect from floating-point differences (due to different hardware or algorithms). The ONNX Backend Test suite typically uses a relative or absolute error tolerance when validating outputs for this reason.&lt;br /&gt;
&lt;br /&gt;
Because of this, important details like the use of extended precision or accumulation in higher precision are left ambiguous. This has led to discussions in the ONNX community about making such behavior more explicit. For example, a proposal was raised to allow specification of accumulation precision for certain ops like MatMul&amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;. The reasoning is that as lower-precision types (float16, bfloat16, float8) become more common, knowing or controlling whether an op internally accumulates in a higher precision is important for both accuracy and reproducibility&amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;. In June 2025, an issue on the ONNX GitHub suggested adding an attribute to MatMul (and similarly Gemm, Softmax, etc.) to specify the accumulator type&amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;. By default it would use input precision (status quo), but an attribute could request (for instance) 32-bit accumulation even if inputs are 16-bit&amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;. As of this writing, however, no such attribute exists in the official spec – it&#039;s an open issue. Thus, the numerical semantics are effectively: &#039;&#039;“compute the result as if in the same type as inputs, but actual implementations may use more precision internally as an optimization.”&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
From a guarantee standpoint, ONNX operators promise the mathematically correct result assuming real-number arithmetic, but they &#039;&#039;&#039;do not guarantee bit-exact results&#039;&#039;&#039; due to floating-point effects. There is an understanding that differences can arise from different summation orders, fused operations (e.g., an implementation might use an FMA (fused multiply-add) which has one rounding instead of two), or use of extended precision registers. ONNX does not formally define rounding modes either – it relies on IEEE 754 defaults, but if a backend used something like deterministic summation or alternative rounding, ONNX has no mechanism to express that.&lt;br /&gt;
&lt;br /&gt;
In summary, ONNX&#039;s current stance is largely &#039;&#039;&#039;implementation-dependent&#039;&#039;&#039; for numerical precision. It provides minimal explicit numerical guarantees beyond type constraints. This is why an ONNX model run on two different hardware or runtime libraries can have slightly different results, especially in reduced precision. The burden is on backends to produce results &amp;quot;close enough&amp;quot; to each other that they don&#039;t affect the model&#039;s decisions in any significant way.&lt;br /&gt;
&lt;br /&gt;
=== 5.2. MatMul, Conv, and Reduction Operators ===&lt;br /&gt;
&lt;br /&gt;
Consider ONNX &amp;lt;code&amp;gt;MatMul&amp;lt;/code&amp;gt;: it takes two tensors (often two matrices) and produces their matrix product. In pure math, $C_{ij} = \sum_k A_{ik} &#039;&#039; B_{kj}$. If A and B are float16, should that sum be computed in float16 or float32? As discussed, ONNX doesn’t specify – it simply says the output is a float16 matrix containing the product. In practice, many backends will &#039;&#039;promote&#039;&#039; the computation: for example, NVIDIA GPUs will do the multiplication and addition in a mixed FP16/FP32 mode by default (because of tensor cores), then convert the final result back to float16. ONNX Runtime on CPU might not support float16 multiplication natively, so it will insert cast ops to float32, perform the MatMul in float32, then cast back to float16&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;. From ONNX’s perspective, as long as the final result is within acceptable error of the “ideal” float16 computation, it’s fine. But note, an “ideal” float16 computation (where every multiply and add is rounded to FP16) might actually be &#039;&#039;less accurate* than what the backend did with FP32. This means that a backend using higher precision can produce a result that is not bit-equal to a pure FP16 implementation – it&#039;s actually closer to the true real number result. This is usually considered a good thing (improved accuracy), but it does highlight a lack of consistency: another backend might do everything in pure FP16 and get a slightly different answer.&lt;br /&gt;
&lt;br /&gt;
The same situation occurs for &#039;&#039;&#039;convolution&#039;&#039;&#039; (&amp;lt;code&amp;gt;Conv&amp;lt;/code&amp;gt; op) which is essentially a batched sum of products (like MatMul in sliding window), and for explicit reduction ops like &amp;lt;code&amp;gt;ReduceSum&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;ReduceMean&amp;lt;/code&amp;gt;, etc. If you have an ONNX &amp;lt;code&amp;gt;ReduceSum&amp;lt;/code&amp;gt; that sums 1000 float16 values, the spec doesn’t say how to sum them. A GPU kernel might sum in a tree using FP32 accumulators; a naive implementation might sum in order with FP16 and possibly overflow or underflow intermediate steps. Both are allowed as long as they output a float16 result that is “reasonable.” In practice, ONNX Runtime and others take care to avoid obviously bad behavior (like overflow) by using higher precision or by splitting the sum. But again, it’s not mandated by the ONNX standard explicitly.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Softmax&#039;&#039;&#039; is an interesting case: Softmax involves an internal sum of exponentials. ONNX defines the Softmax operator as $$\mathrm{Softmax}(x)_i = \frac{\exp(x_i)}{\sum_j \exp(x_j)},$$ typically applied along a specified axis. Directly computing this in float16 can be problematic for large vectors, as discussed in Section 6.3. Indeed, the issue of accumulation precision arises: should the sum $\sum_j \exp(x_j)$ be done in FP16 or can it be in FP32? Many implementations will do it in FP32 even if $x$ is FP16, because the dynamic range of $\exp(x)$ is large. The ONNX Softmax spec doesn’t detail this, but it expects the result to be a proper probability distribution (summing to 1.0 within normal floating error). A pure FP16 softmax on, say, 1000 elements could sum to something like 0.996 or 1.004 due to rounding, whereas FP32 accumulation would be much closer to 1.000. ONNX likely allows both; the small difference is usually inconsequential, but if it were large (say the FP16 sum overflowed to inf), that would be a correctness issue. A robust backend will prevent that (by using the common max-subtraction trick and higher precision accumulation). The ONNX issue 7072 indeed calls out Softmax as another op where explicit control of accumulation precision would be useful&amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
In summary, &#039;&#039;&#039;MatMul, Conv, and reduction ops&#039;&#039;&#039; in ONNX are defined mathematically but with &#039;&#039;implicit&#039;&#039; numerical behavior. Implementations commonly use higher precision for intermediate sums especially when input is low precision, but this is not standardized. Consequently, results can differ slightly between implementations. For critical applications, one must be aware of these differences. A model exported to ONNX with float16 weights might perform slightly differently on one runtime vs another if one does all float16 math and the other uses float32 accumulations. Both conform to ONNX. As a user, if you require a certain precision for accumulation, currently you have to ensure it by the way you export or run the model (for example, insert manual cast nodes in the ONNX graph to force certain ops to FP32). We may see future ONNX versions address this with attributes or metadata as mentioned, but for now these ops are effectively “compute with best effort accuracy”.&lt;br /&gt;
&lt;br /&gt;
=== 5.3. Backend and Hardware-Dependent Behavior ===&lt;br /&gt;
&lt;br /&gt;
Because ONNX is implemented by many backends (ONNX Runtime, TensorRT, CoreML, PyTorch JIT, etc.), and these in turn run on varied hardware (CPU, GPU, accelerators), the numeric behavior can be backend-dependent. Some examples:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;CPU backends:&#039;&#039;&#039; Many CPUs do not have native float16 vector arithmetic (with a few exceptions in recent CPUs). ONNX Runtime’s default CPU execution provider will often upcast float16 to float32, compute, then downcast&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;. This ensures accuracy but means that if you thought you were getting a full float16 pipeline, you’re not – the CPU is essentially ignoring the low precision in compute. On the other hand, x86 CPUs have extended 80-bit precision in x87 registers (though less used now) and 64-bit SIMD registers for doubles; some compiler optimizations might carry intermediate results in higher precision. This could make CPU results slightly different from a GPU’s if, say, a sum is done in 80-bit on CPU versus 32-bit on GPU. Generally, modern compilers avoid such extended precision unless explicitly asked, to improve reproducibility.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;GPU backends:&#039;&#039;&#039; GPUs often perform summations in parallel using tree reductions. The order of operations can change depending on how threads are scheduled. This means even on the same GPU, summing an array in one launch versus another might not give bit-identical results if the internal parallel reduction algorithm differs (though it should be deterministically the same given the same library version and input, but if you change GPU model or BLAS library, it might differ). Also, GPUs have fused multiply-add (FMA) instructions that do $(a*b + c)$ in one step with one rounding, which can produce slightly different results than doing the multiply and add separately (which would round twice). If an ONNX backend uses a fused kernel (like a GEMM library) it might yield different low-order bits than a simple loop.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Vendor libraries:&#039;&#039;&#039; ONNX Runtime can use vendor BLAS or DNN libraries (MKL, cuDNN, TensorRT, etc.). These libraries often implement their own strategies for numerical stability. For example, NVIDIA’s cuDNN might use FP32 accumulation for FP16 inputs by default. TensorRT (NVIDIA’s inference engine) will sometimes automatically promote precisions or insert scaling to maintain stability&amp;lt;ref name=&amp;quot;ref13&amp;quot;/&amp;gt;. On the other hand, if you explicitly request FP16 in TensorRT with no FP32 fallback, it might do the entire op in FP16 and you’d see more error. Some libraries offer knobs: TensorRT allows choosing FP32, FP16, or INT8 precision for each layer, and will use FP32 internally for safety in some cases unless forced otherwise.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Hardware-specific quirks:&#039;&#039;&#039; As another example, NVIDIA’s &#039;&#039;&#039;TensorFloat-32 (TF32)&#039;&#039;&#039; on Ampere GPUs is a precision mode where matrix multiplications use 10-bit mantissa (so about FP16 precision for the multiply) but keep 8-bit exponent (range like FP32). By default, CUDA libraries use TF32 for FP32 matrix multiplies on Ampere to speed them up, unless you disable it. This means if you run an ONNX model with FP32 on an A100 GPU, your matmuls might actually be lower precision than FP32 (though higher than FP16)&amp;lt;ref name=&amp;quot;ref6&amp;quot;/&amp;gt;. It’s an example of hardware choosing an intermediate precision for performance. From ONNX’s perspective, the output is still FP32 and (hopefully) within error tolerances, but a user might be surprised that it’s not a “true” FP32 computation internally. If exactness is needed, one has to turn off TF32.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Non-determinism:&#039;&#039;&#039; Some backends (especially on GPUs) allow non-deterministic execution for performance. For instance, atomic adds in parallel reductions can produce results that vary run-to-run because the order of accumulation is race-condition-dependent. ONNX’s spec doesn’t cover this explicitly, but it’s something to consider. In ONNX Runtime, there are settings for deterministic computing, but by default it may use all available performance optimizations, even if results vary at the 1e-7 level.&lt;br /&gt;
&lt;br /&gt;
In short, the numerical behavior under ONNX can depend on the combination of backend software and hardware capabilities. The lack of a strict spec for accumulation means implementations have freedom to use clever strategies (higher precision, reordering) to improve accuracy or speed. Usually this is positive (e.g. getting better accuracy than a naive approach). But it could also lead to slight inconsistencies. For example, you might find that exporting a PyTorch model to ONNX and running it with one engine yields outputs slightly different from PyTorch’s – not because ONNX is wrong, but because the summation or convolution was done differently (maybe PyTorch summed in FP32 as well, but maybe not in exactly the same way).&lt;br /&gt;
&lt;br /&gt;
The key takeaway is that ONNX &#039;&#039;by design&#039;&#039; abstracts away these details, which is good for portability but means you need to &#039;&#039;&#039;trust the backend&#039;&#039;&#039; or verify its precision behavior for sensitive models. If necessary, you can enforce certain behavior by inserting ops (for example, if you want to ensure FP32 accumulation, you could insert a cast to FP32 for inputs of a MatMul and cast the result back to FP16 – this way the ONNX graph itself mandates FP32 compute in between). This of course sacrifices some performance or device-specific magic.&lt;br /&gt;
&lt;br /&gt;
Looking forward, as mentioned, there are proposals to enrich ONNX’s specification regarding precision. This might include explicit flags for “exact summation” or “accumulate in int32” etc., which would make models more self-descriptive but also potentially less portable (if a backend doesn’t support that, what then?). For now, users should be aware of these backend differences. Most of the time, well-engineered ONNX runtimes yield results close enough that they don&#039;t affect the application (e.g., classification top-1 remains the same even if low-level sums differ by 1e-5). But for high-stakes numerical computations, testing and maybe adjusting the model or backend settings is prudent.&lt;br /&gt;
&lt;br /&gt;
== Numerical Stability in Deep Learning Models ==&lt;br /&gt;
&lt;br /&gt;
Deep learning computations involve many operations that, while theoretically well-defined, can be sensitive to numerical issues when implemented in finite precision. We&#039;ll discuss a few common scenarios in neural networks where numerical stability is a concern, and how precision and accumulation play a role.&lt;br /&gt;
&lt;br /&gt;
=== 6.1. Dot Products and Large-Scale Reductions ===&lt;br /&gt;
&lt;br /&gt;
Neural networks are full of dot products: every fully-connected layer computes $\mathbf{y} = W\mathbf{x}$, which is a series of inner products between weight vectors and the input. Convolutional layers compute weighted sums of local regions. Attention mechanisms compute large matrix products of query/key/value matrices. All these boil down to summing a lot of terms: $\sum_{k=1}^K a_k b_k$. When $K$ is large (it could be in the thousands or more for a single neuron in a fully connected layer of a large model), the potential for floating-point error in that sum increases.&lt;br /&gt;
&lt;br /&gt;
If using FP32, the error may still be negligible in many cases, but in lower precision like FP16, it can become significant. For example, consider a dot product of length 2048 in FP16. Each product is in FP16 (with ~3 decimal digits precision) and if accumulated in FP16, the worst-case error could be on the order of $2048 \times \varepsilon_{FP16} \approx 2048 \times 0.0009766 \approx 2$ (i.e., you could lose a couple units in the third decimal place relative accuracy in worst case). Even in random error accumulation, you might see error on the order of $\sqrt{2048} \times 0.0009766 \approx 0.044$ or 4.4% (though this is a rough estimate). That’s quite significant. Fortunately, as discussed, most hardware will accumulate such sums in a higher precision like FP32, dramatically reducing that error (~7 orders of magnitude smaller $\varepsilon$). But if it didn’t, the network’s outputs could get noisy. A fully-connected layer with many inputs might output slightly different results depending on summation order if done in low precision.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Large-scale reductions&#039;&#039;&#039; also occur in operations like global average pooling (summing all elements of a feature map) or computing the total loss by summing loss over batch samples. If one sums 100,000 values in FP32, the worst-case error could be on the order of 100,000 &#039;&#039; 1e-7 ≈ 1e-2 (1% uncertainty), though typical error would be much less. In FP16, summing 100k values is likely impossible without overflow: as noted, adding ~10^4 moderate numbers can overflow FP16&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. So large reductions &#039;&#039;must* be handled carefully (either in chunks or in higher precision). Frameworks generally do: for example, when performing a sum reduction on GPU, libraries break it into parallel blocks that accumulate in registers (usually 32-bit registers, even if inputs are 16-bit). &lt;br /&gt;
&lt;br /&gt;
Another aspect is that many dot products in neural nets involve values of varying magnitude. If the distribution of terms is such that some are much larger than others, the smaller terms might not contribute much if summed last. Ideally, one might sort by magnitude or use pairwise summation to improve that. Most BLAS libraries effectively do a pairwise strategy due to their parallel structure. So, while worst-case analyses are grim, practical implementations mitigate a lot of it.&lt;br /&gt;
&lt;br /&gt;
However, certain layers can suffer if not handled properly. One famous example is in RNNs or LSTMs: if one uses FP16 naive summation for the gating mechanism, errors could accumulate over many time steps and cause drift or instability. In modern practice, recurrent layers are often run in FP16 with FP32 accumulators to avoid this.&lt;br /&gt;
&lt;br /&gt;
In summary, &#039;&#039;&#039;dot products and large reductions need sufficient precision&#039;&#039;&#039; or robust algorithms. If you are implementing a custom kernel (say a custom ONNX op) and you have to sum a huge array, you should consider using a technique from Section 3 (pairwise summation, Kahan, or at least double accumulator if possible). Many deep learning computations, by virtue of hardware and library design, already do something like this under the hood. But it&#039;s always good to be aware, especially if pushing into lower precision territories.&lt;br /&gt;
&lt;br /&gt;
=== 6.2. Cancellation, Dynamic Range, and Conditioning ===&lt;br /&gt;
&lt;br /&gt;
We introduced &#039;&#039;cancellation&#039;&#039; earlier in the context of summation. In deep learning, cancellation can appear in a few places. One is in the computation of certain norms or variances. For instance, Batch Normalization computes mean and variance of activations. The variance formula $\mathrm{Var}(x) = E[x^2] - (E[x])^2$ is subject to catastrophic cancellation if $E[x]$ and $\sqrt{E[x^2]}$ are close in magnitude (which can happen if the variance is small compared to the mean). If implemented naively in single precision, you might get a negative variance due to numerical error (which would then be clamped to zero or sqrt would yield NaN). Robust implementations compute variance in a single pass or increase precision. Indeed, in many frameworks, batchnorm accumulation is done in FP32 even if the activations are FP16, precisely to avoid numerical issues in computing mean and variance. ONNX itself doesn’t specify this, but an ONNX backend is likely to do batchnorm stats in higher precision for safety. The &#039;&#039;conditioning&#039;&#039; of the variance calculation is poor when variance is tiny relative to mean, so it&#039;s a spot where higher precision is needed.&lt;br /&gt;
&lt;br /&gt;
Another example is in certain loss functions or regularizations. If you subtract two large numbers to get a small difference, you lose precision. Say you have two nearly identical predictions and you subtract them as part of some operation – if done in float16, you might get zero whereas float32 would have a slight difference. This could affect, say, a gradient if that difference was supposed to propagate information. Fortunately, neural nets often avoid directly subtracting large nearly equal numbers; but it can happen. In attention mechanisms, the &#039;&#039;score normalization&#039;&#039; (softmax) subtracts a max to avoid huge exponentials, which is good for range but if multiple scores are close to the max, you subtract numbers that are close. However, those subtractions are not catastrophic in the same way; they&#039;re intentional to reduce range and are typically fine in FP32 or even FP16.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Dynamic range&#039;&#039;&#039; issues are everywhere in deep learning. Activation functions like ReLU and exponential can produce very large or very small values. If subsequent computations don’t account for that, you get infinities or zeros. For example, consider a network output that produces a logit of 100 in FP16. $\exp(100)$ in FP16 overflows to infinity (since FP16 max ~ 6e4 and $e^{100} \sim 3.7e43$). If you then do softmax, that one infinity will dominate the sum and you might get a probability of 1 for that class (or NaNs if not handled). The standard softmax implementation subtracts the max logit, so in this case it would subtract 100 from all logits, making the largest exponent $\exp(0)=1$ and others tiny – this avoids overflow. But what about underflow? If you had a very negative logit, $\exp(-100)$ in FP16 would underflow to 0. In softmax, that just contributes ~0 probability, which is fine (it effectively is zero anyway). Underflow tends to be less dangerous than overflow in inference — underflow usually just means “this probability/neuron is effectively 0,” whereas overflow can poison the results with NaNs or Infs. &lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Conditioning&#039;&#039;&#039; refers to how sensitive a function is to input perturbations. Neural networks themselves as functions can be poorly conditioned (e.g., tiny changes in weights cause big changes in output for some architectures), but typically they are designed to be reasonably well-behaved. Certain internal computations like matrix inversion (if one does a pseudoinverse or something for a layer) can be extremely ill-conditioned if the matrix is near singular. That again would need double precision perhaps. In ML, one common ill-conditioned operation is the &#039;&#039;&#039;softmax denominator&#039;&#039;&#039;: $\sum_j e^{x_j}$ can be very large and also very sensitive to changes in the largest $x_j$. However, the softmax is more stable if computed carefully (max trick). Another is &#039;&#039;&#039;normalization&#039;&#039;&#039;: computing $\frac{x - \mu}{\sigma}$ for layer norm or batch norm – if $\sigma$ is very small, then the division blows up noise. In those cases, a small $\epsilon$ is usually added to $\sigma^2$ to prevent instability (this is a model-level fix, not a numeric one per se, but it acknowledges numeric limits).&lt;br /&gt;
&lt;br /&gt;
In training, &#039;&#039;&#039;optimizers&#039;&#039;&#039; can suffer from precision issues. For example, the Adam optimizer keeps a moving average of squared gradients; in half precision, these small updates might flush to zero or not change at some point. Some research found that certain adaptive optimizers break in FP16 because the small differences are lost (&amp;quot;Why FP16 training breaks NAdam&amp;quot; etc.). The solution is often to keep the optimizer states in FP32 while doing the bulk of compute in FP16. This again is a form of mixed precision.&lt;br /&gt;
&lt;br /&gt;
From an inference perspective, once the model is trained, the main concerns are avoiding overflow in activations and ensuring summations (like in softmax or reductions) are done with enough precision. The model’s weights are fixed, so we don&#039;t have to worry about cumulative error over iterations as in training, just the forward pass.&lt;br /&gt;
&lt;br /&gt;
=== 6.3. Normalization Layers and Softmax ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Normalization layers&#039;&#039;&#039; (BatchNorm, LayerNorm, etc.) and &#039;&#039;&#039;Softmax&#039;&#039;&#039; are specifically worth examining for numerical stability:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Batch Normalization:&#039;&#039;&#039; During inference, batchnorm applies an affine transform using precomputed mean and variance (from training). It’s basically: $y = \frac{x - \mu}{\sqrt{\sigma^2 + \epsilon}} \gamma + \beta$. This is a stable operation as long as $\epsilon$ is provided (to avoid division by zero). Since $\mu, \sigma^2$ are constants in inference, it reduces to linear ops which are fine. The main numerical work (computing mean/variance) happened during training or model calibration, and frameworks usually did that in FP32. So batchnorm at inference time is not a source of instability (it’s just a scale and shift of the data).&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Layer Normalization / Group Normalization:&#039;&#039;&#039; These compute mean/var on the fly for each sample (and possibly channel group). In inference, they still need to compute these statistics for each forward pass. If implemented in low precision, the variance calculation can suffer cancellation as mentioned. However, it&#039;s likely that inference engines compute the mean and variance in FP32 even if inputs are FP16 (especially since these are per-sample operations, not huge batch reductions – it&#039;s feasible to do in higher precision). If someone implemented layer norm purely in FP16, they might see issues when the variance is very small. Usually there’s an $\epsilon$ added which might dominate a tiny variance and thus mitigate catastrophic cancellation (the result might just output something mostly from $\epsilon$). In summary, normalization layers are usually fine as long as the implementation uses enough precision for the summation of squares.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Softmax:&#039;&#039;&#039; Softmax is notorious if done improperly. The safe algorithm: subtract the maximum input from all inputs (to bring the largest exponent to 1.0), exponentiate, sum them up, divide each exponent by the sum. The max subtraction handles overflow in $\exp()$. But what about the sum? As discussed, if there are $N$ elements in the softmax, the sum is at most $N$ (when all inputs equal the max). In classification, $N$ might be the number of classes. For ImageNet, $N=1000$, so worst sum ~1000 (which is fine in FP16, as 1000 &amp;lt;&amp;lt; 65504). For an extreme case like a language model, $N$ could be 50,000 (vocab size). Sum could be 50,000 in worst case (everyone equal probability). 50,000 is still below 65504, so actually an FP16 sum would &#039;&#039;not&#039;&#039; overflow in that scenario, and the rounding error on a sum of 50k identical terms would be maybe a few ULPs (which at 50k magnitude might be ~0.5 difference worst-case). So maybe softmax summation in FP16 is not catastrophic for output probabilities – you’d get essentially 1.0 after normalization instead of 0.99999 in some case. Usually fine.&lt;br /&gt;
&lt;br /&gt;
:The bigger issue is if one or a few terms dominate the softmax. If one term is much larger, the others underflow to 0 after the max subtraction. That’s fine: you get a one-hot distribution essentially. If the model expected a softer distribution, that could be a problem. But if the logits were really that far apart, even FP32 would give nearly one-hot. So not really a precision issue, more of a model property.&lt;br /&gt;
&lt;br /&gt;
:However, the &#039;&#039;&#039;stability during training&#039;&#039;&#039; is a big issue. The GitHub thread we saw (ColossalAI) mentioned training a 130B model where FP16 softmax in attention caused training to occasionally blow up&amp;lt;ref name=&amp;quot;ref14&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref14&amp;quot;/&amp;gt;. They observed attention scores in some heads ranging up to 1e4 or as low as 1e-3&amp;lt;ref name=&amp;quot;ref14&amp;quot;/&amp;gt;. 1e4 exponent in FP16 is definitely Inf (because $\exp(10^4)$ is astronomically huge, but even exp(100) is inf in half). The method CogView introduced (Precision Bottleneck Relaxation) involved subtracting the max &#039;&#039;per head&#039;&#039; to reduce range&amp;lt;ref name=&amp;quot;ref14&amp;quot;/&amp;gt;—which is basically the softmax max trick at a finer granularity or something similar. In the end, they found &#039;&#039;&#039;the simplest fix was to compute softmax in FP32&#039;&#039;&#039; for those layers&amp;lt;ref name=&amp;quot;ref14&amp;quot;/&amp;gt;. They reported that it had negligible speed impact but significantly improved stability&amp;lt;ref name=&amp;quot;ref14&amp;quot;/&amp;gt;. This highlights a general principle: certain operations like softmax (and sometimes matrix multiplications in attention) benefit from higher precision, and using FP32 in a few critical spots can save a mixed-precision training from crashing. In inference, using FP32 for softmax is also a safeguard if one is uncertain. It costs little because softmax is not a heavy compute compared to matrix multiplies.&lt;br /&gt;
&lt;br /&gt;
:ONNX does have a Softmax operator; if the input is float16, a backend might do exactly what they described: internally cast to float32 for the softmax computation, then cast output back to float16. Some inference engines (like DeepSpeed’s inference or others) keep attention softmax in FP32 even if rest is FP16, for robustness&amp;lt;ref name=&amp;quot;ref14&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Apart from softmax, other places where exponentials occur are in loss functions like cross-entropy (which uses log-softmax internally), or in activation functions like Sigmoid and Tanh (which are basically scaled logistic functions). Sigmoid($x)=1/(1+e^{-x})$. If $x$ is moderately large positive, $e^{-x}$ underflows to 0 in FP16 if $x&amp;gt;~11$. That gives a sigmoid output of 1.0 exactly. In FP32, $e^{-x}$ for $x=11$ is about $5e-5$, not underflow, so sigmoid ~0.99995. So FP16 will saturate some outputs to exactly 0 or 1 sooner than FP32 would. This can slightly change behaviors (e.g., a neuron might go to full saturation a bit early). Usually, this isn’t a huge deal, but it could make gradients zero earlier. In inference, we don’t care about gradients, just the output value. Getting exactly 1.0 vs 0.99995 probably has no practical difference for a classification output. But one should be aware that &#039;&#039;&#039;activation functions can saturate faster&#039;&#039;&#039; in lower precision due to limited range or precision. ReLU has no issue (it’s linear or 0), but exponential-based activations do. In practice, again, frameworks often simply compute those in the given precision and accept the slight difference. If it were a problem, one could do those in FP32 as well.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Summary:&#039;&#039;&#039; Numerical stability in inference is maintained by using robust computation methods: subtracting maxima in softmax/LogSumExp, adding epsilon in denominators for normalization, and using higher precision for summations or critical steps. Most of these are built into the algorithms. ONNX models assume these standard practices (they don&#039;t need to explicitly do them in the graph because the backend inherently will for ops like Softmax). It&#039;s wise when building or exporting models to ONNX to test them in the target precision and ensure there are no surprises (like an output becomes NaN or Inf due to precision). Usually if training was done in mixed precision, the model will have been vetted in that environment already.&lt;br /&gt;
&lt;br /&gt;
== Performance vs. Accuracy Trade-offs ==&lt;br /&gt;
&lt;br /&gt;
There is an inherent trade-off between numerical precision and computational performance (speed, memory usage) in AI systems. Choosing lower precision can dramatically increase throughput and reduce memory, but it may introduce arithmetic errors. Engineers must balance these considerations:&lt;br /&gt;
&lt;br /&gt;
=== 7.1. Throughput, Memory Bandwidth, and Precision ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Throughput (speed)&#039;&#039;&#039; is often higher for lower-precision operations. Modern hardware can execute more low-precision ops in parallel than high-precision ops. For example, an NVIDIA V100 GPU can perform 2× more half-precision FLOPs than single-precision per clock, and newer architectures with Tensor Cores can do 8× more (since a Tensor Core warp operation might perform 64 FP16 FMAs per cycle vs 16 FP32). Similarly, int8 operations can be even faster and more energy efficient. Lower precision numbers also use less memory bandwidth – fetching 16-bit data is twice as fast as 32-bit data from memory (assuming the same bus width), which can relieve memory bottlenecks. Many deep learning workloads are memory-bound, so halving the data size can nearly double throughput if compute is not the limiting factor. This is a huge incentive to use lower precision.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Memory and storage&#039;&#039;&#039;: Using FP16 or int8 weights reduces model size, which is important for deploying large models to memory-limited environments (like edge devices or GPUs with limited VRAM). It also means you can fit larger batch sizes in memory, or more models on a chip, etc. There is also a cascade effect: smaller data means better cache utilization, which further improves performance.&lt;br /&gt;
&lt;br /&gt;
However, &#039;&#039;&#039;accuracy&#039;&#039;&#039; suffers if precision is too low. We have discussed how rounding error and limited range can cause deviations in outputs. The question is: do those deviations matter for the application? Often for neural networks, a small amount of noise or error in intermediate calculations does not change the final prediction. Neural nets are somewhat error-tolerant due to their redundant distributed representations. This is why quantization and FP16 techniques can work at all. The final model accuracy (e.g. top-1 accuracy on ImageNet) might drop only 0.5% or less when using FP16 instead of FP32, or int8 instead of FP16, if done properly. For many use-cases, this loss is acceptable given the speed gains.&lt;br /&gt;
&lt;br /&gt;
If one goes too low (like naive FP8 or int4 without special care), accuracy might drop severely – perhaps making the model unusable. Thus the trade-off: as precision decreases, performance increases but at some point accuracy degrades beyond an acceptable threshold. Engineers aim to find the lowest precision that still preserves accuracy.&lt;br /&gt;
&lt;br /&gt;
There is also the consideration of &#039;&#039;&#039;accumulation strategy&#039;&#039;&#039; affecting performance: for instance, Kahan summation (compensated) is more accurate but ~4× slower than naive summation. If you need that accuracy, you pay a performance cost. In many AI inference scenarios, the slight extra accuracy from something like Kahan isn&#039;t needed, so it’s not used. But in a scenario where you sum millions of elements for a final result that is thresholded, maybe you do need it.&lt;br /&gt;
&lt;br /&gt;
Another dimension is &#039;&#039;&#039;determinism vs speed&#039;&#039;&#039;. Enforcing a deterministic reduction order (for reproducibility) might require disabling some parallelism, which could slow things down. If exact repeatability is critical (like certain scientific or financial applications of neural nets), one might sacrifice some performance to ensure the summation happens the same way every time. ONNX allows both modes depending on the runtime.&lt;br /&gt;
&lt;br /&gt;
From a memory perspective, if precision is higher than needed, you waste memory. For example, storing intermediate activations in FP32 when they could be FP16 uses 2× memory, which might mean lower batch size or more memory traffic. So there&#039;s strong motivation to keep things as low-precision as possible &#039;&#039;except&#039;&#039; where accuracy truly demands.&lt;br /&gt;
&lt;br /&gt;
In summary, the trade-off often manifests as &#039;&#039;diminishing returns&#039;&#039;: FP64 vs FP32 – huge slowdown for usually negligible accuracy gain in ML; FP32 vs FP16 – moderate speedup for usually minor accuracy hit; FP16 vs int8 – another big speedup but maybe more accuracy loss unless calibration is done; int8 vs int4 – speed gain but often big accuracy drop that might not be worth it for current models.&lt;br /&gt;
&lt;br /&gt;
Engineers use techniques like &#039;&#039;&#039;mixed precision&#039;&#039;&#039; to target high precision only where needed. For example, run most of the model in FP16, but keep a few sensitive layers in FP32. This yields most of the performance benefits while ensuring critical parts are accurate. We already saw softmax as one example. Another might be final output layer: sometimes using FP32 for the logits and softmax can be safer.&lt;br /&gt;
&lt;br /&gt;
Hardware is also trending to support mixed modes – like FP8 Tensor Cores accumulate in FP32, etc. So hardware tries to give accuracy cheaply.&lt;br /&gt;
&lt;br /&gt;
=== 7.2. Hardware Accelerators and Tensor Cores ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Hardware accelerators&#039;&#039;&#039; (GPUs, TPUs, FPGAs, ASICs like GraphCore IPU, etc.) have specific support for certain precisions. Leveraging these can provide massive speedups:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;NVIDIA Tensor Cores:&#039;&#039;&#039; These started in Volta (V100) with support for 4x4 matrix multiply-adds on FP16 inputs with FP32 accumulation&amp;lt;ref name=&amp;quot;ref6&amp;quot;/&amp;gt;. Using Tensor Cores can give up to 8x throughput compared to using regular FP32 cores. But to use them, the data needs to be FP16 (or now BF16 or FP8 on newer GPUs) and aligned to certain dimensions (multiples of 8 etc. in matrix sizes, which frameworks handle by padding if needed). If an ONNX model is all FP32, by default it would not use Tensor Cores unless the runtime automatically converts some ops to mixed precision (which some do via a flag or environment variable enabling TF32 or FP16). The introduction of &#039;&#039;&#039;TensorFloat-32 (TF32)&#039;&#039;&#039; in Ampere GPUs is essentially a compromise: treat FP32 inputs as two 16-bit significand halves (10-bit mantissa actually) so that it can run on Tensor Cores, giving ~8x speed, while still outputting FP32. TF32 has about the precision of FP16 but range of FP32, and it was chosen because it typically doesn’t hurt convnet accuracy. ONNX Runtime or others on Ampere will use TF32 by default for conv and matmul unless disabled, giving a free speed boost at minimal accuracy cost. If exact FP32 is needed, users have to turn it off (sacrificing speed).&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;TPU (Google):&#039;&#039;&#039; TPUs have native support for BF16 matrix ops with FP32 accumulators. They were designed specifically for the mixed precision approach in training. For inference, TPUs also support int8 and other quantized ops (e.g., via Edge TPU for lower-power). If exporting ONNX to run on a TPU (via some bridge), one might get automatic BF16 usage. BF16 has the advantage of no loss in range, so TPUs can more straightforwardly run models in BF16 without modifications.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;FPGAs and ASICs:&#039;&#039;&#039; Some custom accelerators might use fixed-point or block floating-point arithmetic for efficiency. They often treat the network in quantized terms. The ONNX quantization specification (QuantizeLinear/DequantizeLinear ops) exists for representing quantized models (INT8 etc.), which is another domain beyond floating-point, but it overlaps as a precision issue. In those systems, accumulators for INT8 × INT8 multiplies are usually 32-bit (to avoid overflow for dot products up to certain lengths). The trade-off there is between integer vs float representation, which can give big speedups on CPUs/DSAs via SIMD instructions (many CPUs have vector int8 multiply-add instructions now for inference).&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Memory bandwidth on hardware:&#039;&#039;&#039; Some accelerators include special high-bandwidth memory or cache specifically for lower precision. For example, an accelerator might fetch two FP16 values in one 32-bit memory transaction – effectively doubling memory bandwidth for half precision. On some hardware, using FP16 can even reduce memory &#039;&#039;latency&#039;&#039; if multiple values can be packed.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Parallelism and Vectorization:&#039;&#039;&#039; Many hardware units process data in fixed vector widths (e.g., 256-bit registers). That could be 8 floats at once or 16 half-floats at once in the same 256 bits. If your code is in FP16, you might use the full vector width (16 values) whereas FP32 might only use half (8 values). This means FP16 can make better use of vector units. Similarly, matrix units often have fixed tile sizes, and smaller data types allow larger tiles or more tiles concurrently.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Energy efficiency:&#039;&#039;&#039; Lower precision often means less energy per operation, which on large deployments (data centers or battery-powered devices) is a significant advantage. This isn&#039;t directly performance, but it&#039;s an important trade: you might choose a slightly lower precision to save power even if accuracy drops a tiny bit.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Software overhead:&#039;&#039;&#039; Using mixed precision on hardware involves some overhead of casting and moving data between precisions. For instance, if you have to frequently cast FP16 to FP32 and back, that adds instructions and memory copies. A well-designed accelerator (like GPUs with unified memory for FP16/32 or special hardware casting) minimize this. Still, the runtime might insert extra operations (as we saw, ONNX Runtime CPU inserted many Cast nodes converting FP16 to FP32 and back&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;, which actually &#039;&#039;slowed down&#039;&#039; that FP16 model by 4x vs FP32 on a CPU, because the CPU had no native FP16). So ironically, on some hardware, using lower precision in ONNX might degrade performance if not supported (the CPU case). Always ensure the target hardware actually supports the low precision you plan to use; otherwise, you&#039;ll pay overhead for emulation.&lt;br /&gt;
&lt;br /&gt;
To illustrate trade-off: Suppose you have a model that achieves 99% accuracy in FP32 and drops to 98.5% in FP16. If FP16 runs 2x faster, many would accept the 0.5% accuracy drop for double throughput. If int8 runs 4x faster but accuracy drops to 95%, that might be too much drop for some applications, but acceptable for others that value speed. It&#039;s application-dependent.&lt;br /&gt;
&lt;br /&gt;
In critical applications, one might maintain higher precision accumulators or run certain validation steps in high precision to ensure nothing drifted. In research, sometimes a model is trained in mixed precision but validated in double to ensure any small numeric differences don’t influence the reported result (rare, but for scientific computing integration maybe).&lt;br /&gt;
&lt;br /&gt;
In the ONNX world, performance-vs-accuracy is often managed via &#039;&#039;&#039;Execution Providers&#039;&#039;&#039; (EPs) that target specific hardware. For example, the TensorRT EP in ONNX Runtime can take an ONNX model and run it using NVIDIA&#039;s high-performance inference library, which will use FP16 or INT8 if allowed, to maximize speed. The user can configure the EP with precision flags. If the accuracy drop is too high, one might restrict it to FP32. If it&#039;s fine, allow FP16. Tools exist to measure the accuracy difference (like calibrating int8 quantization).&lt;br /&gt;
&lt;br /&gt;
To conclude, the trade-offs revolve around picking the lowest precision that yields acceptable accuracy, and utilizing hardware features fully. ONNX being a high-level spec abstracts this, but it&#039;s ultimately up to the deployment to decide and sometimes up to the exporter to incorporate quantization. ONNX’s role is to be able to &#039;&#039;represent&#039;&#039; these choices (they have added quantized types, FP8 types, etc., to allow the graph to include those when needed).&lt;br /&gt;
&lt;br /&gt;
== Best Practices and Design Guidelines ==&lt;br /&gt;
&lt;br /&gt;
Given the above insights, here are some best practices and guidelines for dealing with numerical precision and accumulators in AI inference (and training) design:&lt;br /&gt;
&lt;br /&gt;
=== 8.1. Choosing an Accumulator Precision ===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Match precision to data length and range:&#039;&#039;&#039; If an operation accumulates a large number of terms, consider using a higher precision accumulator than the input data. For example, for summing &amp;gt;1000 values of type FP16, an FP32 accumulator is recommended to avoid large rounding error or overflow. If summing &amp;gt; million values of FP32, consider double (FP64) accumulation if high accuracy is needed (though this is rare in DL). &#039;&#039;Rule of thumb:&#039;&#039; if $N \cdot \varepsilon$ is not tiny (say &amp;gt;1e-3), you may benefit from higher precision. For FP16 ($\varepsilon \approx 1e-3$), that threshold N is a few hundred. For FP32 ($\varepsilon \approx 1e-7$), N would be in the millions before 1e-3 error.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Use higher precision in critical layers:&#039;&#039;&#039; Identify if certain layers of the model are more sensitive to precision. For instance, the output layer or a layer that feeds into a very sharp decision boundary might require more precision. If a small change there could flip the prediction, you don&#039;t want excessive numerical error. Using FP32 for logits and softmax is a common practice to maintain classification confidence stability.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Follow hardware defaults, but override if needed:&#039;&#039;&#039; Many hardware accelerators have sensible defaults (like accumulate in FP32 for tensor ops). It&#039;s usually best to use those defaults as they were chosen to balance accuracy and speed. Only override if you have evidence of a problem. For example, if using ONNX Runtime on CPU, you discovered it upcasts to float for you – great. But if using some custom accelerator that does pure FP16, you might manually insert casts in the ONNX graph around certain ops to force higher precision on those ops.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Maintain precision for gradient accumulation (training):&#039;&#039;&#039; This is more training-oriented, but worth noting: when summing gradients over batches or updating weights, use higher precision (FP32 or even 64) for those accumulations. This ensures training doesn&#039;t diverge due to numeric error. Many frameworks keep a &amp;quot;master copy&amp;quot; of weights in FP32 when training in FP16&amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;. For inference, this translates to: any running statistics or counters in the model (like if you had a running average inside the model) should probably be in higher precision to avoid drift.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Be mindful of intermediate cast costs:&#039;&#039;&#039; If you do decide to cast to higher precision for an operation in ONNX, remember it incurs memory copies and compute overhead. Make sure it&#039;s worth it. For example, if you cast a huge tensor to double, that could slow things drastically. Usually FP16-&amp;gt;FP32 is fine (hardware handles it well), but FP32-&amp;gt;FP64 might be expensive on GPUs.&lt;br /&gt;
&lt;br /&gt;
=== 8.2. Safe Reduction Lengths and Heuristics ===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Chunk long reductions:&#039;&#039;&#039; If you must do a very long sum in low precision (due to memory constraints or hardware), chunk the sum into parts, sum each to a higher precision local accumulator, then sum the partial sums. This is essentially pairwise summation. Many libraries use a fixed chunk size heuristic (e.g., sum in blocks of 256 in registers, then add those up). This bounds the error growth. If implementing manually, you can choose a chunk size such that chunk_length &#039;&#039; ε is small (like 256 &#039;&#039; 1e-3 = 0.256 for FP16, hmm maybe choose 100).&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Sort by magnitude if possible:&#039;&#039;&#039; In some cases (not typical in DL, but possible in post-processing), if you have numbers varying by orders of magnitude, summing from smallest to largest improves accuracy. It prevents small ones from being swallowed by big ones. This is more relevant if you output a sum of mixed-scale values (like some algorithm mixing big and small contributions). It’s not always feasible in real-time inference to sort, but keep in mind.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Use compensation for critical sums:&#039;&#039;&#039; If you find a particular sum or difference in your inference computation is crucial (like computing a small residual from two close numbers), consider using Kahan summation or another compensated scheme just for that part. For example, if you had an ONNX custom function that needs to sum a lot of almost cancelling terms, you might implement it with compensation internally.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Heuristics for FP8/low-bit:&#039;&#039;&#039; When using extremely low precision like FP8, adopt the recommended practices: use scaling (don&#039;t feed raw data into FP8, normalize it first), and perhaps only use FP8 on portions of the network that are empirically robust. The industry is still establishing heuristics, but one might be: use FP8 for matrix multiplications, but accumulate in FP16 or FP32; keep normalization in FP16; use FP16 for first and last layer maybe. This is evolving, so keep an eye on literature.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Keep an eye on sum length in softmax and normalization:&#039;&#039;&#039; If you have extremely large class counts or group sizes (e.g., a softmax over 1e6 items, or layer norm over tens of thousands of features), realize this is a giant reduction. You may want to ensure that reduction is done in higher precision. ONNX Softmax by default will likely not handle a million-class case in FP16 well (the sum would overflow since 1e6 &amp;gt; 65504). So either do that on CPU in FP32 or break it down (could do hierarchical softmax, etc.). This is a corner case, but relevant for huge vocab language models. In practice, vocab ~50k is fine as we said.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Test numeric differences:&#039;&#039;&#039; A practical tip: after quantizing or reducing precision, run some test inputs through both the high-precision model and low-precision model. Compare outputs (and maybe some intermediate signals if possible). If differences are consistently small relative to the application tolerance, you&#039;re good. If you see occasional large differences or instabilities (like outputs become NaN or Inf in low precision), that&#039;s a red flag. Use that testing to guide where you might need to increase precision or change algorithms.&lt;br /&gt;
&lt;br /&gt;
=== 8.3. When Higher Precision Is Required ===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Ill-conditioned calculations:&#039;&#039;&#039; If your model or pipeline performs any operation known to be ill-conditioned (matrix inversion, subtraction of nearly equal quantities, computing a tiny difference of large numbers), you likely need higher precision for that part. For example, if your ONNX model includes a custom op to solve a linear system, use double precision in that op if possible (or at least float with iterative refinement). In ML, this might occur in something like a physics-informed neural network or a differentiable simulation within the model.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Strict accuracy requirements:&#039;&#039;&#039; Some applications (medical, aerospace, finance) might have strict tolerances. If an inference model’s output needs to be correct to 5 decimal places, you probably cannot use FP16, which has only 3-4 decimal digits precision. FP32 might be barely enough (~7 digits). FP64 might be warranted to guarantee that level of accuracy in worst case. Most deep learning applications (like image classification) don&#039;t demand that level of numeric precision in outputs – as long as the top class is correct, it doesn&#039;t matter if its probability was 0.901 vs 0.899. But if you&#039;re doing something like summing probabilities to make a risk estimate or doing a cumulative sum over many predictions, be careful to accumulate precisely.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Large sums or products in post-processing:&#039;&#039;&#039; Sometimes the neural net is fine, but the &#039;&#039;post-processing&#039;&#039; of results might need precision. E.g., summing up probabilities of many events, or computing confidence intervals, etc. Do those calculations in double if needed outside the model.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Preventing drift in long sequences:&#039;&#039;&#039; For sequential models (RNNs processing thousands of steps), rounding error can accumulate over steps. If each step has a small error, after 10000 steps it could add up. In such cases, using a higher precision for the state might reduce drift. Some practitioners use FP64 for the hidden state accumulation in very long sequence processing to maintain numerical stability (though this is not common due to speed loss). Alternatively, periodically renormalize or reset small errors.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Simulations or differential equations:&#039;&#039;&#039; If your ONNX model involves simulating something (like a differential equation solver inside a model, or an ODE/RNN hybrid), those often need double precision to be stable for long durations. Recognize those situations and allocate precision accordingly.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Verification and debugging:&#039;&#039;&#039; Use higher precision as a tool to verify correctness. For example, run your model in FP64 (if possible) and compare to FP32 to see if there are differences. If not, FP32 is fine. If yes, you might find a particular calculation losing too much info in FP32, implying maybe you should keep it in FP64 in production as well. This is rare in inference but can be part of rigorous validation.&lt;br /&gt;
&lt;br /&gt;
In ONNX, using higher precision may involve casting to &amp;lt;code&amp;gt;tensor(double)&amp;lt;/code&amp;gt; for certain ops. Not all runtimes support double on GPU though (some do via emulation or just slower path). ONNX CPU does support double for general ops. So be mindful of the target.&lt;br /&gt;
&lt;br /&gt;
Finally, &#039;&#039;&#039;document your precision choices&#039;&#039;&#039;. If you deliver an ONNX model that uses mixed types (float16 inputs, float32 accumulations, int8 weights, etc.), ensure that is clear to whoever maintains or uses it. Precision issues can be sneaky, so clarity helps.&lt;br /&gt;
&lt;br /&gt;
== Limitations and Open Problems ==&lt;br /&gt;
&lt;br /&gt;
Despite advances in handling numerical precision, there remain limitations and open challenges in this domain:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Lack of Precision Specification in ONNX:&#039;&#039;&#039; As noted, ONNX currently has no way to formally specify that an op &#039;&#039;should&#039;&#039; use a higher precision internally. This can lead to nondeterministic differences across platforms&amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;. While proposals exist to add attributes for this&amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;, it is not yet standard. Thus an ONNX model might behave slightly differently on two compliant backends, and there&#039;s no built-in mechanism to ensure identical results. This is a limitation for applications requiring cross-platform consistency. Solving it may involve extending ONNX (e.g., an attribute like &amp;lt;code&amp;gt;accumulatePrecision=&amp;quot;FLOAT32&amp;quot;&amp;lt;/code&amp;gt; for MatMul) or tightening the spec, but those come with backward compatibility and implementation burden concerns.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Reproducibility and Determinism:&#039;&#039;&#039; Even with the same precision, parallel computation can introduce nondeterminism (due to different summation orders). Ensuring bit-identical results across runs (important for some sensitive domains) is non-trivial. Frameworks often provide &amp;quot;deterministic mode&amp;quot; which might use fixed ordering at cost of speed. In ONNX, there&#039;s no global flag for this; it&#039;s up to the runtime. More research could be done on algorithms that produce bit-wise identical results independent of parallel execution order (perhaps using techniques like pairwise summation or consistent partitioning). Stochastic rounding is another area: sometimes using probabilistic rounding can reduce bias in accumulated error, but it introduces randomness – reconciling that with reproducibility is tricky.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Ultra-low Precision (FP8 and below):&#039;&#039;&#039; While FP8 is being explored, going to 4-bit floats or mixed schemes is an open challenge. The OpenAI community is investigating 4-bit (quantization-wise) for LLMs with some success, but a true 4-bit float (like E3M1) would be extremely limited. Perhaps hybrid schemes (like block floating point, log scales, etc.) will come. ONNX already has some support (they mention 4-bit in documentation placeholders&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;), but it&#039;s nascent. Research into training techniques, calibration, and new number formats (like &#039;&#039;&#039;posit numbers&#039;&#039;&#039; or tapering precision floats) is ongoing. If a new format proves useful, ONNX will need to adapt to represent it.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Quantization Noise and Model Robustness:&#039;&#039;&#039; Reducing precision can sometimes expose models to issues like adversarial vulnerability or fragility – e.g., a slight perturbation might flip a decision if the margin was small and precision loss nudged it. Ensuring model robustness under quantization is an open problem. It might require re-training the model with quantization in the loop (quantization-aware training) or designing models that have large &amp;quot;margin&amp;quot; between classes to tolerate quantization noise.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Dynamic range during training vs inference:&#039;&#039;&#039; In training, activations can have wider distributions (especially early in training). Models often learn to constrain their ranges (via activation functions saturating or normalization), which makes inference more stable in lower precision. But if one tries to train in FP8 from scratch, it&#039;s very hard. This gap between what&#039;s possible in a post-trained inference model vs training is an open area – how to train directly in low precision without significant accuracy loss. Solving that could simplify workflows (no need for high-precision training then quantize; just train low-prec end to end). Techniques like loss scaling, gradient clipping, etc., are partial solutions. The development of optimizers that are resilient to low precision is ongoing.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Hardware variability:&#039;&#039;&#039; As new hardware emerges (with new numeric capabilities or quirks), ensuring that ONNX models run consistently is an ongoing task. For instance, one device might implement a fused op that reduces rounding error, another does it separately. Even something like convolution might use a different algorithm (FFT-based vs direct) which has different numeric properties. Open problem: could ONNX allow specifying numeric tolerances per op or preferred algorithms? That may be overkill for most, but for some HPC ML use cases, maybe desired.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Testing and validation of numerics:&#039;&#039;&#039; There isn&#039;t a standardized set of &amp;quot;numeric compliance&amp;quot; tests for ONNX runtimes beyond simple correctness. Perhaps in the future, test suites might include challenging numeric scenarios (large sums, nearly singular matrices) to ensure a runtime handles them gracefully (maybe by switching precision). Right now, it&#039;s mostly on the user to test their model under expected conditions.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Combining quantization with floating point:&#039;&#039;&#039; Many production models use a mix (e.g., int8 for some layers, FP16 for others). ONNX can represent this (with QuantizeLinear ops or MixedPrecision through casting), but tooling to automatically find the optimal mix is still developing. It&#039;s somewhat trial-and-error or using heuristics. An open question is how to automate precision assignment: given a model and a target hardware, how to automatically choose which layers can be int8, which should be FP16, etc., to meet an accuracy target. This crosses into NAS (neural architecture search) territory or automated quantization. &lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Numerical debugging tools:&#039;&#039;&#039; When a model goes wrong due to precision, it&#039;s often hard to pinpoint where. Better tooling to analyze numerical error layer by layer could be useful. For instance, one could run a model in double and float and diff the intermediate results to see where the largest divergence happens. Doing this systematically for big nets is an open tooling challenge. Some frameworks have hooks for this, but not standardized.&lt;br /&gt;
&lt;br /&gt;
In conclusion, while we&#039;ve come far with using lower precision in AI without sacrificing much accuracy, the landscape is always evolving. The key open problem is how to push precision lower (for efficiency gains) without crossing the threshold where accuracy falls off a cliff. Each new generation of hardware and techniques (like FP8, novel quantization schemes) extends that boundary, but also brings new questions on how to manage the numerical errors. ONNX as an interchange will need to keep pace, possibly by adding more expressive power for these numeric considerations.&lt;br /&gt;
&lt;br /&gt;
== See Also ==&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;IEEE 754 Floating-Point&#039;&#039;&#039; – The standard defining binary16, binary32, binary64 formats and rounding behavior.&lt;br /&gt;
* &#039;&#039;&#039;Floating-Point Arithmetic&#039;&#039;&#039; – General topic on how floating-point math works, common issues (overflow, underflow, cancellation).&lt;br /&gt;
* &#039;&#039;&#039;Kahan Summation Algorithm&#039;&#039;&#039; – Algorithm for compensated summation to reduce numerical error&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;.&lt;br /&gt;
* &#039;&#039;&#039;BFloat16&#039;&#039;&#039; – A short-format float with 8-bit exponent, used in TPUs and some GPUs for training networks.&lt;br /&gt;
* &#039;&#039;&#039;Mixed Precision Training&#039;&#039;&#039; – Technique of using FP16 (or BF16) along with FP32 accumulators to accelerate training&amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;.&lt;br /&gt;
* &#039;&#039;&#039;Quantization in Deep Learning&#039;&#039;&#039; – Converting networks to lower bit integer arithmetic for inference (INT8 quantization and calibration techniques).&lt;br /&gt;
* &#039;&#039;&#039;ONNX Operator Specification&#039;&#039;&#039; – Official docs for ONNX ops, which describe their mathematical function but not the required numeric precision (a point of potential extension in future).&lt;br /&gt;
* &#039;&#039;&#039;CUDA Tensor Cores &amp;amp; TF32&#039;&#039;&#039; – NVIDIA documentation on how tensor cores perform mixed precision and the TF32 format for accelerating FP32.&lt;br /&gt;
* &#039;&#039;&#039;Pairwise Summation&#039;&#039;&#039; – Summation algorithm with logN error growth&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;, used in many numerical libraries to improve accuracy of reductions.&lt;br /&gt;
* &#039;&#039;&#039;Softmax Stability&#039;&#039;&#039; – Articles on numerically stable softmax and log-sum-exp implementations to avoid overflow.&lt;br /&gt;
* &#039;&#039;&#039;Performance Tuning for Low Precision&#039;&#039;&#039; – Guides (e.g., NVIDIA or Intel whitepapers) on using low precision and the trade-offs involved in various hardware.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;references&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref1&amp;quot;&amp;gt;Accelerate and simplify Scikit-learn model inference with ONNX Runtime - Microsoft Open Source Blog — opensource.microsoft.com — https://opensource.microsoft.com/blog/2020/12/17/accelerate-simplify-scikit-learn-model-inference-onnx-runtime&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref2&amp;quot;&amp;gt;Floating Point Precision: Understanding FP64, FP32, and FP16 in Large Language Models - DEV Community — dev.to — https://dev.to/lukehinds/floating-point-precision-understanding-fp64-fp32-and-fp16-in-large-language-models-3gk6&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref3&amp;quot;&amp;gt;Floating Point Precision and Its Limitations | by Umair Akbar | Medium — akbu.medium.com — https://akbu.medium.com/floating-point-precision-and-its-limitations-cfb7247d7789&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref4&amp;quot;&amp;gt;Kahan summation algorithm - Wikipedia — en.wikipedia.org — https://en.wikipedia.org/wiki/Kahan_summation_algorithm&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref5&amp;quot;&amp;gt;Pairwise summation - Wikipedia — en.wikipedia.org — https://en.wikipedia.org/wiki/Pairwise_summation&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref6&amp;quot;&amp;gt;Numerical behavior of NVIDIA tensor cores - PMC — pmc.ncbi.nlm.nih.gov — https://pmc.ncbi.nlm.nih.gov/articles/PMC7959640/&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref7&amp;quot;&amp;gt;Mixed precision and performance-accuracy tuning (neuron-cc) — AWS Neuron Documentation — awsdocs-neuron.readthedocs-hosted.com — https://awsdocs-neuron.readthedocs-hosted.com/en/latest/about-neuron/appnotes/neuron-cc/mixed-precision.html&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref8&amp;quot;&amp;gt;Create Float16 and Mixed Precision Models - ONNX Runtime — onnxruntime.ai — https://onnxruntime.ai/docs/performance/model-optimizations/float16.html&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref9&amp;quot;&amp;gt;[Performance] ONNX FP16 model is having performance bottle neck when compared to FP32 variant · Issue #25824 · microsoft/onnxruntime · GitHub — github.com — https://github.com/microsoft/onnxruntime/issues/25824&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref10&amp;quot;&amp;gt;Float stored in 8 bits - ONNX 1.21.0 documentation — onnx.ai — https://onnx.ai/onnx/technical/float8.html&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref11&amp;quot;&amp;gt;[2209.05433] FP8 Formats for Deep Learning — arxiv.org — https://arxiv.org/abs/2209.05433&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref12&amp;quot;&amp;gt;Should MatMul op allow specification of accumulation precision? · Issue #7072 · onnx/onnx · GitHub — github.com — https://github.com/onnx/onnx/issues/7072&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref13&amp;quot;&amp;gt;Quantize ONNX models | onnxruntime — onnxruntime.ai — https://onnxruntime.ai/docs/performance/model-optimizations/quantization.html&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref14&amp;quot;&amp;gt;Keep Attention Softmax FP32 during FP16/ZeRO Training · Issue #1485 · hpcaitech/ColossalAI · GitHub — github.com — https://github.com/hpcaitech/ColossalAI/issues/1485&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;/references&amp;gt;&lt;/div&gt;</summary>
		<author><name>Timo.stripf</name></author>
	</entry>
	<entry>
		<id>https://www.emmtrix.com/w139/index.php?title=Numerical_Precision_in_ONNX_and_AI_Inference&amp;diff=2899</id>
		<title>Numerical Precision in ONNX and AI Inference</title>
		<link rel="alternate" type="text/html" href="https://www.emmtrix.com/w139/index.php?title=Numerical_Precision_in_ONNX_and_AI_Inference&amp;diff=2899"/>
		<updated>2026-01-30T15:23:47Z</updated>

		<summary type="html">&lt;p&gt;Timo.stripf: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&#039;&#039;&#039;Open Neural Network Exchange (ONNX)&#039;&#039;&#039; is an open standard format for representing machine learning models and neural network computations across different frameworks and hardware&amp;lt;ref name=&amp;quot;ref1&amp;quot;/&amp;gt;. As models are exported and deployed via ONNX, the &#039;&#039;&#039;numerical precision&#039;&#039;&#039; of computations becomes critical. Deep learning inference involves a variety of floating-point operations, and small numerical discrepancies can accumulate and affect model accuracy or reproducibility. This article provides an in-depth overview of floating-point precision issues in numerical computing, with a focus on how they manifest in AI inference and the ONNX ecosystem. We cover floating-point formats and rounding behavior, accumulation strategies for summations, the use of low-precision formats (FP16, BFloat16, FP8) in modern AI, and how ONNX defines (or leaves unspecified) the precision of accumulators in operations. Strategies for maintaining numerical stability in deep neural networks are discussed, along with the trade-offs between computational performance and arithmetic accuracy. The goal is to give engineers and researchers a comprehensive, encyclopedic reference on numerical precision in ONNX-based AI inference systems.&lt;br /&gt;
&lt;br /&gt;
== Floating-Point Precision in Numerical Computing ==&lt;br /&gt;
&lt;br /&gt;
=== 2.1. IEEE 754 Floating-Point Formats ===&lt;br /&gt;
&lt;br /&gt;
Modern computers represent real numbers using the IEEE 754 floating-point standard, which defines formats like 32-bit &#039;&#039;single precision&#039;&#039; (FP32) and 64-bit &#039;&#039;double precision&#039;&#039; (FP64). In IEEE 754 binary formats, a floating-point number is composed of three fields: 1 sign bit, several exponent bits (with a bias), and several fraction (mantissa) bits. For example, FP32 consists of 1 sign bit, 8 exponent bits, and 23 fraction bits (24 bits of significand precision including the implicit leading 1)&amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;. This provides roughly 7 decimal digits of precision and an exponent range allowing values on the order of $10^{\pm38}$. FP64 allocates 1 sign bit, 11 exponent bits, and 52 fraction bits (53-bit precision), yielding about 15–16 decimal digits of precision and a vastly larger range (~$10^{\pm308}$)&amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;. These larger formats (especially FP64) are the &amp;quot;gold standard&amp;quot; for numerical accuracy in scientific computing, as they reduce round-off error in complex calculations&amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;. However, the increased precision comes at the cost of doubled storage and typically reduced computational throughput (e.g. many GPUs execute FP64 instructions at a much lower rate than FP32&amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
In addition to single and double precision, the IEEE 754-2008 standard introduced a 16-bit &#039;&#039;half precision&#039;&#039; format (FP16, or binary16). FP16 has 1 sign bit, 5 exponent bits, and 10 fraction bits (11-bit significand including the implicit 1)&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. This format covers a much smaller dynamic range (approximately $6.1\times10^{-5}$ to $6.5\times10^4$ for normalized values&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;) and offers only around 3 decimal digits of precision. Half precision was originally used in graphics, but it has gained popularity in machine learning for its speed and memory advantages (discussed later in Section 4). Another format relevant to AI is &#039;&#039;&#039;BFloat16&#039;&#039;&#039; (Brain Float 16), a 16-bit float with &#039;&#039;&#039;8 exponent bits and 7 fraction bits&#039;&#039;&#039;&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. BFloat16 sacrifices precision (only ~7 fraction bits ≈ 2–3 decimal digits) in order to have the &#039;&#039;&#039;same exponent range as FP32&#039;&#039;&#039;&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. This means BFloat16 can represent very large or very small numbers (∼$10^{\pm38}$, similar range as FP32) but with much less precision between representable values&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. BFloat16 was introduced for deep learning by Google (for TPUs) to allow reduced precision &#039;&#039;training&#039;&#039; without frequent overflow/underflow, relying on the wide range and accepting the coarse precision.&lt;br /&gt;
&lt;br /&gt;
Each floating-point format has a &#039;&#039;&#039;unit roundoff&#039;&#039;&#039; or machine precision, denoted $\epsilon$. This is effectively the spacing between 1.0 and the next representable number. For FP32, $\epsilon \approx 2^{-23} \approx 1.19\times10^{-7}$, corresponding to about 7–8 decimal digits of precision&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. In FP64, $\epsilon = 2^{-52} \approx 2.22\times10^{-16}$ (15–16 decimal digits). Half precision is much less precise: FP16 has $\epsilon = 2^{-10} \approx 9.76\times10^{-4}$&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;, and BFloat16 has $\epsilon = 2^{-7} = 7.8125\times10^{-3}$&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. These values quantify how finely the continuum of real numbers is quantized in each format.&lt;br /&gt;
&lt;br /&gt;
=== 2.2. Machine Epsilon and Rounding Error ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Machine epsilon ($\varepsilon$)&#039;&#039;&#039; is formally defined as the smallest positive number such that $1.0 + \varepsilon &amp;gt; 1.0$ in the floating-point format&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. It effectively measures the &#039;&#039;&#039;precision&#039;&#039;&#039; of the representation. In FP32, $\varepsilon = 2^{-23} \approx 1.19\times10^{-7}$, meaning any result is rounded to about 7 decimal digits. FP16’s $\varepsilon = 2^{-10} \approx 9.77\times10^{-4}$, over 1000× larger (so only ~3 decimal digits of precision), and BFloat16’s $\varepsilon = 2^{-7} \approx 7.8\times10^{-3}$, reflecting its mere ~8-bit precision&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. Rounding in IEEE 754 by default uses &#039;&#039;&#039;round-to-nearest (ties to even)&#039;&#039;&#039;, which ensures the rounding error for any single operation is at most half a unit in the last place (0.5 ULP). We can model a floating-point addition as: &lt;br /&gt;
&lt;br /&gt;
$$\text{fl}(a+b) = (a+b)\,(1 + \delta),$$ &lt;br /&gt;
&lt;br /&gt;
where $|\delta| \le \varepsilon$ for that format&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. Here $\varepsilon/2$ is the maximum &#039;&#039;&#039;relative error&#039;&#039;&#039; in one rounding (also called unit roundoff). For example, when adding two FP32 numbers, the result is accurate to within about $10^{-7}$ of the true sum (relative), whereas in BF16, the result can deviate by up to $\sim7.8\times10^{-3}$ (nearly 0.78% relative error per operation)&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. Thus, &#039;&#039;&#039;each arithmetic operation incurs a small rounding error&#039;&#039;&#039; (on the order of machine epsilon) that can propagate or accumulate through a sequence of calculations.&lt;br /&gt;
&lt;br /&gt;
Because floating-point representations have finite exponent range, we must also consider &#039;&#039;&#039;overflow&#039;&#039;&#039; and &#039;&#039;&#039;underflow&#039;&#039;&#039;. If a result’s magnitude exceeds the maximum representable value, it overflows to infinity; if it falls below the minimum normal (or minimum subnormal) value, it underflows to zero (or a denormalized tiny value). FP32 and BF16 share an exponent size (8 bits), so their range for normal numbers is roughly $[10^{-38}, 10^{38}]$ (approx $~\!3.4\times10^{38}$ max)&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. FP16’s 5-bit exponent allows max $\sim 6.55\times10^4$ and min normal $\sim 6.10\times10^{-5}$&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. In practice, &#039;&#039;&#039;underflow/overflow&#039;&#039;&#039; can occur in low-precision arithmetic if values are not appropriately scaled. For instance, summing a large number of moderately sized terms in FP16 could overflow where FP32 would not. As an example, summing on the order of $10^4$ values of magnitude ~$10^1$ (say, adding 10,000 terms around 10 each) would produce a total $\sim10^5$, which exceeds FP16’s max finite value ~$6.5\times10^4$ and would overflow to $+\infty$ in half precision&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. Underflow is usually a concern when subtracting nearly equal numbers (catastrophic cancellation yielding a tiny difference that might flush to zero in low precision).&lt;br /&gt;
&lt;br /&gt;
=== 2.3. Accumulation Error in Reductions ===&lt;br /&gt;
&lt;br /&gt;
Floating-point addition is &#039;&#039;&#039;not associative&#039;&#039;&#039;: $(a+b)+c$ can differ from $a+(b+c)$ due to rounding, and thus the order in which we sum a list of numbers can change the result slightly. When summing a large sequence of $N$ numbers $x_1, x_2, ..., x_N$, the &#039;&#039;&#039;accumulated rounding error&#039;&#039;&#039; can be significant. In the &#039;&#039;worst case&#039;&#039; (e.g., adding many positive and negative terms that cause maximal cancellation or always rounding one way), the error can grow on the order of $O(N \varepsilon)$ relative to the exact sum&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;. Simply summing $N$ numbers in sequence (the &#039;&#039;&#039;naive linear summation&#039;&#039;&#039;) has a worst-case error bound proportional to $N\,\varepsilon$&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. This worst-case occurs for adversarial arrangements of values; in more typical scenarios with rounding errors of random signs, one can model the error as a random walk, yielding an expected root-mean-square (RMS) error growth on the order of $O(\sqrt{N}\,\varepsilon)$&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;. Still, with very large $N$, even $\sqrt{N}$ times $\varepsilon$ can become large enough to matter.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Cancellation&#039;&#039;&#039; is a particular problem: if we add two nearly equal numbers of opposite sign, most leading digits cancel and the result loses significant bits of precision. For example, in single precision, subtracting two numbers that agree in the first 6 decimal digits will yield a result with only ~1 digit of accuracy left. Catastrophic cancellation can dramatically increase relative error. Summation is especially sensitive to cancellation if large positive and negative terms are summed naively. The &#039;&#039;&#039;condition number&#039;&#039;&#039; of a summation (the ratio of the sum of absolute values $\sum|x_i|$ to the absolute value of the true sum $|\sum x_i|$) indicates how sensitive the result is to perturbations&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;. If the numbers have mixed signs and nearly cancel out (making the condition number large), even an optimal summation order will have a large relative error. For instance, summing [1.0, $10^{100}$, 1.0, $-10^{100}$] in double precision has an exact sum of 2.0, but naive summation may yield 0.0 due to cancellation; an improved summation algorithm yields the correct 2.0&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
In practical terms, when summing a long list of floating-point numbers (such as accumulating a dot product or reducing a tensor), the &#039;&#039;&#039;accumulation error&#039;&#039;&#039; can become noticeable if no precautions are taken. Techniques to mitigate this error include carefully choosing the summation order or using higher precision for intermediate sums. We will explore these strategies in the next section.&lt;br /&gt;
&lt;br /&gt;
== Accumulators and Reduction Strategies ==&lt;br /&gt;
&lt;br /&gt;
When performing reductions (sums, dot products, etc.), the method of accumulation can greatly affect the accuracy of the result. Below, we discuss various strategies:&lt;br /&gt;
&lt;br /&gt;
=== 3.1. Naive Linear Accumulation ===&lt;br /&gt;
&lt;br /&gt;
The simplest accumulation strategy is to use a single accumulator and add each element sequentially. In C-like pseudocode:&lt;br /&gt;
&lt;br /&gt;
This &#039;&#039;&#039;naive linear summation&#039;&#039;&#039; adds values one by one to the running total. While straightforward and fast, it is prone to the rounding error accumulation described earlier. The partial sum &amp;lt;code&amp;gt;sum&amp;lt;/code&amp;gt; continually grows (in absolute value) as terms are added, so new small terms may fall outside its precision. For example, if &amp;lt;code&amp;gt;sum&amp;lt;/code&amp;gt; is large and positive, adding a comparatively tiny positive number may produce no change because the small term&#039;s significant digits vanish in the presence of the large sum (the increment is below the rounding threshold of &amp;lt;code&amp;gt;sum&amp;lt;/code&amp;gt;). Conversely, adding a small number of opposite sign can suffer from cancellation. The order of addition matters: summing from smallest magnitude to largest (ascending order) tends to yield a more accurate result than summing in descending order, because adding small terms first preserves their contributions before the sum becomes very large. The naive loop above implicitly sums in the given index order; if the data is unsorted, this order is essentially arbitrary.&lt;br /&gt;
&lt;br /&gt;
The performance of linear accumulation is excellent (minimal overhead), and for many applications the resulting error is within acceptable limits. However, for &#039;&#039;&#039;very large N&#039;&#039;&#039; or ill-conditioned sums, the inaccuracy can become a problem. In a reduction involving millions of elements, a single-precision sum could lose several digits of accuracy. In deep learning, this could translate to small discrepancies in layer outputs or aggregated gradients if done naively in low precision.&lt;br /&gt;
&lt;br /&gt;
=== 3.2. Pairwise and Tree-Based Reductions ===&lt;br /&gt;
&lt;br /&gt;
A better approach to summation is &#039;&#039;&#039;pairwise summation&#039;&#039;&#039;, which sums numbers in a binary tree structure rather than linearly. The idea is to recursively split the array into halves, sum each half, then add the two partial sums. This can be implemented as:&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
By always adding numbers of similar magnitudes first (small subsets are reduced to subtotals, then those are added, etc.), pairwise summation avoids the situation of a very large running total absorbing small additions until the final steps. The error of pairwise summation grows logarithmically with $N$ in the worst case (roughly $O(\varepsilon \log_2 N)$)&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;, instead of linearly. Intuitively, at each level of the tree, rounding error is constrained, and there are about $\log_2 N$ levels. In fact, the worst-case relative error bound for pairwise summation (base case of 1 element) is:&lt;br /&gt;
&lt;br /&gt;
\[ |E_N| \le \frac{\varepsilon \log_2 N}{1 - \varepsilon \log_2 N} \sum_{i=1}^N |x_i|, \]&lt;br /&gt;
&lt;br /&gt;
which for practical $N$ simplifies to on the order of $\varepsilon \log_2 N$ times the condition number of the sum&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;. In typical random-error scenarios, the error behaves even better (on the order of $\sqrt{\log N}$)&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;. The key is that &#039;&#039;&#039;pairwise (tree) reduction balances the accumulations&#039;&#039;&#039;, preventing one sum from overwhelming the others too early.&lt;br /&gt;
&lt;br /&gt;
From a performance standpoint, pairwise summation has the same $O(N)$ total additions as linear summation, just a different order. It is also highly amenable to parallelization: independent partial sums can be computed in parallel at each level of the tree. Many numerical libraries and BLAS implementations use a form of blocked or pairwise summation in dot products for improved accuracy at negligible performance cost. For example, NumPy and Julia perform partial pairwise summation by default for better numerical behavior&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=== 3.3. Compensated Summation (Kahan, Neumaier) ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Compensated summation&#039;&#039;&#039; is an alternative technique to improve summation accuracy by tracking the small “lost” quantities. The most famous algorithm is the &#039;&#039;&#039;Kahan summation algorithm&#039;&#039;&#039; (also called Kahan–Babuška)&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. Kahan summation uses an extra variable to accumulate the round-off error. In pseudocode:&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Here &amp;lt;code&amp;gt;c&amp;lt;/code&amp;gt; accumulates the tiny leftovers that would otherwise be dropped&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. Each iteration essentially subtracts the last error from the new addend (&amp;lt;code&amp;gt;y = x[i] - c&amp;lt;/code&amp;gt;) so that &amp;lt;code&amp;gt;t = sum + y&amp;lt;/code&amp;gt; includes the small bits from the previous operation. The new error is then stored in &amp;lt;code&amp;gt;c&amp;lt;/code&amp;gt; for the next iteration. The result is that the final &amp;lt;code&amp;gt;sum&amp;lt;/code&amp;gt; is as if computed with higher precision than the machine’s: Kahan’s algorithm can &#039;&#039;&#039;greatly reduce the error&#039;&#039;&#039; in summing a sequence of floats. In fact, with exact arithmetic for the compensation, Kahan summation yields a result accurate to within 1 or 2 ULP of the true sum, regardless of N (assuming the sequence of partial sums doesn’t overflow)&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. More precisely, Kahan summation achieves a &#039;&#039;&#039;worst-case error independent of N&#039;&#039;&#039; (error bound depends only on machine precision, not the number of terms)&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. In practice, using a double accumulator for summing single-precision numbers via Kahan can give near double-precision accuracy.&lt;br /&gt;
&lt;br /&gt;
An important subtlety is that the compensation variable &amp;lt;code&amp;gt;c&amp;lt;/code&amp;gt; should be kept in &#039;&#039;full precision&#039;&#039;. In the implementation above, we used &amp;lt;code&amp;gt;double&amp;lt;/code&amp;gt; for both sum and c. If summing FP32 values, one might use &amp;lt;code&amp;gt;float sum&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;float c&amp;lt;/code&amp;gt; (both 32-bit), which still improves accuracy, but using a higher precision for the accumulator and compensation (i.e. double) yields even better results. Many implementations of Kahan summation use the same type for sum and c, but it&#039;s assumed to be higher precision than the inputs when possible.&lt;br /&gt;
&lt;br /&gt;
Kahan’s algorithm carries a performance cost: each iteration does five arithmetic operations (two adds, one subtract, etc.) and has a data dependency, which can hinder parallelism or pipelining&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. Thus, it runs slower than naive summation (roughly 2–3× slower or more, depending on CPU pipeline). In loops where summation is a bottleneck, this may be significant. Nonetheless, for modest N or where maximum accuracy is required, compensated summation is extremely useful. In fact, Python’s &amp;lt;code&amp;gt;math.fsum&amp;lt;/code&amp;gt; function uses a variant of Kahan’s algorithm (actually a more advanced multi-part compensation) to sum iterables with high precision.&lt;br /&gt;
&lt;br /&gt;
There are variations and improvements on basic Kahan summation. &#039;&#039;&#039;Neumaier’s algorithm&#039;&#039;&#039; (sometimes called &#039;&#039;Kahan-Babuška-Neumaier&#039;&#039;) improves Kahan’s method by also handling cases where the next input is larger in magnitude than the running sum&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. In such cases, the original Kahan formula could suffer error; Neumaier’s version effectively swaps roles when a huge value comes in. Its pseudocode is:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight&amp;gt;&lt;br /&gt;
xyz&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This algorithm ensures the compensation covers both scenarios: when the sum is the bigger addend or when the new term is bigger&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. In the end, it adds the final &amp;lt;code&amp;gt;compensation&amp;lt;/code&amp;gt; once to the result. Neumaier’s method can produce the correct result in cases where basic Kahan fails. For instance, summing [1.0, $+10^{100}$, 1.0, $-10^{100}$] in double precision: regular Kahan summation might give 0.0 (losing the small terms), whereas Neumaier’s algorithm yields 2.0 exactly&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. There are also higher-order compensation methods (carrying multiple compensation terms, e.g., the &#039;&#039;&#039;Klein summation&#039;&#039;&#039; variant&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt; or using arbitrary precision for intermediate sums) that further reduce error at increased computational cost&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
In summary, compensated summation algorithms trade extra computation for greatly improved numerical accuracy. They are especially valuable when summing large arrays of numbers with varying magnitudes, where naive summation could lose most of the low-order bits. Many scientific computing environments provide compensated sum routines for this reason. In deep learning, however, a different approach to improved precision is often taken: &#039;&#039;&#039;using a wider accumulator type&#039;&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
=== 3.4. Mixed-Precision Accumulation ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Mixed-precision accumulation&#039;&#039;&#039; refers to performing arithmetic in a higher precision than the input data. A common scenario is accumulating sums in 32-bit floats when the operands are 16-bit floats. Rather than implementing a compensation algorithm, one can simply use a wider accumulator so that intermediate rounding error is smaller. This is effectively what modern GPUs do for tensor operations: for example, NVIDIA &#039;&#039;&#039;Tensor Cores operate on FP16 input data with an FP32 accumulator&#039;&#039;&#039; (the 16-bit products are accumulated in 32-bit)&amp;lt;ref name=&amp;quot;ref6&amp;quot;/&amp;gt;. In this way, summing 2048 products in a dot product has far less precision loss than it would with a 16-bit accumulator. Hardware that supports mixed precision can give the best of both worlds: fast low-precision multiplies and high-precision accumulation.&lt;br /&gt;
&lt;br /&gt;
Software can also leverage mixed precision. For instance, one can sum an array of &amp;lt;code&amp;gt;float&amp;lt;/code&amp;gt; values into a &amp;lt;code&amp;gt;double&amp;lt;/code&amp;gt; variable, accumulating the result in double precision and then converting back to float at the end:&lt;br /&gt;
&lt;br /&gt;
This approach often yields a more accurate &amp;lt;code&amp;gt;result&amp;lt;/code&amp;gt; than summing in single precision directly, because the intermediate sum carries 52-bit precision. The final rounding to float happens only once at the end (minimizing overall rounding error). In fact, the above is one way to implement &amp;lt;code&amp;gt;math.fsum&amp;lt;/code&amp;gt; in Python (it internally does something similar with multiple partial sums in double precision).&lt;br /&gt;
&lt;br /&gt;
In machine learning training and inference, &#039;&#039;&#039;mixed precision&#039;&#039;&#039; is widely used. When using FP16 for speed, it&#039;s common to accumulate with FP32. For example, &#039;&#039;&#039;Google&#039;s TPUs use BFloat16 for multiplication but accumulate in FP32&#039;&#039;&#039; for matrix ops&amp;lt;ref name=&amp;quot;ref6&amp;quot;/&amp;gt;. AWS’s Inferentia (Neuron) hardware similarly supports FP16/BF16 matrix multiplication with FP32 accumulation&amp;lt;ref name=&amp;quot;ref7&amp;quot;/&amp;gt;. The motivation is that summing hundreds or thousands of half-precision values would otherwise introduce intolerable error or risk overflow. With an FP32 accumulator, the reduction error is drastically reduced (by about $2^{13}$ times, since FP32 has 13 more precision bits than FP16). Mixed-precision accumulation thus provides a simple and effective form of compensation: &#039;&#039;&#039;use a larger bucket to collect many small contributions&#039;&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
There is a spectrum of options: summing int8 or int16 integers might use 32-bit integers as accumulators for the same reason (to avoid overflow). Some BLAS libraries even offer &#039;&#039;extended precision accumulators&#039;&#039; for single precision dot products (accumulating in double). The downside of mixed precision is the extra memory and maybe throughput cost of the wider type. But on many platforms, the add latency is dominated by hardware design rather than bit-width, so using a 32-bit adder vs 16-bit has negligible speed impact if supported natively. The key is whether data movement or register pressure increases.&lt;br /&gt;
&lt;br /&gt;
In summary, &#039;&#039;&#039;using a higher precision accumulator is often the easiest way to improve numerical accuracy&#039;&#039;&#039; for reductions. It is a primary strategy in deep learning hardware to maintain stability while exploiting low precision for storage and initial computations.&lt;br /&gt;
&lt;br /&gt;
== Precision Formats in AI and Machine Learning ==&lt;br /&gt;
&lt;br /&gt;
Modern AI systems use a variety of numeric formats to balance speed, memory, and accuracy. We discuss common precisions and their roles:&lt;br /&gt;
&lt;br /&gt;
=== 4.1. FP32 and FP64 in Training and Inference ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;FP32 (single precision)&#039;&#039;&#039; has been the workhorse of deep learning for years. Most training scripts in frameworks like TensorFlow and PyTorch used FP32 by default, as it offers sufficient precision to propagate gradients and accumulate weight updates over millions of iterations without excessive error. With ~7 decimal digits of precision and a wide exponent range, FP32 can handle the dynamic range of activations and gradients in typical neural networks. In inference, FP32 is often more precision than strictly necessary for final predictions, but it has remained common because of its ease and the fact that many GPU operations were optimized for FP32.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;FP64 (double precision)&#039;&#039;&#039; is rarely used in deep learning model training or inference, except in specific scenarios requiring extreme precision. FP64 is common in scientific computing and certain types of numerical simulations. In neural networks, however, the extra precision is usually not needed for model accuracy—using doubles tends to yield negligible improvements in model metrics but at a large performance cost (since on GPUs FP64 throughput can be 1/2, 1/8, or even 1/32 of FP32 throughput depending on hardware). One exception might be when integrating neural nets into larger physics simulations or when solving ill-conditioned problems as part of the model. Some researchers use FP64 to diagnose numerical issues or ensure reproducibility. Generally though, deep learning has favored lower precision for efficiency, and FP64 is considered overkill for most networks&amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
That said, certain classical machine learning algorithms benefit from double precision. For example, computing the inverse of a matrix (as in a Gaussian Process regression or Kalman filter embedded in a model) can incur large rounding errors in single precision, so using FP64 can significantly improve the result&amp;lt;ref name=&amp;quot;ref1&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref1&amp;quot;/&amp;gt;. The ONNX standard supports a float64 tensor type for such needs. In ONNX Runtime, however, emphasis has been on FP32 performance, and some pipelines default to float even if the original model was double&amp;lt;ref name=&amp;quot;ref1&amp;quot;/&amp;gt;. If a model’s prediction computation involves operations that are numerically sensitive (like matrix inversion, Cholesky decomposition, or summing extremely large counts), then FP64 may be required to avoid noticeable discrepancies&amp;lt;ref name=&amp;quot;ref1&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref1&amp;quot;/&amp;gt;. A good practice is to test a model in both float and double if you suspect precision issues; significant differences indicate an ill-conditioned calculation that might need higher precision.&lt;br /&gt;
&lt;br /&gt;
In summary, FP32 remains the &#039;&#039;&#039;default&#039;&#039;&#039; format for most training and inference due to its balance of accuracy and efficiency&amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;, while FP64 is reserved for niche cases or precision-critical subroutines. FP32 typically achieves the same accuracy as FP64 for end-to-end deep learning tasks, because network training can often compensate for small noise, and other sources of error (like generalization error) dominate. Therefore, the community trend has been moving to &#039;&#039;&#039;lower&#039;&#039;&#039; precisions, not higher, to gain speed — provided the models still converge and perform well.&lt;br /&gt;
&lt;br /&gt;
=== 4.2. FP16 and BFloat16 in Deep Learning ===&lt;br /&gt;
&lt;br /&gt;
To further speed up neural network training and inference, &#039;&#039;&#039;half-precision (FP16)&#039;&#039;&#039; was adopted, especially with the advent of GPUs supporting FP16 math at twice the rate of FP32. Using FP16 reduces memory storage and bandwidth by 50% compared to FP32, which is significant for large models. However, FP16’s small exponent range and precision posed challenges: training a large network purely in FP16 can fail due to gradient underflow/overflow or weight updates not registering (because changes are below the 0.001 quantization step). To address this, frameworks introduced &#039;&#039;&#039;mixed-precision training&#039;&#039;&#039;, where compute-intensive parts (matrix multiplies, convolutions) run in FP16, but certain accumulations and model parameters remain in FP32. For instance, NVIDIA’s &#039;&#039;&#039;Tensor Core&#039;&#039;&#039; units require FP16 (or BF16) inputs but accumulate results in FP32, effectively ensuring that reductions are done in higher precision&amp;lt;ref name=&amp;quot;ref6&amp;quot;/&amp;gt;. Research showed that with proper loss scaling and mixed precision, models can train to FP32-level accuracy using mostly FP16 arithmetic, achieving significant speedups.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;BFloat16 (BF16)&#039;&#039;&#039; emerged as an alternative 16-bit format primarily from Google’s TPU training systems. BF16 has a larger exponent (8 bits, same as FP32) but only 7 fraction bits&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. This means BF16 can handle the &#039;&#039;&#039;same dynamic range&#039;&#039;&#039; as FP32 (no overflows where FP32 wouldn’t) but at the cost of even lower precision than FP16 (BF16 has about 2–3 decimal digits of precision vs FP16’s ~3–4). Despite its coarse precision, BF16 turns out to be effective for training deep networks because the algorithms (stochastic gradient descent, etc.) are somewhat robust to that noise, and having the wide range prevents catastrophic loss of signal (no gradient will overflow to Inf unless it was going to in FP32 anyway). Many training frameworks (PyTorch, TensorFlow) support BF16 training on hardware like &#039;&#039;&#039;TPU, NVIDIA A100+, Intel CPUs with AVX-512 BF16, etc&#039;&#039;&#039;. Typically, when using BF16, &#039;&#039;&#039;accumulations are still done in FP32&#039;&#039;&#039; (for example, a TPU v2/v3 multiplies BF16 matrices and accumulates into an FP32 result for the dot product&amp;lt;ref name=&amp;quot;ref6&amp;quot;/&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
In inference, both FP16 and BF16 are used to deploy models for faster speed or lower memory. FP16 is popular on NVIDIA GPUs (which have specialized FP16 units). BF16 is advantageous on TPUs and some CPUs that support it, and increasingly on GPUs (Ampere and newer GPUs also support BF16 arithmetic). The choice between FP16 and BF16 can depend on hardware: BF16’s advantage is easier software conversion (you can often take an FP32-trained model and cast weights to BF16 with minimal loss, since the dynamic range is intact), whereas FP16 might require adjusting the model or ensuring values fit in range (sometimes needing techniques like maintaining a master copy of weights in FP32 during training, or handling out-of-range values).&lt;br /&gt;
&lt;br /&gt;
For ONNX, both Float16 and BFloat16 types are part of the standard. ONNX models can be converted to FP16 (truncating weights and inserting cast nodes for computations) using tools like &amp;lt;code&amp;gt;float16_converter&amp;lt;/code&amp;gt; in ONNX Runtime&amp;lt;ref name=&amp;quot;ref8&amp;quot;/&amp;gt;. ONNX Runtime on GPUs will typically execute those FP16 ops with mixed precision on tensor cores (FP16 input, FP32 accumulate), whereas on CPU it may emulate FP16 or upcast to FP32 (as many CPU implementations do not natively support half-precision math)&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;. In fact, as an example, running an ONNX model in FP16 on a CPU might insert hidden Cast operations to FP32 behind the scenes, because the CPU kernel for that op only exists in FP32&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;. This ensures correctness (the computation is done in higher precision) but can reduce performance. We will revisit such backend-dependent behavior in Section 5.3.&lt;br /&gt;
&lt;br /&gt;
In summary, &#039;&#039;&#039;FP16 and BF16&#039;&#039;&#039; are now established as important low-precision formats in deep learning. FP16 offers more precision bits, which can matter for e.g. small differences, but has a limited range (care needed to avoid overflow). BF16 offers safety in range but less precision (which can slightly affect convergence or final accuracy in some cases). Both typically rely on FP32 for accumulation to achieve acceptable accuracy. When deploying models, one should choose a format supported efficiently by the target hardware and verify that the accuracy is not significantly degraded. Often, CNNs and transformers can run in FP16/BF16 with virtually no drop in score, but certain operations (like softmax, see Section 6.3) might need special handling.&lt;br /&gt;
&lt;br /&gt;
=== 4.3. FP8 Formats (E4M3, E5M2) and Scaling ===&lt;br /&gt;
&lt;br /&gt;
The push for ever lower precision has led to experimental &#039;&#039;&#039;8-bit floating point (FP8)&#039;&#039;&#039; formats for deep learning. In 2022, researchers from NVIDIA, Intel, and Arm proposed an FP8 standard with two variants: &#039;&#039;&#039;E4M3&#039;&#039;&#039; (4 exponent bits, 3 fraction bits) and &#039;&#039;&#039;E5M2&#039;&#039;&#039; (5 exponent bits, 2 fraction bits)&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;. Both have 1 sign bit, for a total of 8 bits. E5M2 has a wider exponent range (5 bits gives bias 15) but one fewer mantissa bit than E4M3. The idea is to use E4M3 for numbers that need more precision (like weights or activations around 1.0) and E5M2 for numbers that need more dynamic range (like gradient updates which can be very small or large). In fact, the study found that using E4M3 for forward activations/weights and E5M2 for backward gradients can allow training of large networks with FP8 to accuracy on par with FP16 training&amp;lt;ref name=&amp;quot;ref11&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
FP8 formats are &#039;&#039;not&#039;&#039; IEEE 754 standard (though IEEE is considering lower precision in its 2019 revision). Notably, the E4M3 format as used by NVIDIA does not allocate any bit-pattern for $+\infty$ or $-\infty$ – it has only NaN for invalid results, using that freed bit pattern to extend the representable range by one exponent value&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;. Essentially, E4M3 can represent numbers up to ~$2^{15}$ (around 3e4) instead of capping at infinity at $2^{16}$, by foregoing infinities&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;. Variations also exist regarding negative zero and how NaNs are handled&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;. For instance, GraphCore’s IPU uses FP8 formats that disallow negative zero and infinities, to maximize useable range&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Using 8-bit floats for neural networks requires additional techniques, chiefly &#039;&#039;&#039;scaling&#039;&#039;&#039;. Because 8-bit has very limited precision and range, one typically applies a power-of-two scale factor to groups of values to normalize them into an FP8-friendly range. This is akin to block floating-point or “microscaling” per layer or per tensor. For example, one might keep a FP32 scale for each channel or tensor, and represent values as $(FP8\_value) \times 2^{scale}$. The ONNX standard has introduced support for FP8 in version 1.15, including types for E4M3 and E5M2 (with variations FNUZ: finite-only and no negative zero)&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;, and even a type called &#039;&#039;&#039;E8M0&#039;&#039;&#039; which is an 8-bit exponent-only number used purely as a scale factor&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;. The E8M0 type has 8 exponent bits, no mantissa, effectively representing powers of two from $2^{-127}$ to $2^{128}$ for scaling purposes&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;. This indicates an approach where an FP8 tensor would be accompanied by an E8M0 scale (or scales) to interpret its value correctly.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Rounding&#039;&#039;&#039; becomes critical when casting values to FP8. The quantization error is much larger relative to FP16 or FP32. Studies have shown that choosing the right rounding mode (e.g., round-to-nearest, stochastic rounding, etc.) and clamping behavior (saturating to max instead of inf) can affect model accuracy&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;. ONNX added a &amp;lt;code&amp;gt;round_mode&amp;lt;/code&amp;gt; attribute to its Cast operator to allow controlling this for FP8&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;. In particular, one paper found that &#039;&#039;&#039;rounding up with saturation&#039;&#039;&#039; (round-to-nearest ties away from 0, and clamp overflows to max finite rather than inf) gave better accuracy in large language model training than other rounding modes&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
At present, FP8 is on the cutting edge. NVIDIA’s H100 GPU supports FP8 tensor cores (for both E4M3 and E5M2), and early results show minimal accuracy loss on some vision and NLP tasks when properly tuned. However, using FP8 requires fine-tuning hyperparameters like scaling factors for each layer and perhaps tweaking training procedures. ONNX’s inclusion of FP8 types means you can represent and transport models using FP8 weights or activations, but actual hardware support is limited and typically paired with calibration or dynamic scaling logic outside of ONNX graph (or using custom ONNX functions for scaling). It remains an &#039;&#039;&#039;open research area&#039;&#039;&#039; how to fully automate FP8 quantization for arbitrary models without accuracy loss.&lt;br /&gt;
&lt;br /&gt;
In summary, FP8 formats promise further compression of neural network computation and data. They are an order of magnitude more error-prone than FP16, but when combined with careful scaling, mixed usage of E4M3/E5M2, and maybe modified training techniques, they have shown surprising success. ONNX is actively evolving to support these ultra-low precision types, signaling their potential importance in future AI inference deployments.&lt;br /&gt;
&lt;br /&gt;
== Accumulation Semantics in ONNX ==&lt;br /&gt;
&lt;br /&gt;
One of the roles of the ONNX specification is to define the mathematical behavior of operators (Conv, MatMul, Sum, etc.) in a framework-agnostic way. However, ONNX mostly defines &#039;&#039;what&#039;&#039; an operator computes in exact arithmetic, not &#039;&#039;how&#039;&#039; the computation is carried out in finite precision. This leaves certain details – like accumulator precision or summation order – up to the implementation (runtime or hardware). We&#039;ll examine how ONNX treats accumulation and what guarantees (or lack thereof) are provided.&lt;br /&gt;
&lt;br /&gt;
=== 5.1. Operator Definitions and Numerical Guarantees ===&lt;br /&gt;
&lt;br /&gt;
Each ONNX operator is described as performing an ideal mathematical function on its inputs. For example, &amp;lt;code&amp;gt;MatMul&amp;lt;/code&amp;gt; is defined as the standard matrix product of two tensors, and &amp;lt;code&amp;gt;ReduceSum&amp;lt;/code&amp;gt; computes a summation along given axes. The ONNX spec, as of current versions, &#039;&#039;&#039;does not mandate the precision of intermediate calculations&#039;&#039;&#039; beyond the types of the inputs and outputs. If an operator’s inputs and outputs are float16, the spec implies the computation is done in float16 in a sense, but it does not forbid an implementation from using higher precision internally – it simply doesn&#039;t mention it. There is &#039;&#039;&#039;no explicit guarantee of identical bit-for-bit results&#039;&#039;&#039; across different ONNX backends for floating-point ops. Instead, results are expected to be numerically close, within tolerances one would normally expect from floating-point differences (due to different hardware or algorithms). The ONNX Backend Test suite typically uses a relative or absolute error tolerance when validating outputs for this reason.&lt;br /&gt;
&lt;br /&gt;
Because of this, important details like the use of extended precision or accumulation in higher precision are left ambiguous. This has led to discussions in the ONNX community about making such behavior more explicit. For example, a proposal was raised to allow specification of accumulation precision for certain ops like MatMul&amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;. The reasoning is that as lower-precision types (float16, bfloat16, float8) become more common, knowing or controlling whether an op internally accumulates in a higher precision is important for both accuracy and reproducibility&amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;. In June 2025, an issue on the ONNX GitHub suggested adding an attribute to MatMul (and similarly Gemm, Softmax, etc.) to specify the accumulator type&amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;. By default it would use input precision (status quo), but an attribute could request (for instance) 32-bit accumulation even if inputs are 16-bit&amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;. As of this writing, however, no such attribute exists in the official spec – it&#039;s an open issue. Thus, the numerical semantics are effectively: &#039;&#039;“compute the result as if in the same type as inputs, but actual implementations may use more precision internally as an optimization.”&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
From a guarantee standpoint, ONNX operators promise the mathematically correct result assuming real-number arithmetic, but they &#039;&#039;&#039;do not guarantee bit-exact results&#039;&#039;&#039; due to floating-point effects. There is an understanding that differences can arise from different summation orders, fused operations (e.g., an implementation might use an FMA (fused multiply-add) which has one rounding instead of two), or use of extended precision registers. ONNX does not formally define rounding modes either – it relies on IEEE 754 defaults, but if a backend used something like deterministic summation or alternative rounding, ONNX has no mechanism to express that.&lt;br /&gt;
&lt;br /&gt;
In summary, ONNX&#039;s current stance is largely &#039;&#039;&#039;implementation-dependent&#039;&#039;&#039; for numerical precision. It provides minimal explicit numerical guarantees beyond type constraints. This is why an ONNX model run on two different hardware or runtime libraries can have slightly different results, especially in reduced precision. The burden is on backends to produce results &amp;quot;close enough&amp;quot; to each other that they don&#039;t affect the model&#039;s decisions in any significant way.&lt;br /&gt;
&lt;br /&gt;
=== 5.2. MatMul, Conv, and Reduction Operators ===&lt;br /&gt;
&lt;br /&gt;
Consider ONNX &amp;lt;code&amp;gt;MatMul&amp;lt;/code&amp;gt;: it takes two tensors (often two matrices) and produces their matrix product. In pure math, $C_{ij} = \sum_k A_{ik} &#039;&#039; B_{kj}$. If A and B are float16, should that sum be computed in float16 or float32? As discussed, ONNX doesn’t specify – it simply says the output is a float16 matrix containing the product. In practice, many backends will &#039;&#039;promote&#039;&#039; the computation: for example, NVIDIA GPUs will do the multiplication and addition in a mixed FP16/FP32 mode by default (because of tensor cores), then convert the final result back to float16. ONNX Runtime on CPU might not support float16 multiplication natively, so it will insert cast ops to float32, perform the MatMul in float32, then cast back to float16&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;. From ONNX’s perspective, as long as the final result is within acceptable error of the “ideal” float16 computation, it’s fine. But note, an “ideal” float16 computation (where every multiply and add is rounded to FP16) might actually be &#039;&#039;less accurate* than what the backend did with FP32. This means that a backend using higher precision can produce a result that is not bit-equal to a pure FP16 implementation – it&#039;s actually closer to the true real number result. This is usually considered a good thing (improved accuracy), but it does highlight a lack of consistency: another backend might do everything in pure FP16 and get a slightly different answer.&lt;br /&gt;
&lt;br /&gt;
The same situation occurs for &#039;&#039;&#039;convolution&#039;&#039;&#039; (&amp;lt;code&amp;gt;Conv&amp;lt;/code&amp;gt; op) which is essentially a batched sum of products (like MatMul in sliding window), and for explicit reduction ops like &amp;lt;code&amp;gt;ReduceSum&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;ReduceMean&amp;lt;/code&amp;gt;, etc. If you have an ONNX &amp;lt;code&amp;gt;ReduceSum&amp;lt;/code&amp;gt; that sums 1000 float16 values, the spec doesn’t say how to sum them. A GPU kernel might sum in a tree using FP32 accumulators; a naive implementation might sum in order with FP16 and possibly overflow or underflow intermediate steps. Both are allowed as long as they output a float16 result that is “reasonable.” In practice, ONNX Runtime and others take care to avoid obviously bad behavior (like overflow) by using higher precision or by splitting the sum. But again, it’s not mandated by the ONNX standard explicitly.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Softmax&#039;&#039;&#039; is an interesting case: Softmax involves an internal sum of exponentials. ONNX defines the Softmax operator as $$\mathrm{Softmax}(x)_i = \frac{\exp(x_i)}{\sum_j \exp(x_j)},$$ typically applied along a specified axis. Directly computing this in float16 can be problematic for large vectors, as discussed in Section 6.3. Indeed, the issue of accumulation precision arises: should the sum $\sum_j \exp(x_j)$ be done in FP16 or can it be in FP32? Many implementations will do it in FP32 even if $x$ is FP16, because the dynamic range of $\exp(x)$ is large. The ONNX Softmax spec doesn’t detail this, but it expects the result to be a proper probability distribution (summing to 1.0 within normal floating error). A pure FP16 softmax on, say, 1000 elements could sum to something like 0.996 or 1.004 due to rounding, whereas FP32 accumulation would be much closer to 1.000. ONNX likely allows both; the small difference is usually inconsequential, but if it were large (say the FP16 sum overflowed to inf), that would be a correctness issue. A robust backend will prevent that (by using the common max-subtraction trick and higher precision accumulation). The ONNX issue 7072 indeed calls out Softmax as another op where explicit control of accumulation precision would be useful&amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
In summary, &#039;&#039;&#039;MatMul, Conv, and reduction ops&#039;&#039;&#039; in ONNX are defined mathematically but with &#039;&#039;implicit&#039;&#039; numerical behavior. Implementations commonly use higher precision for intermediate sums especially when input is low precision, but this is not standardized. Consequently, results can differ slightly between implementations. For critical applications, one must be aware of these differences. A model exported to ONNX with float16 weights might perform slightly differently on one runtime vs another if one does all float16 math and the other uses float32 accumulations. Both conform to ONNX. As a user, if you require a certain precision for accumulation, currently you have to ensure it by the way you export or run the model (for example, insert manual cast nodes in the ONNX graph to force certain ops to FP32). We may see future ONNX versions address this with attributes or metadata as mentioned, but for now these ops are effectively “compute with best effort accuracy”.&lt;br /&gt;
&lt;br /&gt;
=== 5.3. Backend and Hardware-Dependent Behavior ===&lt;br /&gt;
&lt;br /&gt;
Because ONNX is implemented by many backends (ONNX Runtime, TensorRT, CoreML, PyTorch JIT, etc.), and these in turn run on varied hardware (CPU, GPU, accelerators), the numeric behavior can be backend-dependent. Some examples:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;CPU backends:&#039;&#039;&#039; Many CPUs do not have native float16 vector arithmetic (with a few exceptions in recent CPUs). ONNX Runtime’s default CPU execution provider will often upcast float16 to float32, compute, then downcast&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;. This ensures accuracy but means that if you thought you were getting a full float16 pipeline, you’re not – the CPU is essentially ignoring the low precision in compute. On the other hand, x86 CPUs have extended 80-bit precision in x87 registers (though less used now) and 64-bit SIMD registers for doubles; some compiler optimizations might carry intermediate results in higher precision. This could make CPU results slightly different from a GPU’s if, say, a sum is done in 80-bit on CPU versus 32-bit on GPU. Generally, modern compilers avoid such extended precision unless explicitly asked, to improve reproducibility.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;GPU backends:&#039;&#039;&#039; GPUs often perform summations in parallel using tree reductions. The order of operations can change depending on how threads are scheduled. This means even on the same GPU, summing an array in one launch versus another might not give bit-identical results if the internal parallel reduction algorithm differs (though it should be deterministically the same given the same library version and input, but if you change GPU model or BLAS library, it might differ). Also, GPUs have fused multiply-add (FMA) instructions that do $(a*b + c)$ in one step with one rounding, which can produce slightly different results than doing the multiply and add separately (which would round twice). If an ONNX backend uses a fused kernel (like a GEMM library) it might yield different low-order bits than a simple loop.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Vendor libraries:&#039;&#039;&#039; ONNX Runtime can use vendor BLAS or DNN libraries (MKL, cuDNN, TensorRT, etc.). These libraries often implement their own strategies for numerical stability. For example, NVIDIA’s cuDNN might use FP32 accumulation for FP16 inputs by default. TensorRT (NVIDIA’s inference engine) will sometimes automatically promote precisions or insert scaling to maintain stability&amp;lt;ref name=&amp;quot;ref13&amp;quot;/&amp;gt;. On the other hand, if you explicitly request FP16 in TensorRT with no FP32 fallback, it might do the entire op in FP16 and you’d see more error. Some libraries offer knobs: TensorRT allows choosing FP32, FP16, or INT8 precision for each layer, and will use FP32 internally for safety in some cases unless forced otherwise.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Hardware-specific quirks:&#039;&#039;&#039; As another example, NVIDIA’s &#039;&#039;&#039;TensorFloat-32 (TF32)&#039;&#039;&#039; on Ampere GPUs is a precision mode where matrix multiplications use 10-bit mantissa (so about FP16 precision for the multiply) but keep 8-bit exponent (range like FP32). By default, CUDA libraries use TF32 for FP32 matrix multiplies on Ampere to speed them up, unless you disable it. This means if you run an ONNX model with FP32 on an A100 GPU, your matmuls might actually be lower precision than FP32 (though higher than FP16)&amp;lt;ref name=&amp;quot;ref6&amp;quot;/&amp;gt;. It’s an example of hardware choosing an intermediate precision for performance. From ONNX’s perspective, the output is still FP32 and (hopefully) within error tolerances, but a user might be surprised that it’s not a “true” FP32 computation internally. If exactness is needed, one has to turn off TF32.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Non-determinism:&#039;&#039;&#039; Some backends (especially on GPUs) allow non-deterministic execution for performance. For instance, atomic adds in parallel reductions can produce results that vary run-to-run because the order of accumulation is race-condition-dependent. ONNX’s spec doesn’t cover this explicitly, but it’s something to consider. In ONNX Runtime, there are settings for deterministic computing, but by default it may use all available performance optimizations, even if results vary at the 1e-7 level.&lt;br /&gt;
&lt;br /&gt;
In short, the numerical behavior under ONNX can depend on the combination of backend software and hardware capabilities. The lack of a strict spec for accumulation means implementations have freedom to use clever strategies (higher precision, reordering) to improve accuracy or speed. Usually this is positive (e.g. getting better accuracy than a naive approach). But it could also lead to slight inconsistencies. For example, you might find that exporting a PyTorch model to ONNX and running it with one engine yields outputs slightly different from PyTorch’s – not because ONNX is wrong, but because the summation or convolution was done differently (maybe PyTorch summed in FP32 as well, but maybe not in exactly the same way).&lt;br /&gt;
&lt;br /&gt;
The key takeaway is that ONNX &#039;&#039;by design&#039;&#039; abstracts away these details, which is good for portability but means you need to &#039;&#039;&#039;trust the backend&#039;&#039;&#039; or verify its precision behavior for sensitive models. If necessary, you can enforce certain behavior by inserting ops (for example, if you want to ensure FP32 accumulation, you could insert a cast to FP32 for inputs of a MatMul and cast the result back to FP16 – this way the ONNX graph itself mandates FP32 compute in between). This of course sacrifices some performance or device-specific magic.&lt;br /&gt;
&lt;br /&gt;
Looking forward, as mentioned, there are proposals to enrich ONNX’s specification regarding precision. This might include explicit flags for “exact summation” or “accumulate in int32” etc., which would make models more self-descriptive but also potentially less portable (if a backend doesn’t support that, what then?). For now, users should be aware of these backend differences. Most of the time, well-engineered ONNX runtimes yield results close enough that they don&#039;t affect the application (e.g., classification top-1 remains the same even if low-level sums differ by 1e-5). But for high-stakes numerical computations, testing and maybe adjusting the model or backend settings is prudent.&lt;br /&gt;
&lt;br /&gt;
== Numerical Stability in Deep Learning Models ==&lt;br /&gt;
&lt;br /&gt;
Deep learning computations involve many operations that, while theoretically well-defined, can be sensitive to numerical issues when implemented in finite precision. We&#039;ll discuss a few common scenarios in neural networks where numerical stability is a concern, and how precision and accumulation play a role.&lt;br /&gt;
&lt;br /&gt;
=== 6.1. Dot Products and Large-Scale Reductions ===&lt;br /&gt;
&lt;br /&gt;
Neural networks are full of dot products: every fully-connected layer computes $\mathbf{y} = W\mathbf{x}$, which is a series of inner products between weight vectors and the input. Convolutional layers compute weighted sums of local regions. Attention mechanisms compute large matrix products of query/key/value matrices. All these boil down to summing a lot of terms: $\sum_{k=1}^K a_k b_k$. When $K$ is large (it could be in the thousands or more for a single neuron in a fully connected layer of a large model), the potential for floating-point error in that sum increases.&lt;br /&gt;
&lt;br /&gt;
If using FP32, the error may still be negligible in many cases, but in lower precision like FP16, it can become significant. For example, consider a dot product of length 2048 in FP16. Each product is in FP16 (with ~3 decimal digits precision) and if accumulated in FP16, the worst-case error could be on the order of $2048 \times \varepsilon_{FP16} \approx 2048 \times 0.0009766 \approx 2$ (i.e., you could lose a couple units in the third decimal place relative accuracy in worst case). Even in random error accumulation, you might see error on the order of $\sqrt{2048} \times 0.0009766 \approx 0.044$ or 4.4% (though this is a rough estimate). That’s quite significant. Fortunately, as discussed, most hardware will accumulate such sums in a higher precision like FP32, dramatically reducing that error (~7 orders of magnitude smaller $\varepsilon$). But if it didn’t, the network’s outputs could get noisy. A fully-connected layer with many inputs might output slightly different results depending on summation order if done in low precision.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Large-scale reductions&#039;&#039;&#039; also occur in operations like global average pooling (summing all elements of a feature map) or computing the total loss by summing loss over batch samples. If one sums 100,000 values in FP32, the worst-case error could be on the order of 100,000 &#039;&#039; 1e-7 ≈ 1e-2 (1% uncertainty), though typical error would be much less. In FP16, summing 100k values is likely impossible without overflow: as noted, adding ~10^4 moderate numbers can overflow FP16&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. So large reductions &#039;&#039;must* be handled carefully (either in chunks or in higher precision). Frameworks generally do: for example, when performing a sum reduction on GPU, libraries break it into parallel blocks that accumulate in registers (usually 32-bit registers, even if inputs are 16-bit). &lt;br /&gt;
&lt;br /&gt;
Another aspect is that many dot products in neural nets involve values of varying magnitude. If the distribution of terms is such that some are much larger than others, the smaller terms might not contribute much if summed last. Ideally, one might sort by magnitude or use pairwise summation to improve that. Most BLAS libraries effectively do a pairwise strategy due to their parallel structure. So, while worst-case analyses are grim, practical implementations mitigate a lot of it.&lt;br /&gt;
&lt;br /&gt;
However, certain layers can suffer if not handled properly. One famous example is in RNNs or LSTMs: if one uses FP16 naive summation for the gating mechanism, errors could accumulate over many time steps and cause drift or instability. In modern practice, recurrent layers are often run in FP16 with FP32 accumulators to avoid this.&lt;br /&gt;
&lt;br /&gt;
In summary, &#039;&#039;&#039;dot products and large reductions need sufficient precision&#039;&#039;&#039; or robust algorithms. If you are implementing a custom kernel (say a custom ONNX op) and you have to sum a huge array, you should consider using a technique from Section 3 (pairwise summation, Kahan, or at least double accumulator if possible). Many deep learning computations, by virtue of hardware and library design, already do something like this under the hood. But it&#039;s always good to be aware, especially if pushing into lower precision territories.&lt;br /&gt;
&lt;br /&gt;
=== 6.2. Cancellation, Dynamic Range, and Conditioning ===&lt;br /&gt;
&lt;br /&gt;
We introduced &#039;&#039;cancellation&#039;&#039; earlier in the context of summation. In deep learning, cancellation can appear in a few places. One is in the computation of certain norms or variances. For instance, Batch Normalization computes mean and variance of activations. The variance formula $\mathrm{Var}(x) = E[x^2] - (E[x])^2$ is subject to catastrophic cancellation if $E[x]$ and $\sqrt{E[x^2]}$ are close in magnitude (which can happen if the variance is small compared to the mean). If implemented naively in single precision, you might get a negative variance due to numerical error (which would then be clamped to zero or sqrt would yield NaN). Robust implementations compute variance in a single pass or increase precision. Indeed, in many frameworks, batchnorm accumulation is done in FP32 even if the activations are FP16, precisely to avoid numerical issues in computing mean and variance. ONNX itself doesn’t specify this, but an ONNX backend is likely to do batchnorm stats in higher precision for safety. The &#039;&#039;conditioning&#039;&#039; of the variance calculation is poor when variance is tiny relative to mean, so it&#039;s a spot where higher precision is needed.&lt;br /&gt;
&lt;br /&gt;
Another example is in certain loss functions or regularizations. If you subtract two large numbers to get a small difference, you lose precision. Say you have two nearly identical predictions and you subtract them as part of some operation – if done in float16, you might get zero whereas float32 would have a slight difference. This could affect, say, a gradient if that difference was supposed to propagate information. Fortunately, neural nets often avoid directly subtracting large nearly equal numbers; but it can happen. In attention mechanisms, the &#039;&#039;score normalization&#039;&#039; (softmax) subtracts a max to avoid huge exponentials, which is good for range but if multiple scores are close to the max, you subtract numbers that are close. However, those subtractions are not catastrophic in the same way; they&#039;re intentional to reduce range and are typically fine in FP32 or even FP16.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Dynamic range&#039;&#039;&#039; issues are everywhere in deep learning. Activation functions like ReLU and exponential can produce very large or very small values. If subsequent computations don’t account for that, you get infinities or zeros. For example, consider a network output that produces a logit of 100 in FP16. $\exp(100)$ in FP16 overflows to infinity (since FP16 max ~ 6e4 and $e^{100} \sim 3.7e43$). If you then do softmax, that one infinity will dominate the sum and you might get a probability of 1 for that class (or NaNs if not handled). The standard softmax implementation subtracts the max logit, so in this case it would subtract 100 from all logits, making the largest exponent $\exp(0)=1$ and others tiny – this avoids overflow. But what about underflow? If you had a very negative logit, $\exp(-100)$ in FP16 would underflow to 0. In softmax, that just contributes ~0 probability, which is fine (it effectively is zero anyway). Underflow tends to be less dangerous than overflow in inference — underflow usually just means “this probability/neuron is effectively 0,” whereas overflow can poison the results with NaNs or Infs. &lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Conditioning&#039;&#039;&#039; refers to how sensitive a function is to input perturbations. Neural networks themselves as functions can be poorly conditioned (e.g., tiny changes in weights cause big changes in output for some architectures), but typically they are designed to be reasonably well-behaved. Certain internal computations like matrix inversion (if one does a pseudoinverse or something for a layer) can be extremely ill-conditioned if the matrix is near singular. That again would need double precision perhaps. In ML, one common ill-conditioned operation is the &#039;&#039;&#039;softmax denominator&#039;&#039;&#039;: $\sum_j e^{x_j}$ can be very large and also very sensitive to changes in the largest $x_j$. However, the softmax is more stable if computed carefully (max trick). Another is &#039;&#039;&#039;normalization&#039;&#039;&#039;: computing $\frac{x - \mu}{\sigma}$ for layer norm or batch norm – if $\sigma$ is very small, then the division blows up noise. In those cases, a small $\epsilon$ is usually added to $\sigma^2$ to prevent instability (this is a model-level fix, not a numeric one per se, but it acknowledges numeric limits).&lt;br /&gt;
&lt;br /&gt;
In training, &#039;&#039;&#039;optimizers&#039;&#039;&#039; can suffer from precision issues. For example, the Adam optimizer keeps a moving average of squared gradients; in half precision, these small updates might flush to zero or not change at some point. Some research found that certain adaptive optimizers break in FP16 because the small differences are lost (&amp;quot;Why FP16 training breaks NAdam&amp;quot; etc.). The solution is often to keep the optimizer states in FP32 while doing the bulk of compute in FP16. This again is a form of mixed precision.&lt;br /&gt;
&lt;br /&gt;
From an inference perspective, once the model is trained, the main concerns are avoiding overflow in activations and ensuring summations (like in softmax or reductions) are done with enough precision. The model’s weights are fixed, so we don&#039;t have to worry about cumulative error over iterations as in training, just the forward pass.&lt;br /&gt;
&lt;br /&gt;
=== 6.3. Normalization Layers and Softmax ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Normalization layers&#039;&#039;&#039; (BatchNorm, LayerNorm, etc.) and &#039;&#039;&#039;Softmax&#039;&#039;&#039; are specifically worth examining for numerical stability:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Batch Normalization:&#039;&#039;&#039; During inference, batchnorm applies an affine transform using precomputed mean and variance (from training). It’s basically: $y = \frac{x - \mu}{\sqrt{\sigma^2 + \epsilon}} \gamma + \beta$. This is a stable operation as long as $\epsilon$ is provided (to avoid division by zero). Since $\mu, \sigma^2$ are constants in inference, it reduces to linear ops which are fine. The main numerical work (computing mean/variance) happened during training or model calibration, and frameworks usually did that in FP32. So batchnorm at inference time is not a source of instability (it’s just a scale and shift of the data).&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Layer Normalization / Group Normalization:&#039;&#039;&#039; These compute mean/var on the fly for each sample (and possibly channel group). In inference, they still need to compute these statistics for each forward pass. If implemented in low precision, the variance calculation can suffer cancellation as mentioned. However, it&#039;s likely that inference engines compute the mean and variance in FP32 even if inputs are FP16 (especially since these are per-sample operations, not huge batch reductions – it&#039;s feasible to do in higher precision). If someone implemented layer norm purely in FP16, they might see issues when the variance is very small. Usually there’s an $\epsilon$ added which might dominate a tiny variance and thus mitigate catastrophic cancellation (the result might just output something mostly from $\epsilon$). In summary, normalization layers are usually fine as long as the implementation uses enough precision for the summation of squares.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Softmax:&#039;&#039;&#039; Softmax is notorious if done improperly. The safe algorithm: subtract the maximum input from all inputs (to bring the largest exponent to 1.0), exponentiate, sum them up, divide each exponent by the sum. The max subtraction handles overflow in $\exp()$. But what about the sum? As discussed, if there are $N$ elements in the softmax, the sum is at most $N$ (when all inputs equal the max). In classification, $N$ might be the number of classes. For ImageNet, $N=1000$, so worst sum ~1000 (which is fine in FP16, as 1000 &amp;lt;&amp;lt; 65504). For an extreme case like a language model, $N$ could be 50,000 (vocab size). Sum could be 50,000 in worst case (everyone equal probability). 50,000 is still below 65504, so actually an FP16 sum would &#039;&#039;not&#039;&#039; overflow in that scenario, and the rounding error on a sum of 50k identical terms would be maybe a few ULPs (which at 50k magnitude might be ~0.5 difference worst-case). So maybe softmax summation in FP16 is not catastrophic for output probabilities – you’d get essentially 1.0 after normalization instead of 0.99999 in some case. Usually fine.&lt;br /&gt;
&lt;br /&gt;
:The bigger issue is if one or a few terms dominate the softmax. If one term is much larger, the others underflow to 0 after the max subtraction. That’s fine: you get a one-hot distribution essentially. If the model expected a softer distribution, that could be a problem. But if the logits were really that far apart, even FP32 would give nearly one-hot. So not really a precision issue, more of a model property.&lt;br /&gt;
&lt;br /&gt;
:However, the &#039;&#039;&#039;stability during training&#039;&#039;&#039; is a big issue. The GitHub thread we saw (ColossalAI) mentioned training a 130B model where FP16 softmax in attention caused training to occasionally blow up&amp;lt;ref name=&amp;quot;ref14&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref14&amp;quot;/&amp;gt;. They observed attention scores in some heads ranging up to 1e4 or as low as 1e-3&amp;lt;ref name=&amp;quot;ref14&amp;quot;/&amp;gt;. 1e4 exponent in FP16 is definitely Inf (because $\exp(10^4)$ is astronomically huge, but even exp(100) is inf in half). The method CogView introduced (Precision Bottleneck Relaxation) involved subtracting the max &#039;&#039;per head&#039;&#039; to reduce range&amp;lt;ref name=&amp;quot;ref14&amp;quot;/&amp;gt;—which is basically the softmax max trick at a finer granularity or something similar. In the end, they found &#039;&#039;&#039;the simplest fix was to compute softmax in FP32&#039;&#039;&#039; for those layers&amp;lt;ref name=&amp;quot;ref14&amp;quot;/&amp;gt;. They reported that it had negligible speed impact but significantly improved stability&amp;lt;ref name=&amp;quot;ref14&amp;quot;/&amp;gt;. This highlights a general principle: certain operations like softmax (and sometimes matrix multiplications in attention) benefit from higher precision, and using FP32 in a few critical spots can save a mixed-precision training from crashing. In inference, using FP32 for softmax is also a safeguard if one is uncertain. It costs little because softmax is not a heavy compute compared to matrix multiplies.&lt;br /&gt;
&lt;br /&gt;
:ONNX does have a Softmax operator; if the input is float16, a backend might do exactly what they described: internally cast to float32 for the softmax computation, then cast output back to float16. Some inference engines (like DeepSpeed’s inference or others) keep attention softmax in FP32 even if rest is FP16, for robustness&amp;lt;ref name=&amp;quot;ref14&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Apart from softmax, other places where exponentials occur are in loss functions like cross-entropy (which uses log-softmax internally), or in activation functions like Sigmoid and Tanh (which are basically scaled logistic functions). Sigmoid($x)=1/(1+e^{-x})$. If $x$ is moderately large positive, $e^{-x}$ underflows to 0 in FP16 if $x&amp;gt;~11$. That gives a sigmoid output of 1.0 exactly. In FP32, $e^{-x}$ for $x=11$ is about $5e-5$, not underflow, so sigmoid ~0.99995. So FP16 will saturate some outputs to exactly 0 or 1 sooner than FP32 would. This can slightly change behaviors (e.g., a neuron might go to full saturation a bit early). Usually, this isn’t a huge deal, but it could make gradients zero earlier. In inference, we don’t care about gradients, just the output value. Getting exactly 1.0 vs 0.99995 probably has no practical difference for a classification output. But one should be aware that &#039;&#039;&#039;activation functions can saturate faster&#039;&#039;&#039; in lower precision due to limited range or precision. ReLU has no issue (it’s linear or 0), but exponential-based activations do. In practice, again, frameworks often simply compute those in the given precision and accept the slight difference. If it were a problem, one could do those in FP32 as well.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Summary:&#039;&#039;&#039; Numerical stability in inference is maintained by using robust computation methods: subtracting maxima in softmax/LogSumExp, adding epsilon in denominators for normalization, and using higher precision for summations or critical steps. Most of these are built into the algorithms. ONNX models assume these standard practices (they don&#039;t need to explicitly do them in the graph because the backend inherently will for ops like Softmax). It&#039;s wise when building or exporting models to ONNX to test them in the target precision and ensure there are no surprises (like an output becomes NaN or Inf due to precision). Usually if training was done in mixed precision, the model will have been vetted in that environment already.&lt;br /&gt;
&lt;br /&gt;
== Performance vs. Accuracy Trade-offs ==&lt;br /&gt;
&lt;br /&gt;
There is an inherent trade-off between numerical precision and computational performance (speed, memory usage) in AI systems. Choosing lower precision can dramatically increase throughput and reduce memory, but it may introduce arithmetic errors. Engineers must balance these considerations:&lt;br /&gt;
&lt;br /&gt;
=== 7.1. Throughput, Memory Bandwidth, and Precision ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Throughput (speed)&#039;&#039;&#039; is often higher for lower-precision operations. Modern hardware can execute more low-precision ops in parallel than high-precision ops. For example, an NVIDIA V100 GPU can perform 2× more half-precision FLOPs than single-precision per clock, and newer architectures with Tensor Cores can do 8× more (since a Tensor Core warp operation might perform 64 FP16 FMAs per cycle vs 16 FP32). Similarly, int8 operations can be even faster and more energy efficient. Lower precision numbers also use less memory bandwidth – fetching 16-bit data is twice as fast as 32-bit data from memory (assuming the same bus width), which can relieve memory bottlenecks. Many deep learning workloads are memory-bound, so halving the data size can nearly double throughput if compute is not the limiting factor. This is a huge incentive to use lower precision.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Memory and storage&#039;&#039;&#039;: Using FP16 or int8 weights reduces model size, which is important for deploying large models to memory-limited environments (like edge devices or GPUs with limited VRAM). It also means you can fit larger batch sizes in memory, or more models on a chip, etc. There is also a cascade effect: smaller data means better cache utilization, which further improves performance.&lt;br /&gt;
&lt;br /&gt;
However, &#039;&#039;&#039;accuracy&#039;&#039;&#039; suffers if precision is too low. We have discussed how rounding error and limited range can cause deviations in outputs. The question is: do those deviations matter for the application? Often for neural networks, a small amount of noise or error in intermediate calculations does not change the final prediction. Neural nets are somewhat error-tolerant due to their redundant distributed representations. This is why quantization and FP16 techniques can work at all. The final model accuracy (e.g. top-1 accuracy on ImageNet) might drop only 0.5% or less when using FP16 instead of FP32, or int8 instead of FP16, if done properly. For many use-cases, this loss is acceptable given the speed gains.&lt;br /&gt;
&lt;br /&gt;
If one goes too low (like naive FP8 or int4 without special care), accuracy might drop severely – perhaps making the model unusable. Thus the trade-off: as precision decreases, performance increases but at some point accuracy degrades beyond an acceptable threshold. Engineers aim to find the lowest precision that still preserves accuracy.&lt;br /&gt;
&lt;br /&gt;
There is also the consideration of &#039;&#039;&#039;accumulation strategy&#039;&#039;&#039; affecting performance: for instance, Kahan summation (compensated) is more accurate but ~4× slower than naive summation. If you need that accuracy, you pay a performance cost. In many AI inference scenarios, the slight extra accuracy from something like Kahan isn&#039;t needed, so it’s not used. But in a scenario where you sum millions of elements for a final result that is thresholded, maybe you do need it.&lt;br /&gt;
&lt;br /&gt;
Another dimension is &#039;&#039;&#039;determinism vs speed&#039;&#039;&#039;. Enforcing a deterministic reduction order (for reproducibility) might require disabling some parallelism, which could slow things down. If exact repeatability is critical (like certain scientific or financial applications of neural nets), one might sacrifice some performance to ensure the summation happens the same way every time. ONNX allows both modes depending on the runtime.&lt;br /&gt;
&lt;br /&gt;
From a memory perspective, if precision is higher than needed, you waste memory. For example, storing intermediate activations in FP32 when they could be FP16 uses 2× memory, which might mean lower batch size or more memory traffic. So there&#039;s strong motivation to keep things as low-precision as possible &#039;&#039;except&#039;&#039; where accuracy truly demands.&lt;br /&gt;
&lt;br /&gt;
In summary, the trade-off often manifests as &#039;&#039;diminishing returns&#039;&#039;: FP64 vs FP32 – huge slowdown for usually negligible accuracy gain in ML; FP32 vs FP16 – moderate speedup for usually minor accuracy hit; FP16 vs int8 – another big speedup but maybe more accuracy loss unless calibration is done; int8 vs int4 – speed gain but often big accuracy drop that might not be worth it for current models.&lt;br /&gt;
&lt;br /&gt;
Engineers use techniques like &#039;&#039;&#039;mixed precision&#039;&#039;&#039; to target high precision only where needed. For example, run most of the model in FP16, but keep a few sensitive layers in FP32. This yields most of the performance benefits while ensuring critical parts are accurate. We already saw softmax as one example. Another might be final output layer: sometimes using FP32 for the logits and softmax can be safer.&lt;br /&gt;
&lt;br /&gt;
Hardware is also trending to support mixed modes – like FP8 Tensor Cores accumulate in FP32, etc. So hardware tries to give accuracy cheaply.&lt;br /&gt;
&lt;br /&gt;
=== 7.2. Hardware Accelerators and Tensor Cores ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Hardware accelerators&#039;&#039;&#039; (GPUs, TPUs, FPGAs, ASICs like GraphCore IPU, etc.) have specific support for certain precisions. Leveraging these can provide massive speedups:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;NVIDIA Tensor Cores:&#039;&#039;&#039; These started in Volta (V100) with support for 4x4 matrix multiply-adds on FP16 inputs with FP32 accumulation&amp;lt;ref name=&amp;quot;ref6&amp;quot;/&amp;gt;. Using Tensor Cores can give up to 8x throughput compared to using regular FP32 cores. But to use them, the data needs to be FP16 (or now BF16 or FP8 on newer GPUs) and aligned to certain dimensions (multiples of 8 etc. in matrix sizes, which frameworks handle by padding if needed). If an ONNX model is all FP32, by default it would not use Tensor Cores unless the runtime automatically converts some ops to mixed precision (which some do via a flag or environment variable enabling TF32 or FP16). The introduction of &#039;&#039;&#039;TensorFloat-32 (TF32)&#039;&#039;&#039; in Ampere GPUs is essentially a compromise: treat FP32 inputs as two 16-bit significand halves (10-bit mantissa actually) so that it can run on Tensor Cores, giving ~8x speed, while still outputting FP32. TF32 has about the precision of FP16 but range of FP32, and it was chosen because it typically doesn’t hurt convnet accuracy. ONNX Runtime or others on Ampere will use TF32 by default for conv and matmul unless disabled, giving a free speed boost at minimal accuracy cost. If exact FP32 is needed, users have to turn it off (sacrificing speed).&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;TPU (Google):&#039;&#039;&#039; TPUs have native support for BF16 matrix ops with FP32 accumulators. They were designed specifically for the mixed precision approach in training. For inference, TPUs also support int8 and other quantized ops (e.g., via Edge TPU for lower-power). If exporting ONNX to run on a TPU (via some bridge), one might get automatic BF16 usage. BF16 has the advantage of no loss in range, so TPUs can more straightforwardly run models in BF16 without modifications.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;FPGAs and ASICs:&#039;&#039;&#039; Some custom accelerators might use fixed-point or block floating-point arithmetic for efficiency. They often treat the network in quantized terms. The ONNX quantization specification (QuantizeLinear/DequantizeLinear ops) exists for representing quantized models (INT8 etc.), which is another domain beyond floating-point, but it overlaps as a precision issue. In those systems, accumulators for INT8 × INT8 multiplies are usually 32-bit (to avoid overflow for dot products up to certain lengths). The trade-off there is between integer vs float representation, which can give big speedups on CPUs/DSAs via SIMD instructions (many CPUs have vector int8 multiply-add instructions now for inference).&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Memory bandwidth on hardware:&#039;&#039;&#039; Some accelerators include special high-bandwidth memory or cache specifically for lower precision. For example, an accelerator might fetch two FP16 values in one 32-bit memory transaction – effectively doubling memory bandwidth for half precision. On some hardware, using FP16 can even reduce memory &#039;&#039;latency&#039;&#039; if multiple values can be packed.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Parallelism and Vectorization:&#039;&#039;&#039; Many hardware units process data in fixed vector widths (e.g., 256-bit registers). That could be 8 floats at once or 16 half-floats at once in the same 256 bits. If your code is in FP16, you might use the full vector width (16 values) whereas FP32 might only use half (8 values). This means FP16 can make better use of vector units. Similarly, matrix units often have fixed tile sizes, and smaller data types allow larger tiles or more tiles concurrently.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Energy efficiency:&#039;&#039;&#039; Lower precision often means less energy per operation, which on large deployments (data centers or battery-powered devices) is a significant advantage. This isn&#039;t directly performance, but it&#039;s an important trade: you might choose a slightly lower precision to save power even if accuracy drops a tiny bit.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Software overhead:&#039;&#039;&#039; Using mixed precision on hardware involves some overhead of casting and moving data between precisions. For instance, if you have to frequently cast FP16 to FP32 and back, that adds instructions and memory copies. A well-designed accelerator (like GPUs with unified memory for FP16/32 or special hardware casting) minimize this. Still, the runtime might insert extra operations (as we saw, ONNX Runtime CPU inserted many Cast nodes converting FP16 to FP32 and back&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;, which actually &#039;&#039;slowed down&#039;&#039; that FP16 model by 4x vs FP32 on a CPU, because the CPU had no native FP16). So ironically, on some hardware, using lower precision in ONNX might degrade performance if not supported (the CPU case). Always ensure the target hardware actually supports the low precision you plan to use; otherwise, you&#039;ll pay overhead for emulation.&lt;br /&gt;
&lt;br /&gt;
To illustrate trade-off: Suppose you have a model that achieves 99% accuracy in FP32 and drops to 98.5% in FP16. If FP16 runs 2x faster, many would accept the 0.5% accuracy drop for double throughput. If int8 runs 4x faster but accuracy drops to 95%, that might be too much drop for some applications, but acceptable for others that value speed. It&#039;s application-dependent.&lt;br /&gt;
&lt;br /&gt;
In critical applications, one might maintain higher precision accumulators or run certain validation steps in high precision to ensure nothing drifted. In research, sometimes a model is trained in mixed precision but validated in double to ensure any small numeric differences don’t influence the reported result (rare, but for scientific computing integration maybe).&lt;br /&gt;
&lt;br /&gt;
In the ONNX world, performance-vs-accuracy is often managed via &#039;&#039;&#039;Execution Providers&#039;&#039;&#039; (EPs) that target specific hardware. For example, the TensorRT EP in ONNX Runtime can take an ONNX model and run it using NVIDIA&#039;s high-performance inference library, which will use FP16 or INT8 if allowed, to maximize speed. The user can configure the EP with precision flags. If the accuracy drop is too high, one might restrict it to FP32. If it&#039;s fine, allow FP16. Tools exist to measure the accuracy difference (like calibrating int8 quantization).&lt;br /&gt;
&lt;br /&gt;
To conclude, the trade-offs revolve around picking the lowest precision that yields acceptable accuracy, and utilizing hardware features fully. ONNX being a high-level spec abstracts this, but it&#039;s ultimately up to the deployment to decide and sometimes up to the exporter to incorporate quantization. ONNX’s role is to be able to &#039;&#039;represent&#039;&#039; these choices (they have added quantized types, FP8 types, etc., to allow the graph to include those when needed).&lt;br /&gt;
&lt;br /&gt;
== Best Practices and Design Guidelines ==&lt;br /&gt;
&lt;br /&gt;
Given the above insights, here are some best practices and guidelines for dealing with numerical precision and accumulators in AI inference (and training) design:&lt;br /&gt;
&lt;br /&gt;
=== 8.1. Choosing an Accumulator Precision ===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Match precision to data length and range:&#039;&#039;&#039; If an operation accumulates a large number of terms, consider using a higher precision accumulator than the input data. For example, for summing &amp;gt;1000 values of type FP16, an FP32 accumulator is recommended to avoid large rounding error or overflow. If summing &amp;gt; million values of FP32, consider double (FP64) accumulation if high accuracy is needed (though this is rare in DL). &#039;&#039;Rule of thumb:&#039;&#039; if $N \cdot \varepsilon$ is not tiny (say &amp;gt;1e-3), you may benefit from higher precision. For FP16 ($\varepsilon \approx 1e-3$), that threshold N is a few hundred. For FP32 ($\varepsilon \approx 1e-7$), N would be in the millions before 1e-3 error.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Use higher precision in critical layers:&#039;&#039;&#039; Identify if certain layers of the model are more sensitive to precision. For instance, the output layer or a layer that feeds into a very sharp decision boundary might require more precision. If a small change there could flip the prediction, you don&#039;t want excessive numerical error. Using FP32 for logits and softmax is a common practice to maintain classification confidence stability.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Follow hardware defaults, but override if needed:&#039;&#039;&#039; Many hardware accelerators have sensible defaults (like accumulate in FP32 for tensor ops). It&#039;s usually best to use those defaults as they were chosen to balance accuracy and speed. Only override if you have evidence of a problem. For example, if using ONNX Runtime on CPU, you discovered it upcasts to float for you – great. But if using some custom accelerator that does pure FP16, you might manually insert casts in the ONNX graph around certain ops to force higher precision on those ops.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Maintain precision for gradient accumulation (training):&#039;&#039;&#039; This is more training-oriented, but worth noting: when summing gradients over batches or updating weights, use higher precision (FP32 or even 64) for those accumulations. This ensures training doesn&#039;t diverge due to numeric error. Many frameworks keep a &amp;quot;master copy&amp;quot; of weights in FP32 when training in FP16&amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;. For inference, this translates to: any running statistics or counters in the model (like if you had a running average inside the model) should probably be in higher precision to avoid drift.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Be mindful of intermediate cast costs:&#039;&#039;&#039; If you do decide to cast to higher precision for an operation in ONNX, remember it incurs memory copies and compute overhead. Make sure it&#039;s worth it. For example, if you cast a huge tensor to double, that could slow things drastically. Usually FP16-&amp;gt;FP32 is fine (hardware handles it well), but FP32-&amp;gt;FP64 might be expensive on GPUs.&lt;br /&gt;
&lt;br /&gt;
=== 8.2. Safe Reduction Lengths and Heuristics ===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Chunk long reductions:&#039;&#039;&#039; If you must do a very long sum in low precision (due to memory constraints or hardware), chunk the sum into parts, sum each to a higher precision local accumulator, then sum the partial sums. This is essentially pairwise summation. Many libraries use a fixed chunk size heuristic (e.g., sum in blocks of 256 in registers, then add those up). This bounds the error growth. If implementing manually, you can choose a chunk size such that chunk_length &#039;&#039; ε is small (like 256 &#039;&#039; 1e-3 = 0.256 for FP16, hmm maybe choose 100).&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Sort by magnitude if possible:&#039;&#039;&#039; In some cases (not typical in DL, but possible in post-processing), if you have numbers varying by orders of magnitude, summing from smallest to largest improves accuracy. It prevents small ones from being swallowed by big ones. This is more relevant if you output a sum of mixed-scale values (like some algorithm mixing big and small contributions). It’s not always feasible in real-time inference to sort, but keep in mind.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Use compensation for critical sums:&#039;&#039;&#039; If you find a particular sum or difference in your inference computation is crucial (like computing a small residual from two close numbers), consider using Kahan summation or another compensated scheme just for that part. For example, if you had an ONNX custom function that needs to sum a lot of almost cancelling terms, you might implement it with compensation internally.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Heuristics for FP8/low-bit:&#039;&#039;&#039; When using extremely low precision like FP8, adopt the recommended practices: use scaling (don&#039;t feed raw data into FP8, normalize it first), and perhaps only use FP8 on portions of the network that are empirically robust. The industry is still establishing heuristics, but one might be: use FP8 for matrix multiplications, but accumulate in FP16 or FP32; keep normalization in FP16; use FP16 for first and last layer maybe. This is evolving, so keep an eye on literature.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Keep an eye on sum length in softmax and normalization:&#039;&#039;&#039; If you have extremely large class counts or group sizes (e.g., a softmax over 1e6 items, or layer norm over tens of thousands of features), realize this is a giant reduction. You may want to ensure that reduction is done in higher precision. ONNX Softmax by default will likely not handle a million-class case in FP16 well (the sum would overflow since 1e6 &amp;gt; 65504). So either do that on CPU in FP32 or break it down (could do hierarchical softmax, etc.). This is a corner case, but relevant for huge vocab language models. In practice, vocab ~50k is fine as we said.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Test numeric differences:&#039;&#039;&#039; A practical tip: after quantizing or reducing precision, run some test inputs through both the high-precision model and low-precision model. Compare outputs (and maybe some intermediate signals if possible). If differences are consistently small relative to the application tolerance, you&#039;re good. If you see occasional large differences or instabilities (like outputs become NaN or Inf in low precision), that&#039;s a red flag. Use that testing to guide where you might need to increase precision or change algorithms.&lt;br /&gt;
&lt;br /&gt;
=== 8.3. When Higher Precision Is Required ===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Ill-conditioned calculations:&#039;&#039;&#039; If your model or pipeline performs any operation known to be ill-conditioned (matrix inversion, subtraction of nearly equal quantities, computing a tiny difference of large numbers), you likely need higher precision for that part. For example, if your ONNX model includes a custom op to solve a linear system, use double precision in that op if possible (or at least float with iterative refinement). In ML, this might occur in something like a physics-informed neural network or a differentiable simulation within the model.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Strict accuracy requirements:&#039;&#039;&#039; Some applications (medical, aerospace, finance) might have strict tolerances. If an inference model’s output needs to be correct to 5 decimal places, you probably cannot use FP16, which has only 3-4 decimal digits precision. FP32 might be barely enough (~7 digits). FP64 might be warranted to guarantee that level of accuracy in worst case. Most deep learning applications (like image classification) don&#039;t demand that level of numeric precision in outputs – as long as the top class is correct, it doesn&#039;t matter if its probability was 0.901 vs 0.899. But if you&#039;re doing something like summing probabilities to make a risk estimate or doing a cumulative sum over many predictions, be careful to accumulate precisely.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Large sums or products in post-processing:&#039;&#039;&#039; Sometimes the neural net is fine, but the &#039;&#039;post-processing&#039;&#039; of results might need precision. E.g., summing up probabilities of many events, or computing confidence intervals, etc. Do those calculations in double if needed outside the model.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Preventing drift in long sequences:&#039;&#039;&#039; For sequential models (RNNs processing thousands of steps), rounding error can accumulate over steps. If each step has a small error, after 10000 steps it could add up. In such cases, using a higher precision for the state might reduce drift. Some practitioners use FP64 for the hidden state accumulation in very long sequence processing to maintain numerical stability (though this is not common due to speed loss). Alternatively, periodically renormalize or reset small errors.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Simulations or differential equations:&#039;&#039;&#039; If your ONNX model involves simulating something (like a differential equation solver inside a model, or an ODE/RNN hybrid), those often need double precision to be stable for long durations. Recognize those situations and allocate precision accordingly.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Verification and debugging:&#039;&#039;&#039; Use higher precision as a tool to verify correctness. For example, run your model in FP64 (if possible) and compare to FP32 to see if there are differences. If not, FP32 is fine. If yes, you might find a particular calculation losing too much info in FP32, implying maybe you should keep it in FP64 in production as well. This is rare in inference but can be part of rigorous validation.&lt;br /&gt;
&lt;br /&gt;
In ONNX, using higher precision may involve casting to &amp;lt;code&amp;gt;tensor(double)&amp;lt;/code&amp;gt; for certain ops. Not all runtimes support double on GPU though (some do via emulation or just slower path). ONNX CPU does support double for general ops. So be mindful of the target.&lt;br /&gt;
&lt;br /&gt;
Finally, &#039;&#039;&#039;document your precision choices&#039;&#039;&#039;. If you deliver an ONNX model that uses mixed types (float16 inputs, float32 accumulations, int8 weights, etc.), ensure that is clear to whoever maintains or uses it. Precision issues can be sneaky, so clarity helps.&lt;br /&gt;
&lt;br /&gt;
== Limitations and Open Problems ==&lt;br /&gt;
&lt;br /&gt;
Despite advances in handling numerical precision, there remain limitations and open challenges in this domain:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Lack of Precision Specification in ONNX:&#039;&#039;&#039; As noted, ONNX currently has no way to formally specify that an op &#039;&#039;should&#039;&#039; use a higher precision internally. This can lead to nondeterministic differences across platforms&amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;. While proposals exist to add attributes for this&amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;, it is not yet standard. Thus an ONNX model might behave slightly differently on two compliant backends, and there&#039;s no built-in mechanism to ensure identical results. This is a limitation for applications requiring cross-platform consistency. Solving it may involve extending ONNX (e.g., an attribute like &amp;lt;code&amp;gt;accumulatePrecision=&amp;quot;FLOAT32&amp;quot;&amp;lt;/code&amp;gt; for MatMul) or tightening the spec, but those come with backward compatibility and implementation burden concerns.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Reproducibility and Determinism:&#039;&#039;&#039; Even with the same precision, parallel computation can introduce nondeterminism (due to different summation orders). Ensuring bit-identical results across runs (important for some sensitive domains) is non-trivial. Frameworks often provide &amp;quot;deterministic mode&amp;quot; which might use fixed ordering at cost of speed. In ONNX, there&#039;s no global flag for this; it&#039;s up to the runtime. More research could be done on algorithms that produce bit-wise identical results independent of parallel execution order (perhaps using techniques like pairwise summation or consistent partitioning). Stochastic rounding is another area: sometimes using probabilistic rounding can reduce bias in accumulated error, but it introduces randomness – reconciling that with reproducibility is tricky.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Ultra-low Precision (FP8 and below):&#039;&#039;&#039; While FP8 is being explored, going to 4-bit floats or mixed schemes is an open challenge. The OpenAI community is investigating 4-bit (quantization-wise) for LLMs with some success, but a true 4-bit float (like E3M1) would be extremely limited. Perhaps hybrid schemes (like block floating point, log scales, etc.) will come. ONNX already has some support (they mention 4-bit in documentation placeholders&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;), but it&#039;s nascent. Research into training techniques, calibration, and new number formats (like &#039;&#039;&#039;posit numbers&#039;&#039;&#039; or tapering precision floats) is ongoing. If a new format proves useful, ONNX will need to adapt to represent it.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Quantization Noise and Model Robustness:&#039;&#039;&#039; Reducing precision can sometimes expose models to issues like adversarial vulnerability or fragility – e.g., a slight perturbation might flip a decision if the margin was small and precision loss nudged it. Ensuring model robustness under quantization is an open problem. It might require re-training the model with quantization in the loop (quantization-aware training) or designing models that have large &amp;quot;margin&amp;quot; between classes to tolerate quantization noise.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Dynamic range during training vs inference:&#039;&#039;&#039; In training, activations can have wider distributions (especially early in training). Models often learn to constrain their ranges (via activation functions saturating or normalization), which makes inference more stable in lower precision. But if one tries to train in FP8 from scratch, it&#039;s very hard. This gap between what&#039;s possible in a post-trained inference model vs training is an open area – how to train directly in low precision without significant accuracy loss. Solving that could simplify workflows (no need for high-precision training then quantize; just train low-prec end to end). Techniques like loss scaling, gradient clipping, etc., are partial solutions. The development of optimizers that are resilient to low precision is ongoing.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Hardware variability:&#039;&#039;&#039; As new hardware emerges (with new numeric capabilities or quirks), ensuring that ONNX models run consistently is an ongoing task. For instance, one device might implement a fused op that reduces rounding error, another does it separately. Even something like convolution might use a different algorithm (FFT-based vs direct) which has different numeric properties. Open problem: could ONNX allow specifying numeric tolerances per op or preferred algorithms? That may be overkill for most, but for some HPC ML use cases, maybe desired.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Testing and validation of numerics:&#039;&#039;&#039; There isn&#039;t a standardized set of &amp;quot;numeric compliance&amp;quot; tests for ONNX runtimes beyond simple correctness. Perhaps in the future, test suites might include challenging numeric scenarios (large sums, nearly singular matrices) to ensure a runtime handles them gracefully (maybe by switching precision). Right now, it&#039;s mostly on the user to test their model under expected conditions.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Combining quantization with floating point:&#039;&#039;&#039; Many production models use a mix (e.g., int8 for some layers, FP16 for others). ONNX can represent this (with QuantizeLinear ops or MixedPrecision through casting), but tooling to automatically find the optimal mix is still developing. It&#039;s somewhat trial-and-error or using heuristics. An open question is how to automate precision assignment: given a model and a target hardware, how to automatically choose which layers can be int8, which should be FP16, etc., to meet an accuracy target. This crosses into NAS (neural architecture search) territory or automated quantization. &lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Numerical debugging tools:&#039;&#039;&#039; When a model goes wrong due to precision, it&#039;s often hard to pinpoint where. Better tooling to analyze numerical error layer by layer could be useful. For instance, one could run a model in double and float and diff the intermediate results to see where the largest divergence happens. Doing this systematically for big nets is an open tooling challenge. Some frameworks have hooks for this, but not standardized.&lt;br /&gt;
&lt;br /&gt;
In conclusion, while we&#039;ve come far with using lower precision in AI without sacrificing much accuracy, the landscape is always evolving. The key open problem is how to push precision lower (for efficiency gains) without crossing the threshold where accuracy falls off a cliff. Each new generation of hardware and techniques (like FP8, novel quantization schemes) extends that boundary, but also brings new questions on how to manage the numerical errors. ONNX as an interchange will need to keep pace, possibly by adding more expressive power for these numeric considerations.&lt;br /&gt;
&lt;br /&gt;
== See Also ==&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;IEEE 754 Floating-Point&#039;&#039;&#039; – The standard defining binary16, binary32, binary64 formats and rounding behavior.&lt;br /&gt;
* &#039;&#039;&#039;Floating-Point Arithmetic&#039;&#039;&#039; – General topic on how floating-point math works, common issues (overflow, underflow, cancellation).&lt;br /&gt;
* &#039;&#039;&#039;Kahan Summation Algorithm&#039;&#039;&#039; – Algorithm for compensated summation to reduce numerical error&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;.&lt;br /&gt;
* &#039;&#039;&#039;BFloat16&#039;&#039;&#039; – A short-format float with 8-bit exponent, used in TPUs and some GPUs for training networks.&lt;br /&gt;
* &#039;&#039;&#039;Mixed Precision Training&#039;&#039;&#039; – Technique of using FP16 (or BF16) along with FP32 accumulators to accelerate training&amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;.&lt;br /&gt;
* &#039;&#039;&#039;Quantization in Deep Learning&#039;&#039;&#039; – Converting networks to lower bit integer arithmetic for inference (INT8 quantization and calibration techniques).&lt;br /&gt;
* &#039;&#039;&#039;ONNX Operator Specification&#039;&#039;&#039; – Official docs for ONNX ops, which describe their mathematical function but not the required numeric precision (a point of potential extension in future).&lt;br /&gt;
* &#039;&#039;&#039;CUDA Tensor Cores &amp;amp; TF32&#039;&#039;&#039; – NVIDIA documentation on how tensor cores perform mixed precision and the TF32 format for accelerating FP32.&lt;br /&gt;
* &#039;&#039;&#039;Pairwise Summation&#039;&#039;&#039; – Summation algorithm with logN error growth&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;, used in many numerical libraries to improve accuracy of reductions.&lt;br /&gt;
* &#039;&#039;&#039;Softmax Stability&#039;&#039;&#039; – Articles on numerically stable softmax and log-sum-exp implementations to avoid overflow.&lt;br /&gt;
* &#039;&#039;&#039;Performance Tuning for Low Precision&#039;&#039;&#039; – Guides (e.g., NVIDIA or Intel whitepapers) on using low precision and the trade-offs involved in various hardware.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;references&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref1&amp;quot;&amp;gt;Accelerate and simplify Scikit-learn model inference with ONNX Runtime - Microsoft Open Source Blog — opensource.microsoft.com — https://opensource.microsoft.com/blog/2020/12/17/accelerate-simplify-scikit-learn-model-inference-onnx-runtime&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref2&amp;quot;&amp;gt;Floating Point Precision: Understanding FP64, FP32, and FP16 in Large Language Models - DEV Community — dev.to — https://dev.to/lukehinds/floating-point-precision-understanding-fp64-fp32-and-fp16-in-large-language-models-3gk6&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref3&amp;quot;&amp;gt;Floating Point Precision and Its Limitations | by Umair Akbar | Medium — akbu.medium.com — https://akbu.medium.com/floating-point-precision-and-its-limitations-cfb7247d7789&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref4&amp;quot;&amp;gt;Kahan summation algorithm - Wikipedia — en.wikipedia.org — https://en.wikipedia.org/wiki/Kahan_summation_algorithm&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref5&amp;quot;&amp;gt;Pairwise summation - Wikipedia — en.wikipedia.org — https://en.wikipedia.org/wiki/Pairwise_summation&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref6&amp;quot;&amp;gt;Numerical behavior of NVIDIA tensor cores - PMC — pmc.ncbi.nlm.nih.gov — https://pmc.ncbi.nlm.nih.gov/articles/PMC7959640/&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref7&amp;quot;&amp;gt;Mixed precision and performance-accuracy tuning (neuron-cc) — AWS Neuron Documentation — awsdocs-neuron.readthedocs-hosted.com — https://awsdocs-neuron.readthedocs-hosted.com/en/latest/about-neuron/appnotes/neuron-cc/mixed-precision.html&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref8&amp;quot;&amp;gt;Create Float16 and Mixed Precision Models - ONNX Runtime — onnxruntime.ai — https://onnxruntime.ai/docs/performance/model-optimizations/float16.html&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref9&amp;quot;&amp;gt;[Performance] ONNX FP16 model is having performance bottle neck when compared to FP32 variant · Issue #25824 · microsoft/onnxruntime · GitHub — github.com — https://github.com/microsoft/onnxruntime/issues/25824&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref10&amp;quot;&amp;gt;Float stored in 8 bits - ONNX 1.21.0 documentation — onnx.ai — https://onnx.ai/onnx/technical/float8.html&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref11&amp;quot;&amp;gt;[2209.05433] FP8 Formats for Deep Learning — arxiv.org — https://arxiv.org/abs/2209.05433&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref12&amp;quot;&amp;gt;Should MatMul op allow specification of accumulation precision? · Issue #7072 · onnx/onnx · GitHub — github.com — https://github.com/onnx/onnx/issues/7072&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref13&amp;quot;&amp;gt;Quantize ONNX models | onnxruntime — onnxruntime.ai — https://onnxruntime.ai/docs/performance/model-optimizations/quantization.html&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref14&amp;quot;&amp;gt;Keep Attention Softmax FP32 during FP16/ZeRO Training · Issue #1485 · hpcaitech/ColossalAI · GitHub — github.com — https://github.com/hpcaitech/ColossalAI/issues/1485&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;/references&amp;gt;&lt;/div&gt;</summary>
		<author><name>Timo.stripf</name></author>
	</entry>
	<entry>
		<id>https://www.emmtrix.com/w139/index.php?title=Numerical_Precision_in_ONNX_and_AI_Inference&amp;diff=2898</id>
		<title>Numerical Precision in ONNX and AI Inference</title>
		<link rel="alternate" type="text/html" href="https://www.emmtrix.com/w139/index.php?title=Numerical_Precision_in_ONNX_and_AI_Inference&amp;diff=2898"/>
		<updated>2026-01-30T15:23:14Z</updated>

		<summary type="html">&lt;p&gt;Timo.stripf: /* 3.3. Compensated Summation (Kahan, Neumaier) */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&#039;&#039;&#039;Open Neural Network Exchange (ONNX)&#039;&#039;&#039; is an open standard format for representing machine learning models and neural network computations across different frameworks and hardware&amp;lt;ref name=&amp;quot;ref1&amp;quot;/&amp;gt;. As models are exported and deployed via ONNX, the &#039;&#039;&#039;numerical precision&#039;&#039;&#039; of computations becomes critical. Deep learning inference involves a variety of floating-point operations, and small numerical discrepancies can accumulate and affect model accuracy or reproducibility. This article provides an in-depth overview of floating-point precision issues in numerical computing, with a focus on how they manifest in AI inference and the ONNX ecosystem. We cover floating-point formats and rounding behavior, accumulation strategies for summations, the use of low-precision formats (FP16, BFloat16, FP8) in modern AI, and how ONNX defines (or leaves unspecified) the precision of accumulators in operations. Strategies for maintaining numerical stability in deep neural networks are discussed, along with the trade-offs between computational performance and arithmetic accuracy. The goal is to give engineers and researchers a comprehensive, encyclopedic reference on numerical precision in ONNX-based AI inference systems.&lt;br /&gt;
&lt;br /&gt;
== Floating-Point Precision in Numerical Computing ==&lt;br /&gt;
&lt;br /&gt;
=== 2.1. IEEE 754 Floating-Point Formats ===&lt;br /&gt;
&lt;br /&gt;
Modern computers represent real numbers using the IEEE 754 floating-point standard, which defines formats like 32-bit &#039;&#039;single precision&#039;&#039; (FP32) and 64-bit &#039;&#039;double precision&#039;&#039; (FP64). In IEEE 754 binary formats, a floating-point number is composed of three fields: 1 sign bit, several exponent bits (with a bias), and several fraction (mantissa) bits. For example, FP32 consists of 1 sign bit, 8 exponent bits, and 23 fraction bits (24 bits of significand precision including the implicit leading 1)&amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;. This provides roughly 7 decimal digits of precision and an exponent range allowing values on the order of $10^{\pm38}$. FP64 allocates 1 sign bit, 11 exponent bits, and 52 fraction bits (53-bit precision), yielding about 15–16 decimal digits of precision and a vastly larger range (~$10^{\pm308}$)&amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;. These larger formats (especially FP64) are the &amp;quot;gold standard&amp;quot; for numerical accuracy in scientific computing, as they reduce round-off error in complex calculations&amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;. However, the increased precision comes at the cost of doubled storage and typically reduced computational throughput (e.g. many GPUs execute FP64 instructions at a much lower rate than FP32&amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
In addition to single and double precision, the IEEE 754-2008 standard introduced a 16-bit &#039;&#039;half precision&#039;&#039; format (FP16, or binary16). FP16 has 1 sign bit, 5 exponent bits, and 10 fraction bits (11-bit significand including the implicit 1)&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. This format covers a much smaller dynamic range (approximately $6.1\times10^{-5}$ to $6.5\times10^4$ for normalized values&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;) and offers only around 3 decimal digits of precision. Half precision was originally used in graphics, but it has gained popularity in machine learning for its speed and memory advantages (discussed later in Section 4). Another format relevant to AI is &#039;&#039;&#039;BFloat16&#039;&#039;&#039; (Brain Float 16), a 16-bit float with &#039;&#039;&#039;8 exponent bits and 7 fraction bits&#039;&#039;&#039;&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. BFloat16 sacrifices precision (only ~7 fraction bits ≈ 2–3 decimal digits) in order to have the &#039;&#039;&#039;same exponent range as FP32&#039;&#039;&#039;&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. This means BFloat16 can represent very large or very small numbers (∼$10^{\pm38}$, similar range as FP32) but with much less precision between representable values&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. BFloat16 was introduced for deep learning by Google (for TPUs) to allow reduced precision &#039;&#039;training&#039;&#039; without frequent overflow/underflow, relying on the wide range and accepting the coarse precision.&lt;br /&gt;
&lt;br /&gt;
Each floating-point format has a &#039;&#039;&#039;unit roundoff&#039;&#039;&#039; or machine precision, denoted $\epsilon$. This is effectively the spacing between 1.0 and the next representable number. For FP32, $\epsilon \approx 2^{-23} \approx 1.19\times10^{-7}$, corresponding to about 7–8 decimal digits of precision&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. In FP64, $\epsilon = 2^{-52} \approx 2.22\times10^{-16}$ (15–16 decimal digits). Half precision is much less precise: FP16 has $\epsilon = 2^{-10} \approx 9.76\times10^{-4}$&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;, and BFloat16 has $\epsilon = 2^{-7} = 7.8125\times10^{-3}$&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. These values quantify how finely the continuum of real numbers is quantized in each format.&lt;br /&gt;
&lt;br /&gt;
=== 2.2. Machine Epsilon and Rounding Error ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Machine epsilon ($\varepsilon$)&#039;&#039;&#039; is formally defined as the smallest positive number such that $1.0 + \varepsilon &amp;gt; 1.0$ in the floating-point format&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. It effectively measures the &#039;&#039;&#039;precision&#039;&#039;&#039; of the representation. In FP32, $\varepsilon = 2^{-23} \approx 1.19\times10^{-7}$, meaning any result is rounded to about 7 decimal digits. FP16’s $\varepsilon = 2^{-10} \approx 9.77\times10^{-4}$, over 1000× larger (so only ~3 decimal digits of precision), and BFloat16’s $\varepsilon = 2^{-7} \approx 7.8\times10^{-3}$, reflecting its mere ~8-bit precision&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. Rounding in IEEE 754 by default uses &#039;&#039;&#039;round-to-nearest (ties to even)&#039;&#039;&#039;, which ensures the rounding error for any single operation is at most half a unit in the last place (0.5 ULP). We can model a floating-point addition as: &lt;br /&gt;
&lt;br /&gt;
$$\text{fl}(a+b) = (a+b)\,(1 + \delta),$$ &lt;br /&gt;
&lt;br /&gt;
where $|\delta| \le \varepsilon$ for that format&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. Here $\varepsilon/2$ is the maximum &#039;&#039;&#039;relative error&#039;&#039;&#039; in one rounding (also called unit roundoff). For example, when adding two FP32 numbers, the result is accurate to within about $10^{-7}$ of the true sum (relative), whereas in BF16, the result can deviate by up to $\sim7.8\times10^{-3}$ (nearly 0.78% relative error per operation)&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. Thus, &#039;&#039;&#039;each arithmetic operation incurs a small rounding error&#039;&#039;&#039; (on the order of machine epsilon) that can propagate or accumulate through a sequence of calculations.&lt;br /&gt;
&lt;br /&gt;
Because floating-point representations have finite exponent range, we must also consider &#039;&#039;&#039;overflow&#039;&#039;&#039; and &#039;&#039;&#039;underflow&#039;&#039;&#039;. If a result’s magnitude exceeds the maximum representable value, it overflows to infinity; if it falls below the minimum normal (or minimum subnormal) value, it underflows to zero (or a denormalized tiny value). FP32 and BF16 share an exponent size (8 bits), so their range for normal numbers is roughly $[10^{-38}, 10^{38}]$ (approx $~\!3.4\times10^{38}$ max)&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. FP16’s 5-bit exponent allows max $\sim 6.55\times10^4$ and min normal $\sim 6.10\times10^{-5}$&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. In practice, &#039;&#039;&#039;underflow/overflow&#039;&#039;&#039; can occur in low-precision arithmetic if values are not appropriately scaled. For instance, summing a large number of moderately sized terms in FP16 could overflow where FP32 would not. As an example, summing on the order of $10^4$ values of magnitude ~$10^1$ (say, adding 10,000 terms around 10 each) would produce a total $\sim10^5$, which exceeds FP16’s max finite value ~$6.5\times10^4$ and would overflow to $+\infty$ in half precision&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. Underflow is usually a concern when subtracting nearly equal numbers (catastrophic cancellation yielding a tiny difference that might flush to zero in low precision).&lt;br /&gt;
&lt;br /&gt;
=== 2.3. Accumulation Error in Reductions ===&lt;br /&gt;
&lt;br /&gt;
Floating-point addition is &#039;&#039;&#039;not associative&#039;&#039;&#039;: $(a+b)+c$ can differ from $a+(b+c)$ due to rounding, and thus the order in which we sum a list of numbers can change the result slightly. When summing a large sequence of $N$ numbers $x_1, x_2, ..., x_N$, the &#039;&#039;&#039;accumulated rounding error&#039;&#039;&#039; can be significant. In the &#039;&#039;worst case&#039;&#039; (e.g., adding many positive and negative terms that cause maximal cancellation or always rounding one way), the error can grow on the order of $O(N \varepsilon)$ relative to the exact sum&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;. Simply summing $N$ numbers in sequence (the &#039;&#039;&#039;naive linear summation&#039;&#039;&#039;) has a worst-case error bound proportional to $N\,\varepsilon$&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. This worst-case occurs for adversarial arrangements of values; in more typical scenarios with rounding errors of random signs, one can model the error as a random walk, yielding an expected root-mean-square (RMS) error growth on the order of $O(\sqrt{N}\,\varepsilon)$&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;. Still, with very large $N$, even $\sqrt{N}$ times $\varepsilon$ can become large enough to matter.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Cancellation&#039;&#039;&#039; is a particular problem: if we add two nearly equal numbers of opposite sign, most leading digits cancel and the result loses significant bits of precision. For example, in single precision, subtracting two numbers that agree in the first 6 decimal digits will yield a result with only ~1 digit of accuracy left. Catastrophic cancellation can dramatically increase relative error. Summation is especially sensitive to cancellation if large positive and negative terms are summed naively. The &#039;&#039;&#039;condition number&#039;&#039;&#039; of a summation (the ratio of the sum of absolute values $\sum|x_i|$ to the absolute value of the true sum $|\sum x_i|$) indicates how sensitive the result is to perturbations&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;. If the numbers have mixed signs and nearly cancel out (making the condition number large), even an optimal summation order will have a large relative error. For instance, summing [1.0, $10^{100}$, 1.0, $-10^{100}$] in double precision has an exact sum of 2.0, but naive summation may yield 0.0 due to cancellation; an improved summation algorithm yields the correct 2.0&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
In practical terms, when summing a long list of floating-point numbers (such as accumulating a dot product or reducing a tensor), the &#039;&#039;&#039;accumulation error&#039;&#039;&#039; can become noticeable if no precautions are taken. Techniques to mitigate this error include carefully choosing the summation order or using higher precision for intermediate sums. We will explore these strategies in the next section.&lt;br /&gt;
&lt;br /&gt;
== Accumulators and Reduction Strategies ==&lt;br /&gt;
&lt;br /&gt;
When performing reductions (sums, dot products, etc.), the method of accumulation can greatly affect the accuracy of the result. Below, we discuss various strategies:&lt;br /&gt;
&lt;br /&gt;
=== 3.1. Naive Linear Accumulation ===&lt;br /&gt;
&lt;br /&gt;
The simplest accumulation strategy is to use a single accumulator and add each element sequentially. In C-like pseudocode:&lt;br /&gt;
&lt;br /&gt;
This &#039;&#039;&#039;naive linear summation&#039;&#039;&#039; adds values one by one to the running total. While straightforward and fast, it is prone to the rounding error accumulation described earlier. The partial sum &amp;lt;code&amp;gt;sum&amp;lt;/code&amp;gt; continually grows (in absolute value) as terms are added, so new small terms may fall outside its precision. For example, if &amp;lt;code&amp;gt;sum&amp;lt;/code&amp;gt; is large and positive, adding a comparatively tiny positive number may produce no change because the small term&#039;s significant digits vanish in the presence of the large sum (the increment is below the rounding threshold of &amp;lt;code&amp;gt;sum&amp;lt;/code&amp;gt;). Conversely, adding a small number of opposite sign can suffer from cancellation. The order of addition matters: summing from smallest magnitude to largest (ascending order) tends to yield a more accurate result than summing in descending order, because adding small terms first preserves their contributions before the sum becomes very large. The naive loop above implicitly sums in the given index order; if the data is unsorted, this order is essentially arbitrary.&lt;br /&gt;
&lt;br /&gt;
The performance of linear accumulation is excellent (minimal overhead), and for many applications the resulting error is within acceptable limits. However, for &#039;&#039;&#039;very large N&#039;&#039;&#039; or ill-conditioned sums, the inaccuracy can become a problem. In a reduction involving millions of elements, a single-precision sum could lose several digits of accuracy. In deep learning, this could translate to small discrepancies in layer outputs or aggregated gradients if done naively in low precision.&lt;br /&gt;
&lt;br /&gt;
=== 3.2. Pairwise and Tree-Based Reductions ===&lt;br /&gt;
&lt;br /&gt;
A better approach to summation is &#039;&#039;&#039;pairwise summation&#039;&#039;&#039;, which sums numbers in a binary tree structure rather than linearly. The idea is to recursively split the array into halves, sum each half, then add the two partial sums. This can be implemented as:&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
By always adding numbers of similar magnitudes first (small subsets are reduced to subtotals, then those are added, etc.), pairwise summation avoids the situation of a very large running total absorbing small additions until the final steps. The error of pairwise summation grows logarithmically with $N$ in the worst case (roughly $O(\varepsilon \log_2 N)$)&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;, instead of linearly. Intuitively, at each level of the tree, rounding error is constrained, and there are about $\log_2 N$ levels. In fact, the worst-case relative error bound for pairwise summation (base case of 1 element) is:&lt;br /&gt;
&lt;br /&gt;
\[ |E_N| \le \frac{\varepsilon \log_2 N}{1 - \varepsilon \log_2 N} \sum_{i=1}^N |x_i|, \]&lt;br /&gt;
&lt;br /&gt;
which for practical $N$ simplifies to on the order of $\varepsilon \log_2 N$ times the condition number of the sum&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;. In typical random-error scenarios, the error behaves even better (on the order of $\sqrt{\log N}$)&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;. The key is that &#039;&#039;&#039;pairwise (tree) reduction balances the accumulations&#039;&#039;&#039;, preventing one sum from overwhelming the others too early.&lt;br /&gt;
&lt;br /&gt;
From a performance standpoint, pairwise summation has the same $O(N)$ total additions as linear summation, just a different order. It is also highly amenable to parallelization: independent partial sums can be computed in parallel at each level of the tree. Many numerical libraries and BLAS implementations use a form of blocked or pairwise summation in dot products for improved accuracy at negligible performance cost. For example, NumPy and Julia perform partial pairwise summation by default for better numerical behavior&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=== 3.3. Compensated Summation (Kahan, Neumaier) ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Compensated summation&#039;&#039;&#039; is an alternative technique to improve summation accuracy by tracking the small “lost” quantities. The most famous algorithm is the &#039;&#039;&#039;Kahan summation algorithm&#039;&#039;&#039; (also called Kahan–Babuška)&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. Kahan summation uses an extra variable to accumulate the round-off error. In pseudocode:&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Here &amp;lt;code&amp;gt;c&amp;lt;/code&amp;gt; accumulates the tiny leftovers that would otherwise be dropped&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. Each iteration essentially subtracts the last error from the new addend (&amp;lt;code&amp;gt;y = x[i] - c&amp;lt;/code&amp;gt;) so that &amp;lt;code&amp;gt;t = sum + y&amp;lt;/code&amp;gt; includes the small bits from the previous operation. The new error is then stored in &amp;lt;code&amp;gt;c&amp;lt;/code&amp;gt; for the next iteration. The result is that the final &amp;lt;code&amp;gt;sum&amp;lt;/code&amp;gt; is as if computed with higher precision than the machine’s: Kahan’s algorithm can &#039;&#039;&#039;greatly reduce the error&#039;&#039;&#039; in summing a sequence of floats. In fact, with exact arithmetic for the compensation, Kahan summation yields a result accurate to within 1 or 2 ULP of the true sum, regardless of N (assuming the sequence of partial sums doesn’t overflow)&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. More precisely, Kahan summation achieves a &#039;&#039;&#039;worst-case error independent of N&#039;&#039;&#039; (error bound depends only on machine precision, not the number of terms)&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. In practice, using a double accumulator for summing single-precision numbers via Kahan can give near double-precision accuracy.&lt;br /&gt;
&lt;br /&gt;
An important subtlety is that the compensation variable &amp;lt;code&amp;gt;c&amp;lt;/code&amp;gt; should be kept in &#039;&#039;full precision&#039;&#039;. In the implementation above, we used &amp;lt;code&amp;gt;double&amp;lt;/code&amp;gt; for both sum and c. If summing FP32 values, one might use &amp;lt;code&amp;gt;float sum&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;float c&amp;lt;/code&amp;gt; (both 32-bit), which still improves accuracy, but using a higher precision for the accumulator and compensation (i.e. double) yields even better results. Many implementations of Kahan summation use the same type for sum and c, but it&#039;s assumed to be higher precision than the inputs when possible.&lt;br /&gt;
&lt;br /&gt;
Kahan’s algorithm carries a performance cost: each iteration does five arithmetic operations (two adds, one subtract, etc.) and has a data dependency, which can hinder parallelism or pipelining&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. Thus, it runs slower than naive summation (roughly 2–3× slower or more, depending on CPU pipeline). In loops where summation is a bottleneck, this may be significant. Nonetheless, for modest N or where maximum accuracy is required, compensated summation is extremely useful. In fact, Python’s &amp;lt;code&amp;gt;math.fsum&amp;lt;/code&amp;gt; function uses a variant of Kahan’s algorithm (actually a more advanced multi-part compensation) to sum iterables with high precision.&lt;br /&gt;
&lt;br /&gt;
There are variations and improvements on basic Kahan summation. &#039;&#039;&#039;Neumaier’s algorithm&#039;&#039;&#039; (sometimes called &#039;&#039;Kahan-Babuška-Neumaier&#039;&#039;) improves Kahan’s method by also handling cases where the next input is larger in magnitude than the running sum&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. In such cases, the original Kahan formula could suffer error; Neumaier’s version effectively swaps roles when a huge value comes in. Its pseudocode is:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;!--&lt;br /&gt;
&amp;lt;syntaxhighlight&amp;gt;&lt;br /&gt;
function NeumaierSum(array):&lt;br /&gt;
    sum = 0.0&lt;br /&gt;
    compensation = 0.0&lt;br /&gt;
    for each x in array:&lt;br /&gt;
        t = sum + x&lt;br /&gt;
        if (fabs(sum) &amp;gt;= fabs(x))&lt;br /&gt;
            compensation += (sum - t) + x;&lt;br /&gt;
        else&lt;br /&gt;
            compensation += (x - t) + sum;&lt;br /&gt;
        sum = t;&lt;br /&gt;
    return sum + compensation;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
--&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight&amp;gt;&lt;br /&gt;
xyz&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This algorithm ensures the compensation covers both scenarios: when the sum is the bigger addend or when the new term is bigger&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. In the end, it adds the final &amp;lt;code&amp;gt;compensation&amp;lt;/code&amp;gt; once to the result. Neumaier’s method can produce the correct result in cases where basic Kahan fails. For instance, summing [1.0, $+10^{100}$, 1.0, $-10^{100}$] in double precision: regular Kahan summation might give 0.0 (losing the small terms), whereas Neumaier’s algorithm yields 2.0 exactly&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. There are also higher-order compensation methods (carrying multiple compensation terms, e.g., the &#039;&#039;&#039;Klein summation&#039;&#039;&#039; variant&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt; or using arbitrary precision for intermediate sums) that further reduce error at increased computational cost&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
In summary, compensated summation algorithms trade extra computation for greatly improved numerical accuracy. They are especially valuable when summing large arrays of numbers with varying magnitudes, where naive summation could lose most of the low-order bits. Many scientific computing environments provide compensated sum routines for this reason. In deep learning, however, a different approach to improved precision is often taken: &#039;&#039;&#039;using a wider accumulator type&#039;&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
=== 3.4. Mixed-Precision Accumulation ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Mixed-precision accumulation&#039;&#039;&#039; refers to performing arithmetic in a higher precision than the input data. A common scenario is accumulating sums in 32-bit floats when the operands are 16-bit floats. Rather than implementing a compensation algorithm, one can simply use a wider accumulator so that intermediate rounding error is smaller. This is effectively what modern GPUs do for tensor operations: for example, NVIDIA &#039;&#039;&#039;Tensor Cores operate on FP16 input data with an FP32 accumulator&#039;&#039;&#039; (the 16-bit products are accumulated in 32-bit)&amp;lt;ref name=&amp;quot;ref6&amp;quot;/&amp;gt;. In this way, summing 2048 products in a dot product has far less precision loss than it would with a 16-bit accumulator. Hardware that supports mixed precision can give the best of both worlds: fast low-precision multiplies and high-precision accumulation.&lt;br /&gt;
&lt;br /&gt;
Software can also leverage mixed precision. For instance, one can sum an array of &amp;lt;code&amp;gt;float&amp;lt;/code&amp;gt; values into a &amp;lt;code&amp;gt;double&amp;lt;/code&amp;gt; variable, accumulating the result in double precision and then converting back to float at the end:&lt;br /&gt;
&lt;br /&gt;
This approach often yields a more accurate &amp;lt;code&amp;gt;result&amp;lt;/code&amp;gt; than summing in single precision directly, because the intermediate sum carries 52-bit precision. The final rounding to float happens only once at the end (minimizing overall rounding error). In fact, the above is one way to implement &amp;lt;code&amp;gt;math.fsum&amp;lt;/code&amp;gt; in Python (it internally does something similar with multiple partial sums in double precision).&lt;br /&gt;
&lt;br /&gt;
In machine learning training and inference, &#039;&#039;&#039;mixed precision&#039;&#039;&#039; is widely used. When using FP16 for speed, it&#039;s common to accumulate with FP32. For example, &#039;&#039;&#039;Google&#039;s TPUs use BFloat16 for multiplication but accumulate in FP32&#039;&#039;&#039; for matrix ops&amp;lt;ref name=&amp;quot;ref6&amp;quot;/&amp;gt;. AWS’s Inferentia (Neuron) hardware similarly supports FP16/BF16 matrix multiplication with FP32 accumulation&amp;lt;ref name=&amp;quot;ref7&amp;quot;/&amp;gt;. The motivation is that summing hundreds or thousands of half-precision values would otherwise introduce intolerable error or risk overflow. With an FP32 accumulator, the reduction error is drastically reduced (by about $2^{13}$ times, since FP32 has 13 more precision bits than FP16). Mixed-precision accumulation thus provides a simple and effective form of compensation: &#039;&#039;&#039;use a larger bucket to collect many small contributions&#039;&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
There is a spectrum of options: summing int8 or int16 integers might use 32-bit integers as accumulators for the same reason (to avoid overflow). Some BLAS libraries even offer &#039;&#039;extended precision accumulators&#039;&#039; for single precision dot products (accumulating in double). The downside of mixed precision is the extra memory and maybe throughput cost of the wider type. But on many platforms, the add latency is dominated by hardware design rather than bit-width, so using a 32-bit adder vs 16-bit has negligible speed impact if supported natively. The key is whether data movement or register pressure increases.&lt;br /&gt;
&lt;br /&gt;
In summary, &#039;&#039;&#039;using a higher precision accumulator is often the easiest way to improve numerical accuracy&#039;&#039;&#039; for reductions. It is a primary strategy in deep learning hardware to maintain stability while exploiting low precision for storage and initial computations.&lt;br /&gt;
&lt;br /&gt;
== Precision Formats in AI and Machine Learning ==&lt;br /&gt;
&lt;br /&gt;
Modern AI systems use a variety of numeric formats to balance speed, memory, and accuracy. We discuss common precisions and their roles:&lt;br /&gt;
&lt;br /&gt;
=== 4.1. FP32 and FP64 in Training and Inference ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;FP32 (single precision)&#039;&#039;&#039; has been the workhorse of deep learning for years. Most training scripts in frameworks like TensorFlow and PyTorch used FP32 by default, as it offers sufficient precision to propagate gradients and accumulate weight updates over millions of iterations without excessive error. With ~7 decimal digits of precision and a wide exponent range, FP32 can handle the dynamic range of activations and gradients in typical neural networks. In inference, FP32 is often more precision than strictly necessary for final predictions, but it has remained common because of its ease and the fact that many GPU operations were optimized for FP32.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;FP64 (double precision)&#039;&#039;&#039; is rarely used in deep learning model training or inference, except in specific scenarios requiring extreme precision. FP64 is common in scientific computing and certain types of numerical simulations. In neural networks, however, the extra precision is usually not needed for model accuracy—using doubles tends to yield negligible improvements in model metrics but at a large performance cost (since on GPUs FP64 throughput can be 1/2, 1/8, or even 1/32 of FP32 throughput depending on hardware). One exception might be when integrating neural nets into larger physics simulations or when solving ill-conditioned problems as part of the model. Some researchers use FP64 to diagnose numerical issues or ensure reproducibility. Generally though, deep learning has favored lower precision for efficiency, and FP64 is considered overkill for most networks&amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
That said, certain classical machine learning algorithms benefit from double precision. For example, computing the inverse of a matrix (as in a Gaussian Process regression or Kalman filter embedded in a model) can incur large rounding errors in single precision, so using FP64 can significantly improve the result&amp;lt;ref name=&amp;quot;ref1&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref1&amp;quot;/&amp;gt;. The ONNX standard supports a float64 tensor type for such needs. In ONNX Runtime, however, emphasis has been on FP32 performance, and some pipelines default to float even if the original model was double&amp;lt;ref name=&amp;quot;ref1&amp;quot;/&amp;gt;. If a model’s prediction computation involves operations that are numerically sensitive (like matrix inversion, Cholesky decomposition, or summing extremely large counts), then FP64 may be required to avoid noticeable discrepancies&amp;lt;ref name=&amp;quot;ref1&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref1&amp;quot;/&amp;gt;. A good practice is to test a model in both float and double if you suspect precision issues; significant differences indicate an ill-conditioned calculation that might need higher precision.&lt;br /&gt;
&lt;br /&gt;
In summary, FP32 remains the &#039;&#039;&#039;default&#039;&#039;&#039; format for most training and inference due to its balance of accuracy and efficiency&amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;, while FP64 is reserved for niche cases or precision-critical subroutines. FP32 typically achieves the same accuracy as FP64 for end-to-end deep learning tasks, because network training can often compensate for small noise, and other sources of error (like generalization error) dominate. Therefore, the community trend has been moving to &#039;&#039;&#039;lower&#039;&#039;&#039; precisions, not higher, to gain speed — provided the models still converge and perform well.&lt;br /&gt;
&lt;br /&gt;
=== 4.2. FP16 and BFloat16 in Deep Learning ===&lt;br /&gt;
&lt;br /&gt;
To further speed up neural network training and inference, &#039;&#039;&#039;half-precision (FP16)&#039;&#039;&#039; was adopted, especially with the advent of GPUs supporting FP16 math at twice the rate of FP32. Using FP16 reduces memory storage and bandwidth by 50% compared to FP32, which is significant for large models. However, FP16’s small exponent range and precision posed challenges: training a large network purely in FP16 can fail due to gradient underflow/overflow or weight updates not registering (because changes are below the 0.001 quantization step). To address this, frameworks introduced &#039;&#039;&#039;mixed-precision training&#039;&#039;&#039;, where compute-intensive parts (matrix multiplies, convolutions) run in FP16, but certain accumulations and model parameters remain in FP32. For instance, NVIDIA’s &#039;&#039;&#039;Tensor Core&#039;&#039;&#039; units require FP16 (or BF16) inputs but accumulate results in FP32, effectively ensuring that reductions are done in higher precision&amp;lt;ref name=&amp;quot;ref6&amp;quot;/&amp;gt;. Research showed that with proper loss scaling and mixed precision, models can train to FP32-level accuracy using mostly FP16 arithmetic, achieving significant speedups.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;BFloat16 (BF16)&#039;&#039;&#039; emerged as an alternative 16-bit format primarily from Google’s TPU training systems. BF16 has a larger exponent (8 bits, same as FP32) but only 7 fraction bits&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. This means BF16 can handle the &#039;&#039;&#039;same dynamic range&#039;&#039;&#039; as FP32 (no overflows where FP32 wouldn’t) but at the cost of even lower precision than FP16 (BF16 has about 2–3 decimal digits of precision vs FP16’s ~3–4). Despite its coarse precision, BF16 turns out to be effective for training deep networks because the algorithms (stochastic gradient descent, etc.) are somewhat robust to that noise, and having the wide range prevents catastrophic loss of signal (no gradient will overflow to Inf unless it was going to in FP32 anyway). Many training frameworks (PyTorch, TensorFlow) support BF16 training on hardware like &#039;&#039;&#039;TPU, NVIDIA A100+, Intel CPUs with AVX-512 BF16, etc&#039;&#039;&#039;. Typically, when using BF16, &#039;&#039;&#039;accumulations are still done in FP32&#039;&#039;&#039; (for example, a TPU v2/v3 multiplies BF16 matrices and accumulates into an FP32 result for the dot product&amp;lt;ref name=&amp;quot;ref6&amp;quot;/&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
In inference, both FP16 and BF16 are used to deploy models for faster speed or lower memory. FP16 is popular on NVIDIA GPUs (which have specialized FP16 units). BF16 is advantageous on TPUs and some CPUs that support it, and increasingly on GPUs (Ampere and newer GPUs also support BF16 arithmetic). The choice between FP16 and BF16 can depend on hardware: BF16’s advantage is easier software conversion (you can often take an FP32-trained model and cast weights to BF16 with minimal loss, since the dynamic range is intact), whereas FP16 might require adjusting the model or ensuring values fit in range (sometimes needing techniques like maintaining a master copy of weights in FP32 during training, or handling out-of-range values).&lt;br /&gt;
&lt;br /&gt;
For ONNX, both Float16 and BFloat16 types are part of the standard. ONNX models can be converted to FP16 (truncating weights and inserting cast nodes for computations) using tools like &amp;lt;code&amp;gt;float16_converter&amp;lt;/code&amp;gt; in ONNX Runtime&amp;lt;ref name=&amp;quot;ref8&amp;quot;/&amp;gt;. ONNX Runtime on GPUs will typically execute those FP16 ops with mixed precision on tensor cores (FP16 input, FP32 accumulate), whereas on CPU it may emulate FP16 or upcast to FP32 (as many CPU implementations do not natively support half-precision math)&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;. In fact, as an example, running an ONNX model in FP16 on a CPU might insert hidden Cast operations to FP32 behind the scenes, because the CPU kernel for that op only exists in FP32&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;. This ensures correctness (the computation is done in higher precision) but can reduce performance. We will revisit such backend-dependent behavior in Section 5.3.&lt;br /&gt;
&lt;br /&gt;
In summary, &#039;&#039;&#039;FP16 and BF16&#039;&#039;&#039; are now established as important low-precision formats in deep learning. FP16 offers more precision bits, which can matter for e.g. small differences, but has a limited range (care needed to avoid overflow). BF16 offers safety in range but less precision (which can slightly affect convergence or final accuracy in some cases). Both typically rely on FP32 for accumulation to achieve acceptable accuracy. When deploying models, one should choose a format supported efficiently by the target hardware and verify that the accuracy is not significantly degraded. Often, CNNs and transformers can run in FP16/BF16 with virtually no drop in score, but certain operations (like softmax, see Section 6.3) might need special handling.&lt;br /&gt;
&lt;br /&gt;
=== 4.3. FP8 Formats (E4M3, E5M2) and Scaling ===&lt;br /&gt;
&lt;br /&gt;
The push for ever lower precision has led to experimental &#039;&#039;&#039;8-bit floating point (FP8)&#039;&#039;&#039; formats for deep learning. In 2022, researchers from NVIDIA, Intel, and Arm proposed an FP8 standard with two variants: &#039;&#039;&#039;E4M3&#039;&#039;&#039; (4 exponent bits, 3 fraction bits) and &#039;&#039;&#039;E5M2&#039;&#039;&#039; (5 exponent bits, 2 fraction bits)&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;. Both have 1 sign bit, for a total of 8 bits. E5M2 has a wider exponent range (5 bits gives bias 15) but one fewer mantissa bit than E4M3. The idea is to use E4M3 for numbers that need more precision (like weights or activations around 1.0) and E5M2 for numbers that need more dynamic range (like gradient updates which can be very small or large). In fact, the study found that using E4M3 for forward activations/weights and E5M2 for backward gradients can allow training of large networks with FP8 to accuracy on par with FP16 training&amp;lt;ref name=&amp;quot;ref11&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
FP8 formats are &#039;&#039;not&#039;&#039; IEEE 754 standard (though IEEE is considering lower precision in its 2019 revision). Notably, the E4M3 format as used by NVIDIA does not allocate any bit-pattern for $+\infty$ or $-\infty$ – it has only NaN for invalid results, using that freed bit pattern to extend the representable range by one exponent value&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;. Essentially, E4M3 can represent numbers up to ~$2^{15}$ (around 3e4) instead of capping at infinity at $2^{16}$, by foregoing infinities&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;. Variations also exist regarding negative zero and how NaNs are handled&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;. For instance, GraphCore’s IPU uses FP8 formats that disallow negative zero and infinities, to maximize useable range&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Using 8-bit floats for neural networks requires additional techniques, chiefly &#039;&#039;&#039;scaling&#039;&#039;&#039;. Because 8-bit has very limited precision and range, one typically applies a power-of-two scale factor to groups of values to normalize them into an FP8-friendly range. This is akin to block floating-point or “microscaling” per layer or per tensor. For example, one might keep a FP32 scale for each channel or tensor, and represent values as $(FP8\_value) \times 2^{scale}$. The ONNX standard has introduced support for FP8 in version 1.15, including types for E4M3 and E5M2 (with variations FNUZ: finite-only and no negative zero)&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;, and even a type called &#039;&#039;&#039;E8M0&#039;&#039;&#039; which is an 8-bit exponent-only number used purely as a scale factor&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;. The E8M0 type has 8 exponent bits, no mantissa, effectively representing powers of two from $2^{-127}$ to $2^{128}$ for scaling purposes&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;. This indicates an approach where an FP8 tensor would be accompanied by an E8M0 scale (or scales) to interpret its value correctly.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Rounding&#039;&#039;&#039; becomes critical when casting values to FP8. The quantization error is much larger relative to FP16 or FP32. Studies have shown that choosing the right rounding mode (e.g., round-to-nearest, stochastic rounding, etc.) and clamping behavior (saturating to max instead of inf) can affect model accuracy&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;. ONNX added a &amp;lt;code&amp;gt;round_mode&amp;lt;/code&amp;gt; attribute to its Cast operator to allow controlling this for FP8&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;. In particular, one paper found that &#039;&#039;&#039;rounding up with saturation&#039;&#039;&#039; (round-to-nearest ties away from 0, and clamp overflows to max finite rather than inf) gave better accuracy in large language model training than other rounding modes&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
At present, FP8 is on the cutting edge. NVIDIA’s H100 GPU supports FP8 tensor cores (for both E4M3 and E5M2), and early results show minimal accuracy loss on some vision and NLP tasks when properly tuned. However, using FP8 requires fine-tuning hyperparameters like scaling factors for each layer and perhaps tweaking training procedures. ONNX’s inclusion of FP8 types means you can represent and transport models using FP8 weights or activations, but actual hardware support is limited and typically paired with calibration or dynamic scaling logic outside of ONNX graph (or using custom ONNX functions for scaling). It remains an &#039;&#039;&#039;open research area&#039;&#039;&#039; how to fully automate FP8 quantization for arbitrary models without accuracy loss.&lt;br /&gt;
&lt;br /&gt;
In summary, FP8 formats promise further compression of neural network computation and data. They are an order of magnitude more error-prone than FP16, but when combined with careful scaling, mixed usage of E4M3/E5M2, and maybe modified training techniques, they have shown surprising success. ONNX is actively evolving to support these ultra-low precision types, signaling their potential importance in future AI inference deployments.&lt;br /&gt;
&lt;br /&gt;
== Accumulation Semantics in ONNX ==&lt;br /&gt;
&lt;br /&gt;
One of the roles of the ONNX specification is to define the mathematical behavior of operators (Conv, MatMul, Sum, etc.) in a framework-agnostic way. However, ONNX mostly defines &#039;&#039;what&#039;&#039; an operator computes in exact arithmetic, not &#039;&#039;how&#039;&#039; the computation is carried out in finite precision. This leaves certain details – like accumulator precision or summation order – up to the implementation (runtime or hardware). We&#039;ll examine how ONNX treats accumulation and what guarantees (or lack thereof) are provided.&lt;br /&gt;
&lt;br /&gt;
=== 5.1. Operator Definitions and Numerical Guarantees ===&lt;br /&gt;
&lt;br /&gt;
Each ONNX operator is described as performing an ideal mathematical function on its inputs. For example, &amp;lt;code&amp;gt;MatMul&amp;lt;/code&amp;gt; is defined as the standard matrix product of two tensors, and &amp;lt;code&amp;gt;ReduceSum&amp;lt;/code&amp;gt; computes a summation along given axes. The ONNX spec, as of current versions, &#039;&#039;&#039;does not mandate the precision of intermediate calculations&#039;&#039;&#039; beyond the types of the inputs and outputs. If an operator’s inputs and outputs are float16, the spec implies the computation is done in float16 in a sense, but it does not forbid an implementation from using higher precision internally – it simply doesn&#039;t mention it. There is &#039;&#039;&#039;no explicit guarantee of identical bit-for-bit results&#039;&#039;&#039; across different ONNX backends for floating-point ops. Instead, results are expected to be numerically close, within tolerances one would normally expect from floating-point differences (due to different hardware or algorithms). The ONNX Backend Test suite typically uses a relative or absolute error tolerance when validating outputs for this reason.&lt;br /&gt;
&lt;br /&gt;
Because of this, important details like the use of extended precision or accumulation in higher precision are left ambiguous. This has led to discussions in the ONNX community about making such behavior more explicit. For example, a proposal was raised to allow specification of accumulation precision for certain ops like MatMul&amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;. The reasoning is that as lower-precision types (float16, bfloat16, float8) become more common, knowing or controlling whether an op internally accumulates in a higher precision is important for both accuracy and reproducibility&amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;. In June 2025, an issue on the ONNX GitHub suggested adding an attribute to MatMul (and similarly Gemm, Softmax, etc.) to specify the accumulator type&amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;. By default it would use input precision (status quo), but an attribute could request (for instance) 32-bit accumulation even if inputs are 16-bit&amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;. As of this writing, however, no such attribute exists in the official spec – it&#039;s an open issue. Thus, the numerical semantics are effectively: &#039;&#039;“compute the result as if in the same type as inputs, but actual implementations may use more precision internally as an optimization.”&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
From a guarantee standpoint, ONNX operators promise the mathematically correct result assuming real-number arithmetic, but they &#039;&#039;&#039;do not guarantee bit-exact results&#039;&#039;&#039; due to floating-point effects. There is an understanding that differences can arise from different summation orders, fused operations (e.g., an implementation might use an FMA (fused multiply-add) which has one rounding instead of two), or use of extended precision registers. ONNX does not formally define rounding modes either – it relies on IEEE 754 defaults, but if a backend used something like deterministic summation or alternative rounding, ONNX has no mechanism to express that.&lt;br /&gt;
&lt;br /&gt;
In summary, ONNX&#039;s current stance is largely &#039;&#039;&#039;implementation-dependent&#039;&#039;&#039; for numerical precision. It provides minimal explicit numerical guarantees beyond type constraints. This is why an ONNX model run on two different hardware or runtime libraries can have slightly different results, especially in reduced precision. The burden is on backends to produce results &amp;quot;close enough&amp;quot; to each other that they don&#039;t affect the model&#039;s decisions in any significant way.&lt;br /&gt;
&lt;br /&gt;
=== 5.2. MatMul, Conv, and Reduction Operators ===&lt;br /&gt;
&lt;br /&gt;
Consider ONNX &amp;lt;code&amp;gt;MatMul&amp;lt;/code&amp;gt;: it takes two tensors (often two matrices) and produces their matrix product. In pure math, $C_{ij} = \sum_k A_{ik} &#039;&#039; B_{kj}$. If A and B are float16, should that sum be computed in float16 or float32? As discussed, ONNX doesn’t specify – it simply says the output is a float16 matrix containing the product. In practice, many backends will &#039;&#039;promote&#039;&#039; the computation: for example, NVIDIA GPUs will do the multiplication and addition in a mixed FP16/FP32 mode by default (because of tensor cores), then convert the final result back to float16. ONNX Runtime on CPU might not support float16 multiplication natively, so it will insert cast ops to float32, perform the MatMul in float32, then cast back to float16&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;. From ONNX’s perspective, as long as the final result is within acceptable error of the “ideal” float16 computation, it’s fine. But note, an “ideal” float16 computation (where every multiply and add is rounded to FP16) might actually be &#039;&#039;less accurate* than what the backend did with FP32. This means that a backend using higher precision can produce a result that is not bit-equal to a pure FP16 implementation – it&#039;s actually closer to the true real number result. This is usually considered a good thing (improved accuracy), but it does highlight a lack of consistency: another backend might do everything in pure FP16 and get a slightly different answer.&lt;br /&gt;
&lt;br /&gt;
The same situation occurs for &#039;&#039;&#039;convolution&#039;&#039;&#039; (&amp;lt;code&amp;gt;Conv&amp;lt;/code&amp;gt; op) which is essentially a batched sum of products (like MatMul in sliding window), and for explicit reduction ops like &amp;lt;code&amp;gt;ReduceSum&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;ReduceMean&amp;lt;/code&amp;gt;, etc. If you have an ONNX &amp;lt;code&amp;gt;ReduceSum&amp;lt;/code&amp;gt; that sums 1000 float16 values, the spec doesn’t say how to sum them. A GPU kernel might sum in a tree using FP32 accumulators; a naive implementation might sum in order with FP16 and possibly overflow or underflow intermediate steps. Both are allowed as long as they output a float16 result that is “reasonable.” In practice, ONNX Runtime and others take care to avoid obviously bad behavior (like overflow) by using higher precision or by splitting the sum. But again, it’s not mandated by the ONNX standard explicitly.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Softmax&#039;&#039;&#039; is an interesting case: Softmax involves an internal sum of exponentials. ONNX defines the Softmax operator as $$\mathrm{Softmax}(x)_i = \frac{\exp(x_i)}{\sum_j \exp(x_j)},$$ typically applied along a specified axis. Directly computing this in float16 can be problematic for large vectors, as discussed in Section 6.3. Indeed, the issue of accumulation precision arises: should the sum $\sum_j \exp(x_j)$ be done in FP16 or can it be in FP32? Many implementations will do it in FP32 even if $x$ is FP16, because the dynamic range of $\exp(x)$ is large. The ONNX Softmax spec doesn’t detail this, but it expects the result to be a proper probability distribution (summing to 1.0 within normal floating error). A pure FP16 softmax on, say, 1000 elements could sum to something like 0.996 or 1.004 due to rounding, whereas FP32 accumulation would be much closer to 1.000. ONNX likely allows both; the small difference is usually inconsequential, but if it were large (say the FP16 sum overflowed to inf), that would be a correctness issue. A robust backend will prevent that (by using the common max-subtraction trick and higher precision accumulation). The ONNX issue 7072 indeed calls out Softmax as another op where explicit control of accumulation precision would be useful&amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
In summary, &#039;&#039;&#039;MatMul, Conv, and reduction ops&#039;&#039;&#039; in ONNX are defined mathematically but with &#039;&#039;implicit&#039;&#039; numerical behavior. Implementations commonly use higher precision for intermediate sums especially when input is low precision, but this is not standardized. Consequently, results can differ slightly between implementations. For critical applications, one must be aware of these differences. A model exported to ONNX with float16 weights might perform slightly differently on one runtime vs another if one does all float16 math and the other uses float32 accumulations. Both conform to ONNX. As a user, if you require a certain precision for accumulation, currently you have to ensure it by the way you export or run the model (for example, insert manual cast nodes in the ONNX graph to force certain ops to FP32). We may see future ONNX versions address this with attributes or metadata as mentioned, but for now these ops are effectively “compute with best effort accuracy”.&lt;br /&gt;
&lt;br /&gt;
=== 5.3. Backend and Hardware-Dependent Behavior ===&lt;br /&gt;
&lt;br /&gt;
Because ONNX is implemented by many backends (ONNX Runtime, TensorRT, CoreML, PyTorch JIT, etc.), and these in turn run on varied hardware (CPU, GPU, accelerators), the numeric behavior can be backend-dependent. Some examples:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;CPU backends:&#039;&#039;&#039; Many CPUs do not have native float16 vector arithmetic (with a few exceptions in recent CPUs). ONNX Runtime’s default CPU execution provider will often upcast float16 to float32, compute, then downcast&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;. This ensures accuracy but means that if you thought you were getting a full float16 pipeline, you’re not – the CPU is essentially ignoring the low precision in compute. On the other hand, x86 CPUs have extended 80-bit precision in x87 registers (though less used now) and 64-bit SIMD registers for doubles; some compiler optimizations might carry intermediate results in higher precision. This could make CPU results slightly different from a GPU’s if, say, a sum is done in 80-bit on CPU versus 32-bit on GPU. Generally, modern compilers avoid such extended precision unless explicitly asked, to improve reproducibility.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;GPU backends:&#039;&#039;&#039; GPUs often perform summations in parallel using tree reductions. The order of operations can change depending on how threads are scheduled. This means even on the same GPU, summing an array in one launch versus another might not give bit-identical results if the internal parallel reduction algorithm differs (though it should be deterministically the same given the same library version and input, but if you change GPU model or BLAS library, it might differ). Also, GPUs have fused multiply-add (FMA) instructions that do $(a*b + c)$ in one step with one rounding, which can produce slightly different results than doing the multiply and add separately (which would round twice). If an ONNX backend uses a fused kernel (like a GEMM library) it might yield different low-order bits than a simple loop.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Vendor libraries:&#039;&#039;&#039; ONNX Runtime can use vendor BLAS or DNN libraries (MKL, cuDNN, TensorRT, etc.). These libraries often implement their own strategies for numerical stability. For example, NVIDIA’s cuDNN might use FP32 accumulation for FP16 inputs by default. TensorRT (NVIDIA’s inference engine) will sometimes automatically promote precisions or insert scaling to maintain stability&amp;lt;ref name=&amp;quot;ref13&amp;quot;/&amp;gt;. On the other hand, if you explicitly request FP16 in TensorRT with no FP32 fallback, it might do the entire op in FP16 and you’d see more error. Some libraries offer knobs: TensorRT allows choosing FP32, FP16, or INT8 precision for each layer, and will use FP32 internally for safety in some cases unless forced otherwise.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Hardware-specific quirks:&#039;&#039;&#039; As another example, NVIDIA’s &#039;&#039;&#039;TensorFloat-32 (TF32)&#039;&#039;&#039; on Ampere GPUs is a precision mode where matrix multiplications use 10-bit mantissa (so about FP16 precision for the multiply) but keep 8-bit exponent (range like FP32). By default, CUDA libraries use TF32 for FP32 matrix multiplies on Ampere to speed them up, unless you disable it. This means if you run an ONNX model with FP32 on an A100 GPU, your matmuls might actually be lower precision than FP32 (though higher than FP16)&amp;lt;ref name=&amp;quot;ref6&amp;quot;/&amp;gt;. It’s an example of hardware choosing an intermediate precision for performance. From ONNX’s perspective, the output is still FP32 and (hopefully) within error tolerances, but a user might be surprised that it’s not a “true” FP32 computation internally. If exactness is needed, one has to turn off TF32.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Non-determinism:&#039;&#039;&#039; Some backends (especially on GPUs) allow non-deterministic execution for performance. For instance, atomic adds in parallel reductions can produce results that vary run-to-run because the order of accumulation is race-condition-dependent. ONNX’s spec doesn’t cover this explicitly, but it’s something to consider. In ONNX Runtime, there are settings for deterministic computing, but by default it may use all available performance optimizations, even if results vary at the 1e-7 level.&lt;br /&gt;
&lt;br /&gt;
In short, the numerical behavior under ONNX can depend on the combination of backend software and hardware capabilities. The lack of a strict spec for accumulation means implementations have freedom to use clever strategies (higher precision, reordering) to improve accuracy or speed. Usually this is positive (e.g. getting better accuracy than a naive approach). But it could also lead to slight inconsistencies. For example, you might find that exporting a PyTorch model to ONNX and running it with one engine yields outputs slightly different from PyTorch’s – not because ONNX is wrong, but because the summation or convolution was done differently (maybe PyTorch summed in FP32 as well, but maybe not in exactly the same way).&lt;br /&gt;
&lt;br /&gt;
The key takeaway is that ONNX &#039;&#039;by design&#039;&#039; abstracts away these details, which is good for portability but means you need to &#039;&#039;&#039;trust the backend&#039;&#039;&#039; or verify its precision behavior for sensitive models. If necessary, you can enforce certain behavior by inserting ops (for example, if you want to ensure FP32 accumulation, you could insert a cast to FP32 for inputs of a MatMul and cast the result back to FP16 – this way the ONNX graph itself mandates FP32 compute in between). This of course sacrifices some performance or device-specific magic.&lt;br /&gt;
&lt;br /&gt;
Looking forward, as mentioned, there are proposals to enrich ONNX’s specification regarding precision. This might include explicit flags for “exact summation” or “accumulate in int32” etc., which would make models more self-descriptive but also potentially less portable (if a backend doesn’t support that, what then?). For now, users should be aware of these backend differences. Most of the time, well-engineered ONNX runtimes yield results close enough that they don&#039;t affect the application (e.g., classification top-1 remains the same even if low-level sums differ by 1e-5). But for high-stakes numerical computations, testing and maybe adjusting the model or backend settings is prudent.&lt;br /&gt;
&lt;br /&gt;
== Numerical Stability in Deep Learning Models ==&lt;br /&gt;
&lt;br /&gt;
Deep learning computations involve many operations that, while theoretically well-defined, can be sensitive to numerical issues when implemented in finite precision. We&#039;ll discuss a few common scenarios in neural networks where numerical stability is a concern, and how precision and accumulation play a role.&lt;br /&gt;
&lt;br /&gt;
=== 6.1. Dot Products and Large-Scale Reductions ===&lt;br /&gt;
&lt;br /&gt;
Neural networks are full of dot products: every fully-connected layer computes $\mathbf{y} = W\mathbf{x}$, which is a series of inner products between weight vectors and the input. Convolutional layers compute weighted sums of local regions. Attention mechanisms compute large matrix products of query/key/value matrices. All these boil down to summing a lot of terms: $\sum_{k=1}^K a_k b_k$. When $K$ is large (it could be in the thousands or more for a single neuron in a fully connected layer of a large model), the potential for floating-point error in that sum increases.&lt;br /&gt;
&lt;br /&gt;
If using FP32, the error may still be negligible in many cases, but in lower precision like FP16, it can become significant. For example, consider a dot product of length 2048 in FP16. Each product is in FP16 (with ~3 decimal digits precision) and if accumulated in FP16, the worst-case error could be on the order of $2048 \times \varepsilon_{FP16} \approx 2048 \times 0.0009766 \approx 2$ (i.e., you could lose a couple units in the third decimal place relative accuracy in worst case). Even in random error accumulation, you might see error on the order of $\sqrt{2048} \times 0.0009766 \approx 0.044$ or 4.4% (though this is a rough estimate). That’s quite significant. Fortunately, as discussed, most hardware will accumulate such sums in a higher precision like FP32, dramatically reducing that error (~7 orders of magnitude smaller $\varepsilon$). But if it didn’t, the network’s outputs could get noisy. A fully-connected layer with many inputs might output slightly different results depending on summation order if done in low precision.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Large-scale reductions&#039;&#039;&#039; also occur in operations like global average pooling (summing all elements of a feature map) or computing the total loss by summing loss over batch samples. If one sums 100,000 values in FP32, the worst-case error could be on the order of 100,000 &#039;&#039; 1e-7 ≈ 1e-2 (1% uncertainty), though typical error would be much less. In FP16, summing 100k values is likely impossible without overflow: as noted, adding ~10^4 moderate numbers can overflow FP16&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. So large reductions &#039;&#039;must* be handled carefully (either in chunks or in higher precision). Frameworks generally do: for example, when performing a sum reduction on GPU, libraries break it into parallel blocks that accumulate in registers (usually 32-bit registers, even if inputs are 16-bit). &lt;br /&gt;
&lt;br /&gt;
Another aspect is that many dot products in neural nets involve values of varying magnitude. If the distribution of terms is such that some are much larger than others, the smaller terms might not contribute much if summed last. Ideally, one might sort by magnitude or use pairwise summation to improve that. Most BLAS libraries effectively do a pairwise strategy due to their parallel structure. So, while worst-case analyses are grim, practical implementations mitigate a lot of it.&lt;br /&gt;
&lt;br /&gt;
However, certain layers can suffer if not handled properly. One famous example is in RNNs or LSTMs: if one uses FP16 naive summation for the gating mechanism, errors could accumulate over many time steps and cause drift or instability. In modern practice, recurrent layers are often run in FP16 with FP32 accumulators to avoid this.&lt;br /&gt;
&lt;br /&gt;
In summary, &#039;&#039;&#039;dot products and large reductions need sufficient precision&#039;&#039;&#039; or robust algorithms. If you are implementing a custom kernel (say a custom ONNX op) and you have to sum a huge array, you should consider using a technique from Section 3 (pairwise summation, Kahan, or at least double accumulator if possible). Many deep learning computations, by virtue of hardware and library design, already do something like this under the hood. But it&#039;s always good to be aware, especially if pushing into lower precision territories.&lt;br /&gt;
&lt;br /&gt;
=== 6.2. Cancellation, Dynamic Range, and Conditioning ===&lt;br /&gt;
&lt;br /&gt;
We introduced &#039;&#039;cancellation&#039;&#039; earlier in the context of summation. In deep learning, cancellation can appear in a few places. One is in the computation of certain norms or variances. For instance, Batch Normalization computes mean and variance of activations. The variance formula $\mathrm{Var}(x) = E[x^2] - (E[x])^2$ is subject to catastrophic cancellation if $E[x]$ and $\sqrt{E[x^2]}$ are close in magnitude (which can happen if the variance is small compared to the mean). If implemented naively in single precision, you might get a negative variance due to numerical error (which would then be clamped to zero or sqrt would yield NaN). Robust implementations compute variance in a single pass or increase precision. Indeed, in many frameworks, batchnorm accumulation is done in FP32 even if the activations are FP16, precisely to avoid numerical issues in computing mean and variance. ONNX itself doesn’t specify this, but an ONNX backend is likely to do batchnorm stats in higher precision for safety. The &#039;&#039;conditioning&#039;&#039; of the variance calculation is poor when variance is tiny relative to mean, so it&#039;s a spot where higher precision is needed.&lt;br /&gt;
&lt;br /&gt;
Another example is in certain loss functions or regularizations. If you subtract two large numbers to get a small difference, you lose precision. Say you have two nearly identical predictions and you subtract them as part of some operation – if done in float16, you might get zero whereas float32 would have a slight difference. This could affect, say, a gradient if that difference was supposed to propagate information. Fortunately, neural nets often avoid directly subtracting large nearly equal numbers; but it can happen. In attention mechanisms, the &#039;&#039;score normalization&#039;&#039; (softmax) subtracts a max to avoid huge exponentials, which is good for range but if multiple scores are close to the max, you subtract numbers that are close. However, those subtractions are not catastrophic in the same way; they&#039;re intentional to reduce range and are typically fine in FP32 or even FP16.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Dynamic range&#039;&#039;&#039; issues are everywhere in deep learning. Activation functions like ReLU and exponential can produce very large or very small values. If subsequent computations don’t account for that, you get infinities or zeros. For example, consider a network output that produces a logit of 100 in FP16. $\exp(100)$ in FP16 overflows to infinity (since FP16 max ~ 6e4 and $e^{100} \sim 3.7e43$). If you then do softmax, that one infinity will dominate the sum and you might get a probability of 1 for that class (or NaNs if not handled). The standard softmax implementation subtracts the max logit, so in this case it would subtract 100 from all logits, making the largest exponent $\exp(0)=1$ and others tiny – this avoids overflow. But what about underflow? If you had a very negative logit, $\exp(-100)$ in FP16 would underflow to 0. In softmax, that just contributes ~0 probability, which is fine (it effectively is zero anyway). Underflow tends to be less dangerous than overflow in inference — underflow usually just means “this probability/neuron is effectively 0,” whereas overflow can poison the results with NaNs or Infs. &lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Conditioning&#039;&#039;&#039; refers to how sensitive a function is to input perturbations. Neural networks themselves as functions can be poorly conditioned (e.g., tiny changes in weights cause big changes in output for some architectures), but typically they are designed to be reasonably well-behaved. Certain internal computations like matrix inversion (if one does a pseudoinverse or something for a layer) can be extremely ill-conditioned if the matrix is near singular. That again would need double precision perhaps. In ML, one common ill-conditioned operation is the &#039;&#039;&#039;softmax denominator&#039;&#039;&#039;: $\sum_j e^{x_j}$ can be very large and also very sensitive to changes in the largest $x_j$. However, the softmax is more stable if computed carefully (max trick). Another is &#039;&#039;&#039;normalization&#039;&#039;&#039;: computing $\frac{x - \mu}{\sigma}$ for layer norm or batch norm – if $\sigma$ is very small, then the division blows up noise. In those cases, a small $\epsilon$ is usually added to $\sigma^2$ to prevent instability (this is a model-level fix, not a numeric one per se, but it acknowledges numeric limits).&lt;br /&gt;
&lt;br /&gt;
In training, &#039;&#039;&#039;optimizers&#039;&#039;&#039; can suffer from precision issues. For example, the Adam optimizer keeps a moving average of squared gradients; in half precision, these small updates might flush to zero or not change at some point. Some research found that certain adaptive optimizers break in FP16 because the small differences are lost (&amp;quot;Why FP16 training breaks NAdam&amp;quot; etc.). The solution is often to keep the optimizer states in FP32 while doing the bulk of compute in FP16. This again is a form of mixed precision.&lt;br /&gt;
&lt;br /&gt;
From an inference perspective, once the model is trained, the main concerns are avoiding overflow in activations and ensuring summations (like in softmax or reductions) are done with enough precision. The model’s weights are fixed, so we don&#039;t have to worry about cumulative error over iterations as in training, just the forward pass.&lt;br /&gt;
&lt;br /&gt;
=== 6.3. Normalization Layers and Softmax ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Normalization layers&#039;&#039;&#039; (BatchNorm, LayerNorm, etc.) and &#039;&#039;&#039;Softmax&#039;&#039;&#039; are specifically worth examining for numerical stability:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Batch Normalization:&#039;&#039;&#039; During inference, batchnorm applies an affine transform using precomputed mean and variance (from training). It’s basically: $y = \frac{x - \mu}{\sqrt{\sigma^2 + \epsilon}} \gamma + \beta$. This is a stable operation as long as $\epsilon$ is provided (to avoid division by zero). Since $\mu, \sigma^2$ are constants in inference, it reduces to linear ops which are fine. The main numerical work (computing mean/variance) happened during training or model calibration, and frameworks usually did that in FP32. So batchnorm at inference time is not a source of instability (it’s just a scale and shift of the data).&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Layer Normalization / Group Normalization:&#039;&#039;&#039; These compute mean/var on the fly for each sample (and possibly channel group). In inference, they still need to compute these statistics for each forward pass. If implemented in low precision, the variance calculation can suffer cancellation as mentioned. However, it&#039;s likely that inference engines compute the mean and variance in FP32 even if inputs are FP16 (especially since these are per-sample operations, not huge batch reductions – it&#039;s feasible to do in higher precision). If someone implemented layer norm purely in FP16, they might see issues when the variance is very small. Usually there’s an $\epsilon$ added which might dominate a tiny variance and thus mitigate catastrophic cancellation (the result might just output something mostly from $\epsilon$). In summary, normalization layers are usually fine as long as the implementation uses enough precision for the summation of squares.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Softmax:&#039;&#039;&#039; Softmax is notorious if done improperly. The safe algorithm: subtract the maximum input from all inputs (to bring the largest exponent to 1.0), exponentiate, sum them up, divide each exponent by the sum. The max subtraction handles overflow in $\exp()$. But what about the sum? As discussed, if there are $N$ elements in the softmax, the sum is at most $N$ (when all inputs equal the max). In classification, $N$ might be the number of classes. For ImageNet, $N=1000$, so worst sum ~1000 (which is fine in FP16, as 1000 &amp;lt;&amp;lt; 65504). For an extreme case like a language model, $N$ could be 50,000 (vocab size). Sum could be 50,000 in worst case (everyone equal probability). 50,000 is still below 65504, so actually an FP16 sum would &#039;&#039;not&#039;&#039; overflow in that scenario, and the rounding error on a sum of 50k identical terms would be maybe a few ULPs (which at 50k magnitude might be ~0.5 difference worst-case). So maybe softmax summation in FP16 is not catastrophic for output probabilities – you’d get essentially 1.0 after normalization instead of 0.99999 in some case. Usually fine.&lt;br /&gt;
&lt;br /&gt;
:The bigger issue is if one or a few terms dominate the softmax. If one term is much larger, the others underflow to 0 after the max subtraction. That’s fine: you get a one-hot distribution essentially. If the model expected a softer distribution, that could be a problem. But if the logits were really that far apart, even FP32 would give nearly one-hot. So not really a precision issue, more of a model property.&lt;br /&gt;
&lt;br /&gt;
:However, the &#039;&#039;&#039;stability during training&#039;&#039;&#039; is a big issue. The GitHub thread we saw (ColossalAI) mentioned training a 130B model where FP16 softmax in attention caused training to occasionally blow up&amp;lt;ref name=&amp;quot;ref14&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref14&amp;quot;/&amp;gt;. They observed attention scores in some heads ranging up to 1e4 or as low as 1e-3&amp;lt;ref name=&amp;quot;ref14&amp;quot;/&amp;gt;. 1e4 exponent in FP16 is definitely Inf (because $\exp(10^4)$ is astronomically huge, but even exp(100) is inf in half). The method CogView introduced (Precision Bottleneck Relaxation) involved subtracting the max &#039;&#039;per head&#039;&#039; to reduce range&amp;lt;ref name=&amp;quot;ref14&amp;quot;/&amp;gt;—which is basically the softmax max trick at a finer granularity or something similar. In the end, they found &#039;&#039;&#039;the simplest fix was to compute softmax in FP32&#039;&#039;&#039; for those layers&amp;lt;ref name=&amp;quot;ref14&amp;quot;/&amp;gt;. They reported that it had negligible speed impact but significantly improved stability&amp;lt;ref name=&amp;quot;ref14&amp;quot;/&amp;gt;. This highlights a general principle: certain operations like softmax (and sometimes matrix multiplications in attention) benefit from higher precision, and using FP32 in a few critical spots can save a mixed-precision training from crashing. In inference, using FP32 for softmax is also a safeguard if one is uncertain. It costs little because softmax is not a heavy compute compared to matrix multiplies.&lt;br /&gt;
&lt;br /&gt;
:ONNX does have a Softmax operator; if the input is float16, a backend might do exactly what they described: internally cast to float32 for the softmax computation, then cast output back to float16. Some inference engines (like DeepSpeed’s inference or others) keep attention softmax in FP32 even if rest is FP16, for robustness&amp;lt;ref name=&amp;quot;ref14&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Apart from softmax, other places where exponentials occur are in loss functions like cross-entropy (which uses log-softmax internally), or in activation functions like Sigmoid and Tanh (which are basically scaled logistic functions). Sigmoid($x)=1/(1+e^{-x})$. If $x$ is moderately large positive, $e^{-x}$ underflows to 0 in FP16 if $x&amp;gt;~11$. That gives a sigmoid output of 1.0 exactly. In FP32, $e^{-x}$ for $x=11$ is about $5e-5$, not underflow, so sigmoid ~0.99995. So FP16 will saturate some outputs to exactly 0 or 1 sooner than FP32 would. This can slightly change behaviors (e.g., a neuron might go to full saturation a bit early). Usually, this isn’t a huge deal, but it could make gradients zero earlier. In inference, we don’t care about gradients, just the output value. Getting exactly 1.0 vs 0.99995 probably has no practical difference for a classification output. But one should be aware that &#039;&#039;&#039;activation functions can saturate faster&#039;&#039;&#039; in lower precision due to limited range or precision. ReLU has no issue (it’s linear or 0), but exponential-based activations do. In practice, again, frameworks often simply compute those in the given precision and accept the slight difference. If it were a problem, one could do those in FP32 as well.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Summary:&#039;&#039;&#039; Numerical stability in inference is maintained by using robust computation methods: subtracting maxima in softmax/LogSumExp, adding epsilon in denominators for normalization, and using higher precision for summations or critical steps. Most of these are built into the algorithms. ONNX models assume these standard practices (they don&#039;t need to explicitly do them in the graph because the backend inherently will for ops like Softmax). It&#039;s wise when building or exporting models to ONNX to test them in the target precision and ensure there are no surprises (like an output becomes NaN or Inf due to precision). Usually if training was done in mixed precision, the model will have been vetted in that environment already.&lt;br /&gt;
&lt;br /&gt;
== Performance vs. Accuracy Trade-offs ==&lt;br /&gt;
&lt;br /&gt;
There is an inherent trade-off between numerical precision and computational performance (speed, memory usage) in AI systems. Choosing lower precision can dramatically increase throughput and reduce memory, but it may introduce arithmetic errors. Engineers must balance these considerations:&lt;br /&gt;
&lt;br /&gt;
=== 7.1. Throughput, Memory Bandwidth, and Precision ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Throughput (speed)&#039;&#039;&#039; is often higher for lower-precision operations. Modern hardware can execute more low-precision ops in parallel than high-precision ops. For example, an NVIDIA V100 GPU can perform 2× more half-precision FLOPs than single-precision per clock, and newer architectures with Tensor Cores can do 8× more (since a Tensor Core warp operation might perform 64 FP16 FMAs per cycle vs 16 FP32). Similarly, int8 operations can be even faster and more energy efficient. Lower precision numbers also use less memory bandwidth – fetching 16-bit data is twice as fast as 32-bit data from memory (assuming the same bus width), which can relieve memory bottlenecks. Many deep learning workloads are memory-bound, so halving the data size can nearly double throughput if compute is not the limiting factor. This is a huge incentive to use lower precision.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Memory and storage&#039;&#039;&#039;: Using FP16 or int8 weights reduces model size, which is important for deploying large models to memory-limited environments (like edge devices or GPUs with limited VRAM). It also means you can fit larger batch sizes in memory, or more models on a chip, etc. There is also a cascade effect: smaller data means better cache utilization, which further improves performance.&lt;br /&gt;
&lt;br /&gt;
However, &#039;&#039;&#039;accuracy&#039;&#039;&#039; suffers if precision is too low. We have discussed how rounding error and limited range can cause deviations in outputs. The question is: do those deviations matter for the application? Often for neural networks, a small amount of noise or error in intermediate calculations does not change the final prediction. Neural nets are somewhat error-tolerant due to their redundant distributed representations. This is why quantization and FP16 techniques can work at all. The final model accuracy (e.g. top-1 accuracy on ImageNet) might drop only 0.5% or less when using FP16 instead of FP32, or int8 instead of FP16, if done properly. For many use-cases, this loss is acceptable given the speed gains.&lt;br /&gt;
&lt;br /&gt;
If one goes too low (like naive FP8 or int4 without special care), accuracy might drop severely – perhaps making the model unusable. Thus the trade-off: as precision decreases, performance increases but at some point accuracy degrades beyond an acceptable threshold. Engineers aim to find the lowest precision that still preserves accuracy.&lt;br /&gt;
&lt;br /&gt;
There is also the consideration of &#039;&#039;&#039;accumulation strategy&#039;&#039;&#039; affecting performance: for instance, Kahan summation (compensated) is more accurate but ~4× slower than naive summation. If you need that accuracy, you pay a performance cost. In many AI inference scenarios, the slight extra accuracy from something like Kahan isn&#039;t needed, so it’s not used. But in a scenario where you sum millions of elements for a final result that is thresholded, maybe you do need it.&lt;br /&gt;
&lt;br /&gt;
Another dimension is &#039;&#039;&#039;determinism vs speed&#039;&#039;&#039;. Enforcing a deterministic reduction order (for reproducibility) might require disabling some parallelism, which could slow things down. If exact repeatability is critical (like certain scientific or financial applications of neural nets), one might sacrifice some performance to ensure the summation happens the same way every time. ONNX allows both modes depending on the runtime.&lt;br /&gt;
&lt;br /&gt;
From a memory perspective, if precision is higher than needed, you waste memory. For example, storing intermediate activations in FP32 when they could be FP16 uses 2× memory, which might mean lower batch size or more memory traffic. So there&#039;s strong motivation to keep things as low-precision as possible &#039;&#039;except&#039;&#039; where accuracy truly demands.&lt;br /&gt;
&lt;br /&gt;
In summary, the trade-off often manifests as &#039;&#039;diminishing returns&#039;&#039;: FP64 vs FP32 – huge slowdown for usually negligible accuracy gain in ML; FP32 vs FP16 – moderate speedup for usually minor accuracy hit; FP16 vs int8 – another big speedup but maybe more accuracy loss unless calibration is done; int8 vs int4 – speed gain but often big accuracy drop that might not be worth it for current models.&lt;br /&gt;
&lt;br /&gt;
Engineers use techniques like &#039;&#039;&#039;mixed precision&#039;&#039;&#039; to target high precision only where needed. For example, run most of the model in FP16, but keep a few sensitive layers in FP32. This yields most of the performance benefits while ensuring critical parts are accurate. We already saw softmax as one example. Another might be final output layer: sometimes using FP32 for the logits and softmax can be safer.&lt;br /&gt;
&lt;br /&gt;
Hardware is also trending to support mixed modes – like FP8 Tensor Cores accumulate in FP32, etc. So hardware tries to give accuracy cheaply.&lt;br /&gt;
&lt;br /&gt;
=== 7.2. Hardware Accelerators and Tensor Cores ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Hardware accelerators&#039;&#039;&#039; (GPUs, TPUs, FPGAs, ASICs like GraphCore IPU, etc.) have specific support for certain precisions. Leveraging these can provide massive speedups:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;NVIDIA Tensor Cores:&#039;&#039;&#039; These started in Volta (V100) with support for 4x4 matrix multiply-adds on FP16 inputs with FP32 accumulation&amp;lt;ref name=&amp;quot;ref6&amp;quot;/&amp;gt;. Using Tensor Cores can give up to 8x throughput compared to using regular FP32 cores. But to use them, the data needs to be FP16 (or now BF16 or FP8 on newer GPUs) and aligned to certain dimensions (multiples of 8 etc. in matrix sizes, which frameworks handle by padding if needed). If an ONNX model is all FP32, by default it would not use Tensor Cores unless the runtime automatically converts some ops to mixed precision (which some do via a flag or environment variable enabling TF32 or FP16). The introduction of &#039;&#039;&#039;TensorFloat-32 (TF32)&#039;&#039;&#039; in Ampere GPUs is essentially a compromise: treat FP32 inputs as two 16-bit significand halves (10-bit mantissa actually) so that it can run on Tensor Cores, giving ~8x speed, while still outputting FP32. TF32 has about the precision of FP16 but range of FP32, and it was chosen because it typically doesn’t hurt convnet accuracy. ONNX Runtime or others on Ampere will use TF32 by default for conv and matmul unless disabled, giving a free speed boost at minimal accuracy cost. If exact FP32 is needed, users have to turn it off (sacrificing speed).&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;TPU (Google):&#039;&#039;&#039; TPUs have native support for BF16 matrix ops with FP32 accumulators. They were designed specifically for the mixed precision approach in training. For inference, TPUs also support int8 and other quantized ops (e.g., via Edge TPU for lower-power). If exporting ONNX to run on a TPU (via some bridge), one might get automatic BF16 usage. BF16 has the advantage of no loss in range, so TPUs can more straightforwardly run models in BF16 without modifications.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;FPGAs and ASICs:&#039;&#039;&#039; Some custom accelerators might use fixed-point or block floating-point arithmetic for efficiency. They often treat the network in quantized terms. The ONNX quantization specification (QuantizeLinear/DequantizeLinear ops) exists for representing quantized models (INT8 etc.), which is another domain beyond floating-point, but it overlaps as a precision issue. In those systems, accumulators for INT8 × INT8 multiplies are usually 32-bit (to avoid overflow for dot products up to certain lengths). The trade-off there is between integer vs float representation, which can give big speedups on CPUs/DSAs via SIMD instructions (many CPUs have vector int8 multiply-add instructions now for inference).&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Memory bandwidth on hardware:&#039;&#039;&#039; Some accelerators include special high-bandwidth memory or cache specifically for lower precision. For example, an accelerator might fetch two FP16 values in one 32-bit memory transaction – effectively doubling memory bandwidth for half precision. On some hardware, using FP16 can even reduce memory &#039;&#039;latency&#039;&#039; if multiple values can be packed.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Parallelism and Vectorization:&#039;&#039;&#039; Many hardware units process data in fixed vector widths (e.g., 256-bit registers). That could be 8 floats at once or 16 half-floats at once in the same 256 bits. If your code is in FP16, you might use the full vector width (16 values) whereas FP32 might only use half (8 values). This means FP16 can make better use of vector units. Similarly, matrix units often have fixed tile sizes, and smaller data types allow larger tiles or more tiles concurrently.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Energy efficiency:&#039;&#039;&#039; Lower precision often means less energy per operation, which on large deployments (data centers or battery-powered devices) is a significant advantage. This isn&#039;t directly performance, but it&#039;s an important trade: you might choose a slightly lower precision to save power even if accuracy drops a tiny bit.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Software overhead:&#039;&#039;&#039; Using mixed precision on hardware involves some overhead of casting and moving data between precisions. For instance, if you have to frequently cast FP16 to FP32 and back, that adds instructions and memory copies. A well-designed accelerator (like GPUs with unified memory for FP16/32 or special hardware casting) minimize this. Still, the runtime might insert extra operations (as we saw, ONNX Runtime CPU inserted many Cast nodes converting FP16 to FP32 and back&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;, which actually &#039;&#039;slowed down&#039;&#039; that FP16 model by 4x vs FP32 on a CPU, because the CPU had no native FP16). So ironically, on some hardware, using lower precision in ONNX might degrade performance if not supported (the CPU case). Always ensure the target hardware actually supports the low precision you plan to use; otherwise, you&#039;ll pay overhead for emulation.&lt;br /&gt;
&lt;br /&gt;
To illustrate trade-off: Suppose you have a model that achieves 99% accuracy in FP32 and drops to 98.5% in FP16. If FP16 runs 2x faster, many would accept the 0.5% accuracy drop for double throughput. If int8 runs 4x faster but accuracy drops to 95%, that might be too much drop for some applications, but acceptable for others that value speed. It&#039;s application-dependent.&lt;br /&gt;
&lt;br /&gt;
In critical applications, one might maintain higher precision accumulators or run certain validation steps in high precision to ensure nothing drifted. In research, sometimes a model is trained in mixed precision but validated in double to ensure any small numeric differences don’t influence the reported result (rare, but for scientific computing integration maybe).&lt;br /&gt;
&lt;br /&gt;
In the ONNX world, performance-vs-accuracy is often managed via &#039;&#039;&#039;Execution Providers&#039;&#039;&#039; (EPs) that target specific hardware. For example, the TensorRT EP in ONNX Runtime can take an ONNX model and run it using NVIDIA&#039;s high-performance inference library, which will use FP16 or INT8 if allowed, to maximize speed. The user can configure the EP with precision flags. If the accuracy drop is too high, one might restrict it to FP32. If it&#039;s fine, allow FP16. Tools exist to measure the accuracy difference (like calibrating int8 quantization).&lt;br /&gt;
&lt;br /&gt;
To conclude, the trade-offs revolve around picking the lowest precision that yields acceptable accuracy, and utilizing hardware features fully. ONNX being a high-level spec abstracts this, but it&#039;s ultimately up to the deployment to decide and sometimes up to the exporter to incorporate quantization. ONNX’s role is to be able to &#039;&#039;represent&#039;&#039; these choices (they have added quantized types, FP8 types, etc., to allow the graph to include those when needed).&lt;br /&gt;
&lt;br /&gt;
== Best Practices and Design Guidelines ==&lt;br /&gt;
&lt;br /&gt;
Given the above insights, here are some best practices and guidelines for dealing with numerical precision and accumulators in AI inference (and training) design:&lt;br /&gt;
&lt;br /&gt;
=== 8.1. Choosing an Accumulator Precision ===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Match precision to data length and range:&#039;&#039;&#039; If an operation accumulates a large number of terms, consider using a higher precision accumulator than the input data. For example, for summing &amp;gt;1000 values of type FP16, an FP32 accumulator is recommended to avoid large rounding error or overflow. If summing &amp;gt; million values of FP32, consider double (FP64) accumulation if high accuracy is needed (though this is rare in DL). &#039;&#039;Rule of thumb:&#039;&#039; if $N \cdot \varepsilon$ is not tiny (say &amp;gt;1e-3), you may benefit from higher precision. For FP16 ($\varepsilon \approx 1e-3$), that threshold N is a few hundred. For FP32 ($\varepsilon \approx 1e-7$), N would be in the millions before 1e-3 error.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Use higher precision in critical layers:&#039;&#039;&#039; Identify if certain layers of the model are more sensitive to precision. For instance, the output layer or a layer that feeds into a very sharp decision boundary might require more precision. If a small change there could flip the prediction, you don&#039;t want excessive numerical error. Using FP32 for logits and softmax is a common practice to maintain classification confidence stability.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Follow hardware defaults, but override if needed:&#039;&#039;&#039; Many hardware accelerators have sensible defaults (like accumulate in FP32 for tensor ops). It&#039;s usually best to use those defaults as they were chosen to balance accuracy and speed. Only override if you have evidence of a problem. For example, if using ONNX Runtime on CPU, you discovered it upcasts to float for you – great. But if using some custom accelerator that does pure FP16, you might manually insert casts in the ONNX graph around certain ops to force higher precision on those ops.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Maintain precision for gradient accumulation (training):&#039;&#039;&#039; This is more training-oriented, but worth noting: when summing gradients over batches or updating weights, use higher precision (FP32 or even 64) for those accumulations. This ensures training doesn&#039;t diverge due to numeric error. Many frameworks keep a &amp;quot;master copy&amp;quot; of weights in FP32 when training in FP16&amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;. For inference, this translates to: any running statistics or counters in the model (like if you had a running average inside the model) should probably be in higher precision to avoid drift.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Be mindful of intermediate cast costs:&#039;&#039;&#039; If you do decide to cast to higher precision for an operation in ONNX, remember it incurs memory copies and compute overhead. Make sure it&#039;s worth it. For example, if you cast a huge tensor to double, that could slow things drastically. Usually FP16-&amp;gt;FP32 is fine (hardware handles it well), but FP32-&amp;gt;FP64 might be expensive on GPUs.&lt;br /&gt;
&lt;br /&gt;
=== 8.2. Safe Reduction Lengths and Heuristics ===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Chunk long reductions:&#039;&#039;&#039; If you must do a very long sum in low precision (due to memory constraints or hardware), chunk the sum into parts, sum each to a higher precision local accumulator, then sum the partial sums. This is essentially pairwise summation. Many libraries use a fixed chunk size heuristic (e.g., sum in blocks of 256 in registers, then add those up). This bounds the error growth. If implementing manually, you can choose a chunk size such that chunk_length &#039;&#039; ε is small (like 256 &#039;&#039; 1e-3 = 0.256 for FP16, hmm maybe choose 100).&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Sort by magnitude if possible:&#039;&#039;&#039; In some cases (not typical in DL, but possible in post-processing), if you have numbers varying by orders of magnitude, summing from smallest to largest improves accuracy. It prevents small ones from being swallowed by big ones. This is more relevant if you output a sum of mixed-scale values (like some algorithm mixing big and small contributions). It’s not always feasible in real-time inference to sort, but keep in mind.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Use compensation for critical sums:&#039;&#039;&#039; If you find a particular sum or difference in your inference computation is crucial (like computing a small residual from two close numbers), consider using Kahan summation or another compensated scheme just for that part. For example, if you had an ONNX custom function that needs to sum a lot of almost cancelling terms, you might implement it with compensation internally.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Heuristics for FP8/low-bit:&#039;&#039;&#039; When using extremely low precision like FP8, adopt the recommended practices: use scaling (don&#039;t feed raw data into FP8, normalize it first), and perhaps only use FP8 on portions of the network that are empirically robust. The industry is still establishing heuristics, but one might be: use FP8 for matrix multiplications, but accumulate in FP16 or FP32; keep normalization in FP16; use FP16 for first and last layer maybe. This is evolving, so keep an eye on literature.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Keep an eye on sum length in softmax and normalization:&#039;&#039;&#039; If you have extremely large class counts or group sizes (e.g., a softmax over 1e6 items, or layer norm over tens of thousands of features), realize this is a giant reduction. You may want to ensure that reduction is done in higher precision. ONNX Softmax by default will likely not handle a million-class case in FP16 well (the sum would overflow since 1e6 &amp;gt; 65504). So either do that on CPU in FP32 or break it down (could do hierarchical softmax, etc.). This is a corner case, but relevant for huge vocab language models. In practice, vocab ~50k is fine as we said.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Test numeric differences:&#039;&#039;&#039; A practical tip: after quantizing or reducing precision, run some test inputs through both the high-precision model and low-precision model. Compare outputs (and maybe some intermediate signals if possible). If differences are consistently small relative to the application tolerance, you&#039;re good. If you see occasional large differences or instabilities (like outputs become NaN or Inf in low precision), that&#039;s a red flag. Use that testing to guide where you might need to increase precision or change algorithms.&lt;br /&gt;
&lt;br /&gt;
=== 8.3. When Higher Precision Is Required ===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Ill-conditioned calculations:&#039;&#039;&#039; If your model or pipeline performs any operation known to be ill-conditioned (matrix inversion, subtraction of nearly equal quantities, computing a tiny difference of large numbers), you likely need higher precision for that part. For example, if your ONNX model includes a custom op to solve a linear system, use double precision in that op if possible (or at least float with iterative refinement). In ML, this might occur in something like a physics-informed neural network or a differentiable simulation within the model.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Strict accuracy requirements:&#039;&#039;&#039; Some applications (medical, aerospace, finance) might have strict tolerances. If an inference model’s output needs to be correct to 5 decimal places, you probably cannot use FP16, which has only 3-4 decimal digits precision. FP32 might be barely enough (~7 digits). FP64 might be warranted to guarantee that level of accuracy in worst case. Most deep learning applications (like image classification) don&#039;t demand that level of numeric precision in outputs – as long as the top class is correct, it doesn&#039;t matter if its probability was 0.901 vs 0.899. But if you&#039;re doing something like summing probabilities to make a risk estimate or doing a cumulative sum over many predictions, be careful to accumulate precisely.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Large sums or products in post-processing:&#039;&#039;&#039; Sometimes the neural net is fine, but the &#039;&#039;post-processing&#039;&#039; of results might need precision. E.g., summing up probabilities of many events, or computing confidence intervals, etc. Do those calculations in double if needed outside the model.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Preventing drift in long sequences:&#039;&#039;&#039; For sequential models (RNNs processing thousands of steps), rounding error can accumulate over steps. If each step has a small error, after 10000 steps it could add up. In such cases, using a higher precision for the state might reduce drift. Some practitioners use FP64 for the hidden state accumulation in very long sequence processing to maintain numerical stability (though this is not common due to speed loss). Alternatively, periodically renormalize or reset small errors.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Simulations or differential equations:&#039;&#039;&#039; If your ONNX model involves simulating something (like a differential equation solver inside a model, or an ODE/RNN hybrid), those often need double precision to be stable for long durations. Recognize those situations and allocate precision accordingly.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Verification and debugging:&#039;&#039;&#039; Use higher precision as a tool to verify correctness. For example, run your model in FP64 (if possible) and compare to FP32 to see if there are differences. If not, FP32 is fine. If yes, you might find a particular calculation losing too much info in FP32, implying maybe you should keep it in FP64 in production as well. This is rare in inference but can be part of rigorous validation.&lt;br /&gt;
&lt;br /&gt;
In ONNX, using higher precision may involve casting to &amp;lt;code&amp;gt;tensor(double)&amp;lt;/code&amp;gt; for certain ops. Not all runtimes support double on GPU though (some do via emulation or just slower path). ONNX CPU does support double for general ops. So be mindful of the target.&lt;br /&gt;
&lt;br /&gt;
Finally, &#039;&#039;&#039;document your precision choices&#039;&#039;&#039;. If you deliver an ONNX model that uses mixed types (float16 inputs, float32 accumulations, int8 weights, etc.), ensure that is clear to whoever maintains or uses it. Precision issues can be sneaky, so clarity helps.&lt;br /&gt;
&lt;br /&gt;
== Limitations and Open Problems ==&lt;br /&gt;
&lt;br /&gt;
Despite advances in handling numerical precision, there remain limitations and open challenges in this domain:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Lack of Precision Specification in ONNX:&#039;&#039;&#039; As noted, ONNX currently has no way to formally specify that an op &#039;&#039;should&#039;&#039; use a higher precision internally. This can lead to nondeterministic differences across platforms&amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;. While proposals exist to add attributes for this&amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;, it is not yet standard. Thus an ONNX model might behave slightly differently on two compliant backends, and there&#039;s no built-in mechanism to ensure identical results. This is a limitation for applications requiring cross-platform consistency. Solving it may involve extending ONNX (e.g., an attribute like &amp;lt;code&amp;gt;accumulatePrecision=&amp;quot;FLOAT32&amp;quot;&amp;lt;/code&amp;gt; for MatMul) or tightening the spec, but those come with backward compatibility and implementation burden concerns.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Reproducibility and Determinism:&#039;&#039;&#039; Even with the same precision, parallel computation can introduce nondeterminism (due to different summation orders). Ensuring bit-identical results across runs (important for some sensitive domains) is non-trivial. Frameworks often provide &amp;quot;deterministic mode&amp;quot; which might use fixed ordering at cost of speed. In ONNX, there&#039;s no global flag for this; it&#039;s up to the runtime. More research could be done on algorithms that produce bit-wise identical results independent of parallel execution order (perhaps using techniques like pairwise summation or consistent partitioning). Stochastic rounding is another area: sometimes using probabilistic rounding can reduce bias in accumulated error, but it introduces randomness – reconciling that with reproducibility is tricky.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Ultra-low Precision (FP8 and below):&#039;&#039;&#039; While FP8 is being explored, going to 4-bit floats or mixed schemes is an open challenge. The OpenAI community is investigating 4-bit (quantization-wise) for LLMs with some success, but a true 4-bit float (like E3M1) would be extremely limited. Perhaps hybrid schemes (like block floating point, log scales, etc.) will come. ONNX already has some support (they mention 4-bit in documentation placeholders&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;), but it&#039;s nascent. Research into training techniques, calibration, and new number formats (like &#039;&#039;&#039;posit numbers&#039;&#039;&#039; or tapering precision floats) is ongoing. If a new format proves useful, ONNX will need to adapt to represent it.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Quantization Noise and Model Robustness:&#039;&#039;&#039; Reducing precision can sometimes expose models to issues like adversarial vulnerability or fragility – e.g., a slight perturbation might flip a decision if the margin was small and precision loss nudged it. Ensuring model robustness under quantization is an open problem. It might require re-training the model with quantization in the loop (quantization-aware training) or designing models that have large &amp;quot;margin&amp;quot; between classes to tolerate quantization noise.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Dynamic range during training vs inference:&#039;&#039;&#039; In training, activations can have wider distributions (especially early in training). Models often learn to constrain their ranges (via activation functions saturating or normalization), which makes inference more stable in lower precision. But if one tries to train in FP8 from scratch, it&#039;s very hard. This gap between what&#039;s possible in a post-trained inference model vs training is an open area – how to train directly in low precision without significant accuracy loss. Solving that could simplify workflows (no need for high-precision training then quantize; just train low-prec end to end). Techniques like loss scaling, gradient clipping, etc., are partial solutions. The development of optimizers that are resilient to low precision is ongoing.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Hardware variability:&#039;&#039;&#039; As new hardware emerges (with new numeric capabilities or quirks), ensuring that ONNX models run consistently is an ongoing task. For instance, one device might implement a fused op that reduces rounding error, another does it separately. Even something like convolution might use a different algorithm (FFT-based vs direct) which has different numeric properties. Open problem: could ONNX allow specifying numeric tolerances per op or preferred algorithms? That may be overkill for most, but for some HPC ML use cases, maybe desired.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Testing and validation of numerics:&#039;&#039;&#039; There isn&#039;t a standardized set of &amp;quot;numeric compliance&amp;quot; tests for ONNX runtimes beyond simple correctness. Perhaps in the future, test suites might include challenging numeric scenarios (large sums, nearly singular matrices) to ensure a runtime handles them gracefully (maybe by switching precision). Right now, it&#039;s mostly on the user to test their model under expected conditions.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Combining quantization with floating point:&#039;&#039;&#039; Many production models use a mix (e.g., int8 for some layers, FP16 for others). ONNX can represent this (with QuantizeLinear ops or MixedPrecision through casting), but tooling to automatically find the optimal mix is still developing. It&#039;s somewhat trial-and-error or using heuristics. An open question is how to automate precision assignment: given a model and a target hardware, how to automatically choose which layers can be int8, which should be FP16, etc., to meet an accuracy target. This crosses into NAS (neural architecture search) territory or automated quantization. &lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Numerical debugging tools:&#039;&#039;&#039; When a model goes wrong due to precision, it&#039;s often hard to pinpoint where. Better tooling to analyze numerical error layer by layer could be useful. For instance, one could run a model in double and float and diff the intermediate results to see where the largest divergence happens. Doing this systematically for big nets is an open tooling challenge. Some frameworks have hooks for this, but not standardized.&lt;br /&gt;
&lt;br /&gt;
In conclusion, while we&#039;ve come far with using lower precision in AI without sacrificing much accuracy, the landscape is always evolving. The key open problem is how to push precision lower (for efficiency gains) without crossing the threshold where accuracy falls off a cliff. Each new generation of hardware and techniques (like FP8, novel quantization schemes) extends that boundary, but also brings new questions on how to manage the numerical errors. ONNX as an interchange will need to keep pace, possibly by adding more expressive power for these numeric considerations.&lt;br /&gt;
&lt;br /&gt;
== See Also ==&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;IEEE 754 Floating-Point&#039;&#039;&#039; – The standard defining binary16, binary32, binary64 formats and rounding behavior.&lt;br /&gt;
* &#039;&#039;&#039;Floating-Point Arithmetic&#039;&#039;&#039; – General topic on how floating-point math works, common issues (overflow, underflow, cancellation).&lt;br /&gt;
* &#039;&#039;&#039;Kahan Summation Algorithm&#039;&#039;&#039; – Algorithm for compensated summation to reduce numerical error&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;.&lt;br /&gt;
* &#039;&#039;&#039;BFloat16&#039;&#039;&#039; – A short-format float with 8-bit exponent, used in TPUs and some GPUs for training networks.&lt;br /&gt;
* &#039;&#039;&#039;Mixed Precision Training&#039;&#039;&#039; – Technique of using FP16 (or BF16) along with FP32 accumulators to accelerate training&amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;.&lt;br /&gt;
* &#039;&#039;&#039;Quantization in Deep Learning&#039;&#039;&#039; – Converting networks to lower bit integer arithmetic for inference (INT8 quantization and calibration techniques).&lt;br /&gt;
* &#039;&#039;&#039;ONNX Operator Specification&#039;&#039;&#039; – Official docs for ONNX ops, which describe their mathematical function but not the required numeric precision (a point of potential extension in future).&lt;br /&gt;
* &#039;&#039;&#039;CUDA Tensor Cores &amp;amp; TF32&#039;&#039;&#039; – NVIDIA documentation on how tensor cores perform mixed precision and the TF32 format for accelerating FP32.&lt;br /&gt;
* &#039;&#039;&#039;Pairwise Summation&#039;&#039;&#039; – Summation algorithm with logN error growth&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;, used in many numerical libraries to improve accuracy of reductions.&lt;br /&gt;
* &#039;&#039;&#039;Softmax Stability&#039;&#039;&#039; – Articles on numerically stable softmax and log-sum-exp implementations to avoid overflow.&lt;br /&gt;
* &#039;&#039;&#039;Performance Tuning for Low Precision&#039;&#039;&#039; – Guides (e.g., NVIDIA or Intel whitepapers) on using low precision and the trade-offs involved in various hardware.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;references&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref1&amp;quot;&amp;gt;Accelerate and simplify Scikit-learn model inference with ONNX Runtime - Microsoft Open Source Blog — opensource.microsoft.com — https://opensource.microsoft.com/blog/2020/12/17/accelerate-simplify-scikit-learn-model-inference-onnx-runtime&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref2&amp;quot;&amp;gt;Floating Point Precision: Understanding FP64, FP32, and FP16 in Large Language Models - DEV Community — dev.to — https://dev.to/lukehinds/floating-point-precision-understanding-fp64-fp32-and-fp16-in-large-language-models-3gk6&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref3&amp;quot;&amp;gt;Floating Point Precision and Its Limitations | by Umair Akbar | Medium — akbu.medium.com — https://akbu.medium.com/floating-point-precision-and-its-limitations-cfb7247d7789&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref4&amp;quot;&amp;gt;Kahan summation algorithm - Wikipedia — en.wikipedia.org — https://en.wikipedia.org/wiki/Kahan_summation_algorithm&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref5&amp;quot;&amp;gt;Pairwise summation - Wikipedia — en.wikipedia.org — https://en.wikipedia.org/wiki/Pairwise_summation&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref6&amp;quot;&amp;gt;Numerical behavior of NVIDIA tensor cores - PMC — pmc.ncbi.nlm.nih.gov — https://pmc.ncbi.nlm.nih.gov/articles/PMC7959640/&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref7&amp;quot;&amp;gt;Mixed precision and performance-accuracy tuning (neuron-cc) — AWS Neuron Documentation — awsdocs-neuron.readthedocs-hosted.com — https://awsdocs-neuron.readthedocs-hosted.com/en/latest/about-neuron/appnotes/neuron-cc/mixed-precision.html&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref8&amp;quot;&amp;gt;Create Float16 and Mixed Precision Models - ONNX Runtime — onnxruntime.ai — https://onnxruntime.ai/docs/performance/model-optimizations/float16.html&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref9&amp;quot;&amp;gt;[Performance] ONNX FP16 model is having performance bottle neck when compared to FP32 variant · Issue #25824 · microsoft/onnxruntime · GitHub — github.com — https://github.com/microsoft/onnxruntime/issues/25824&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref10&amp;quot;&amp;gt;Float stored in 8 bits - ONNX 1.21.0 documentation — onnx.ai — https://onnx.ai/onnx/technical/float8.html&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref11&amp;quot;&amp;gt;[2209.05433] FP8 Formats for Deep Learning — arxiv.org — https://arxiv.org/abs/2209.05433&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref12&amp;quot;&amp;gt;Should MatMul op allow specification of accumulation precision? · Issue #7072 · onnx/onnx · GitHub — github.com — https://github.com/onnx/onnx/issues/7072&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref13&amp;quot;&amp;gt;Quantize ONNX models | onnxruntime — onnxruntime.ai — https://onnxruntime.ai/docs/performance/model-optimizations/quantization.html&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref14&amp;quot;&amp;gt;Keep Attention Softmax FP32 during FP16/ZeRO Training · Issue #1485 · hpcaitech/ColossalAI · GitHub — github.com — https://github.com/hpcaitech/ColossalAI/issues/1485&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;/references&amp;gt;&lt;/div&gt;</summary>
		<author><name>Timo.stripf</name></author>
	</entry>
	<entry>
		<id>https://www.emmtrix.com/w139/index.php?title=Numerical_Precision_in_ONNX_and_AI_Inference&amp;diff=2897</id>
		<title>Numerical Precision in ONNX and AI Inference</title>
		<link rel="alternate" type="text/html" href="https://www.emmtrix.com/w139/index.php?title=Numerical_Precision_in_ONNX_and_AI_Inference&amp;diff=2897"/>
		<updated>2026-01-30T15:21:36Z</updated>

		<summary type="html">&lt;p&gt;Timo.stripf: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&#039;&#039;&#039;Open Neural Network Exchange (ONNX)&#039;&#039;&#039; is an open standard format for representing machine learning models and neural network computations across different frameworks and hardware&amp;lt;ref name=&amp;quot;ref1&amp;quot;/&amp;gt;. As models are exported and deployed via ONNX, the &#039;&#039;&#039;numerical precision&#039;&#039;&#039; of computations becomes critical. Deep learning inference involves a variety of floating-point operations, and small numerical discrepancies can accumulate and affect model accuracy or reproducibility. This article provides an in-depth overview of floating-point precision issues in numerical computing, with a focus on how they manifest in AI inference and the ONNX ecosystem. We cover floating-point formats and rounding behavior, accumulation strategies for summations, the use of low-precision formats (FP16, BFloat16, FP8) in modern AI, and how ONNX defines (or leaves unspecified) the precision of accumulators in operations. Strategies for maintaining numerical stability in deep neural networks are discussed, along with the trade-offs between computational performance and arithmetic accuracy. The goal is to give engineers and researchers a comprehensive, encyclopedic reference on numerical precision in ONNX-based AI inference systems.&lt;br /&gt;
&lt;br /&gt;
== Floating-Point Precision in Numerical Computing ==&lt;br /&gt;
&lt;br /&gt;
=== 2.1. IEEE 754 Floating-Point Formats ===&lt;br /&gt;
&lt;br /&gt;
Modern computers represent real numbers using the IEEE 754 floating-point standard, which defines formats like 32-bit &#039;&#039;single precision&#039;&#039; (FP32) and 64-bit &#039;&#039;double precision&#039;&#039; (FP64). In IEEE 754 binary formats, a floating-point number is composed of three fields: 1 sign bit, several exponent bits (with a bias), and several fraction (mantissa) bits. For example, FP32 consists of 1 sign bit, 8 exponent bits, and 23 fraction bits (24 bits of significand precision including the implicit leading 1)&amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;. This provides roughly 7 decimal digits of precision and an exponent range allowing values on the order of $10^{\pm38}$. FP64 allocates 1 sign bit, 11 exponent bits, and 52 fraction bits (53-bit precision), yielding about 15–16 decimal digits of precision and a vastly larger range (~$10^{\pm308}$)&amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;. These larger formats (especially FP64) are the &amp;quot;gold standard&amp;quot; for numerical accuracy in scientific computing, as they reduce round-off error in complex calculations&amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;. However, the increased precision comes at the cost of doubled storage and typically reduced computational throughput (e.g. many GPUs execute FP64 instructions at a much lower rate than FP32&amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
In addition to single and double precision, the IEEE 754-2008 standard introduced a 16-bit &#039;&#039;half precision&#039;&#039; format (FP16, or binary16). FP16 has 1 sign bit, 5 exponent bits, and 10 fraction bits (11-bit significand including the implicit 1)&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. This format covers a much smaller dynamic range (approximately $6.1\times10^{-5}$ to $6.5\times10^4$ for normalized values&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;) and offers only around 3 decimal digits of precision. Half precision was originally used in graphics, but it has gained popularity in machine learning for its speed and memory advantages (discussed later in Section 4). Another format relevant to AI is &#039;&#039;&#039;BFloat16&#039;&#039;&#039; (Brain Float 16), a 16-bit float with &#039;&#039;&#039;8 exponent bits and 7 fraction bits&#039;&#039;&#039;&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. BFloat16 sacrifices precision (only ~7 fraction bits ≈ 2–3 decimal digits) in order to have the &#039;&#039;&#039;same exponent range as FP32&#039;&#039;&#039;&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. This means BFloat16 can represent very large or very small numbers (∼$10^{\pm38}$, similar range as FP32) but with much less precision between representable values&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. BFloat16 was introduced for deep learning by Google (for TPUs) to allow reduced precision &#039;&#039;training&#039;&#039; without frequent overflow/underflow, relying on the wide range and accepting the coarse precision.&lt;br /&gt;
&lt;br /&gt;
Each floating-point format has a &#039;&#039;&#039;unit roundoff&#039;&#039;&#039; or machine precision, denoted $\epsilon$. This is effectively the spacing between 1.0 and the next representable number. For FP32, $\epsilon \approx 2^{-23} \approx 1.19\times10^{-7}$, corresponding to about 7–8 decimal digits of precision&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. In FP64, $\epsilon = 2^{-52} \approx 2.22\times10^{-16}$ (15–16 decimal digits). Half precision is much less precise: FP16 has $\epsilon = 2^{-10} \approx 9.76\times10^{-4}$&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;, and BFloat16 has $\epsilon = 2^{-7} = 7.8125\times10^{-3}$&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. These values quantify how finely the continuum of real numbers is quantized in each format.&lt;br /&gt;
&lt;br /&gt;
=== 2.2. Machine Epsilon and Rounding Error ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Machine epsilon ($\varepsilon$)&#039;&#039;&#039; is formally defined as the smallest positive number such that $1.0 + \varepsilon &amp;gt; 1.0$ in the floating-point format&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. It effectively measures the &#039;&#039;&#039;precision&#039;&#039;&#039; of the representation. In FP32, $\varepsilon = 2^{-23} \approx 1.19\times10^{-7}$, meaning any result is rounded to about 7 decimal digits. FP16’s $\varepsilon = 2^{-10} \approx 9.77\times10^{-4}$, over 1000× larger (so only ~3 decimal digits of precision), and BFloat16’s $\varepsilon = 2^{-7} \approx 7.8\times10^{-3}$, reflecting its mere ~8-bit precision&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. Rounding in IEEE 754 by default uses &#039;&#039;&#039;round-to-nearest (ties to even)&#039;&#039;&#039;, which ensures the rounding error for any single operation is at most half a unit in the last place (0.5 ULP). We can model a floating-point addition as: &lt;br /&gt;
&lt;br /&gt;
$$\text{fl}(a+b) = (a+b)\,(1 + \delta),$$ &lt;br /&gt;
&lt;br /&gt;
where $|\delta| \le \varepsilon$ for that format&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. Here $\varepsilon/2$ is the maximum &#039;&#039;&#039;relative error&#039;&#039;&#039; in one rounding (also called unit roundoff). For example, when adding two FP32 numbers, the result is accurate to within about $10^{-7}$ of the true sum (relative), whereas in BF16, the result can deviate by up to $\sim7.8\times10^{-3}$ (nearly 0.78% relative error per operation)&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. Thus, &#039;&#039;&#039;each arithmetic operation incurs a small rounding error&#039;&#039;&#039; (on the order of machine epsilon) that can propagate or accumulate through a sequence of calculations.&lt;br /&gt;
&lt;br /&gt;
Because floating-point representations have finite exponent range, we must also consider &#039;&#039;&#039;overflow&#039;&#039;&#039; and &#039;&#039;&#039;underflow&#039;&#039;&#039;. If a result’s magnitude exceeds the maximum representable value, it overflows to infinity; if it falls below the minimum normal (or minimum subnormal) value, it underflows to zero (or a denormalized tiny value). FP32 and BF16 share an exponent size (8 bits), so their range for normal numbers is roughly $[10^{-38}, 10^{38}]$ (approx $~\!3.4\times10^{38}$ max)&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. FP16’s 5-bit exponent allows max $\sim 6.55\times10^4$ and min normal $\sim 6.10\times10^{-5}$&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. In practice, &#039;&#039;&#039;underflow/overflow&#039;&#039;&#039; can occur in low-precision arithmetic if values are not appropriately scaled. For instance, summing a large number of moderately sized terms in FP16 could overflow where FP32 would not. As an example, summing on the order of $10^4$ values of magnitude ~$10^1$ (say, adding 10,000 terms around 10 each) would produce a total $\sim10^5$, which exceeds FP16’s max finite value ~$6.5\times10^4$ and would overflow to $+\infty$ in half precision&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. Underflow is usually a concern when subtracting nearly equal numbers (catastrophic cancellation yielding a tiny difference that might flush to zero in low precision).&lt;br /&gt;
&lt;br /&gt;
=== 2.3. Accumulation Error in Reductions ===&lt;br /&gt;
&lt;br /&gt;
Floating-point addition is &#039;&#039;&#039;not associative&#039;&#039;&#039;: $(a+b)+c$ can differ from $a+(b+c)$ due to rounding, and thus the order in which we sum a list of numbers can change the result slightly. When summing a large sequence of $N$ numbers $x_1, x_2, ..., x_N$, the &#039;&#039;&#039;accumulated rounding error&#039;&#039;&#039; can be significant. In the &#039;&#039;worst case&#039;&#039; (e.g., adding many positive and negative terms that cause maximal cancellation or always rounding one way), the error can grow on the order of $O(N \varepsilon)$ relative to the exact sum&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;. Simply summing $N$ numbers in sequence (the &#039;&#039;&#039;naive linear summation&#039;&#039;&#039;) has a worst-case error bound proportional to $N\,\varepsilon$&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. This worst-case occurs for adversarial arrangements of values; in more typical scenarios with rounding errors of random signs, one can model the error as a random walk, yielding an expected root-mean-square (RMS) error growth on the order of $O(\sqrt{N}\,\varepsilon)$&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;. Still, with very large $N$, even $\sqrt{N}$ times $\varepsilon$ can become large enough to matter.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Cancellation&#039;&#039;&#039; is a particular problem: if we add two nearly equal numbers of opposite sign, most leading digits cancel and the result loses significant bits of precision. For example, in single precision, subtracting two numbers that agree in the first 6 decimal digits will yield a result with only ~1 digit of accuracy left. Catastrophic cancellation can dramatically increase relative error. Summation is especially sensitive to cancellation if large positive and negative terms are summed naively. The &#039;&#039;&#039;condition number&#039;&#039;&#039; of a summation (the ratio of the sum of absolute values $\sum|x_i|$ to the absolute value of the true sum $|\sum x_i|$) indicates how sensitive the result is to perturbations&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;. If the numbers have mixed signs and nearly cancel out (making the condition number large), even an optimal summation order will have a large relative error. For instance, summing [1.0, $10^{100}$, 1.0, $-10^{100}$] in double precision has an exact sum of 2.0, but naive summation may yield 0.0 due to cancellation; an improved summation algorithm yields the correct 2.0&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
In practical terms, when summing a long list of floating-point numbers (such as accumulating a dot product or reducing a tensor), the &#039;&#039;&#039;accumulation error&#039;&#039;&#039; can become noticeable if no precautions are taken. Techniques to mitigate this error include carefully choosing the summation order or using higher precision for intermediate sums. We will explore these strategies in the next section.&lt;br /&gt;
&lt;br /&gt;
== Accumulators and Reduction Strategies ==&lt;br /&gt;
&lt;br /&gt;
When performing reductions (sums, dot products, etc.), the method of accumulation can greatly affect the accuracy of the result. Below, we discuss various strategies:&lt;br /&gt;
&lt;br /&gt;
=== 3.1. Naive Linear Accumulation ===&lt;br /&gt;
&lt;br /&gt;
The simplest accumulation strategy is to use a single accumulator and add each element sequentially. In C-like pseudocode:&lt;br /&gt;
&lt;br /&gt;
This &#039;&#039;&#039;naive linear summation&#039;&#039;&#039; adds values one by one to the running total. While straightforward and fast, it is prone to the rounding error accumulation described earlier. The partial sum &amp;lt;code&amp;gt;sum&amp;lt;/code&amp;gt; continually grows (in absolute value) as terms are added, so new small terms may fall outside its precision. For example, if &amp;lt;code&amp;gt;sum&amp;lt;/code&amp;gt; is large and positive, adding a comparatively tiny positive number may produce no change because the small term&#039;s significant digits vanish in the presence of the large sum (the increment is below the rounding threshold of &amp;lt;code&amp;gt;sum&amp;lt;/code&amp;gt;). Conversely, adding a small number of opposite sign can suffer from cancellation. The order of addition matters: summing from smallest magnitude to largest (ascending order) tends to yield a more accurate result than summing in descending order, because adding small terms first preserves their contributions before the sum becomes very large. The naive loop above implicitly sums in the given index order; if the data is unsorted, this order is essentially arbitrary.&lt;br /&gt;
&lt;br /&gt;
The performance of linear accumulation is excellent (minimal overhead), and for many applications the resulting error is within acceptable limits. However, for &#039;&#039;&#039;very large N&#039;&#039;&#039; or ill-conditioned sums, the inaccuracy can become a problem. In a reduction involving millions of elements, a single-precision sum could lose several digits of accuracy. In deep learning, this could translate to small discrepancies in layer outputs or aggregated gradients if done naively in low precision.&lt;br /&gt;
&lt;br /&gt;
=== 3.2. Pairwise and Tree-Based Reductions ===&lt;br /&gt;
&lt;br /&gt;
A better approach to summation is &#039;&#039;&#039;pairwise summation&#039;&#039;&#039;, which sums numbers in a binary tree structure rather than linearly. The idea is to recursively split the array into halves, sum each half, then add the two partial sums. This can be implemented as:&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
By always adding numbers of similar magnitudes first (small subsets are reduced to subtotals, then those are added, etc.), pairwise summation avoids the situation of a very large running total absorbing small additions until the final steps. The error of pairwise summation grows logarithmically with $N$ in the worst case (roughly $O(\varepsilon \log_2 N)$)&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;, instead of linearly. Intuitively, at each level of the tree, rounding error is constrained, and there are about $\log_2 N$ levels. In fact, the worst-case relative error bound for pairwise summation (base case of 1 element) is:&lt;br /&gt;
&lt;br /&gt;
\[ |E_N| \le \frac{\varepsilon \log_2 N}{1 - \varepsilon \log_2 N} \sum_{i=1}^N |x_i|, \]&lt;br /&gt;
&lt;br /&gt;
which for practical $N$ simplifies to on the order of $\varepsilon \log_2 N$ times the condition number of the sum&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;. In typical random-error scenarios, the error behaves even better (on the order of $\sqrt{\log N}$)&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;. The key is that &#039;&#039;&#039;pairwise (tree) reduction balances the accumulations&#039;&#039;&#039;, preventing one sum from overwhelming the others too early.&lt;br /&gt;
&lt;br /&gt;
From a performance standpoint, pairwise summation has the same $O(N)$ total additions as linear summation, just a different order. It is also highly amenable to parallelization: independent partial sums can be computed in parallel at each level of the tree. Many numerical libraries and BLAS implementations use a form of blocked or pairwise summation in dot products for improved accuracy at negligible performance cost. For example, NumPy and Julia perform partial pairwise summation by default for better numerical behavior&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=== 3.3. Compensated Summation (Kahan, Neumaier) ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Compensated summation&#039;&#039;&#039; is an alternative technique to improve summation accuracy by tracking the small “lost” quantities. The most famous algorithm is the &#039;&#039;&#039;Kahan summation algorithm&#039;&#039;&#039; (also called Kahan–Babuška)&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. Kahan summation uses an extra variable to accumulate the round-off error. In pseudocode:&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Here &amp;lt;code&amp;gt;c&amp;lt;/code&amp;gt; accumulates the tiny leftovers that would otherwise be dropped&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. Each iteration essentially subtracts the last error from the new addend (&amp;lt;code&amp;gt;y = x[i] - c&amp;lt;/code&amp;gt;) so that &amp;lt;code&amp;gt;t = sum + y&amp;lt;/code&amp;gt; includes the small bits from the previous operation. The new error is then stored in &amp;lt;code&amp;gt;c&amp;lt;/code&amp;gt; for the next iteration. The result is that the final &amp;lt;code&amp;gt;sum&amp;lt;/code&amp;gt; is as if computed with higher precision than the machine’s: Kahan’s algorithm can &#039;&#039;&#039;greatly reduce the error&#039;&#039;&#039; in summing a sequence of floats. In fact, with exact arithmetic for the compensation, Kahan summation yields a result accurate to within 1 or 2 ULP of the true sum, regardless of N (assuming the sequence of partial sums doesn’t overflow)&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. More precisely, Kahan summation achieves a &#039;&#039;&#039;worst-case error independent of N&#039;&#039;&#039; (error bound depends only on machine precision, not the number of terms)&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. In practice, using a double accumulator for summing single-precision numbers via Kahan can give near double-precision accuracy.&lt;br /&gt;
&lt;br /&gt;
An important subtlety is that the compensation variable &amp;lt;code&amp;gt;c&amp;lt;/code&amp;gt; should be kept in &#039;&#039;full precision&#039;&#039;. In the implementation above, we used &amp;lt;code&amp;gt;double&amp;lt;/code&amp;gt; for both sum and c. If summing FP32 values, one might use &amp;lt;code&amp;gt;float sum&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;float c&amp;lt;/code&amp;gt; (both 32-bit), which still improves accuracy, but using a higher precision for the accumulator and compensation (i.e. double) yields even better results. Many implementations of Kahan summation use the same type for sum and c, but it&#039;s assumed to be higher precision than the inputs when possible.&lt;br /&gt;
&lt;br /&gt;
Kahan’s algorithm carries a performance cost: each iteration does five arithmetic operations (two adds, one subtract, etc.) and has a data dependency, which can hinder parallelism or pipelining&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. Thus, it runs slower than naive summation (roughly 2–3× slower or more, depending on CPU pipeline). In loops where summation is a bottleneck, this may be significant. Nonetheless, for modest N or where maximum accuracy is required, compensated summation is extremely useful. In fact, Python’s &amp;lt;code&amp;gt;math.fsum&amp;lt;/code&amp;gt; function uses a variant of Kahan’s algorithm (actually a more advanced multi-part compensation) to sum iterables with high precision.&lt;br /&gt;
&lt;br /&gt;
There are variations and improvements on basic Kahan summation. &#039;&#039;&#039;Neumaier’s algorithm&#039;&#039;&#039; (sometimes called &#039;&#039;Kahan-Babuška-Neumaier&#039;&#039;) improves Kahan’s method by also handling cases where the next input is larger in magnitude than the running sum&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. In such cases, the original Kahan formula could suffer error; Neumaier’s version effectively swaps roles when a huge value comes in. Its pseudocode is:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight&amp;gt;&lt;br /&gt;
function NeumaierSum(array):&lt;br /&gt;
    sum = 0.0&lt;br /&gt;
    compensation = 0.0&lt;br /&gt;
    for each x in array:&lt;br /&gt;
        t = sum + x&lt;br /&gt;
        if (fabs(sum) &amp;gt;= fabs(x))&lt;br /&gt;
            compensation += (sum - t) + x;&lt;br /&gt;
        else&lt;br /&gt;
            compensation += (x - t) + sum;&lt;br /&gt;
        sum = t;&lt;br /&gt;
    return sum + compensation;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This algorithm ensures the compensation covers both scenarios: when the sum is the bigger addend or when the new term is bigger&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. In the end, it adds the final &amp;lt;code&amp;gt;compensation&amp;lt;/code&amp;gt; once to the result. Neumaier’s method can produce the correct result in cases where basic Kahan fails. For instance, summing [1.0, $+10^{100}$, 1.0, $-10^{100}$] in double precision: regular Kahan summation might give 0.0 (losing the small terms), whereas Neumaier’s algorithm yields 2.0 exactly&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. There are also higher-order compensation methods (carrying multiple compensation terms, e.g., the &#039;&#039;&#039;Klein summation&#039;&#039;&#039; variant&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt; or using arbitrary precision for intermediate sums) that further reduce error at increased computational cost&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
In summary, compensated summation algorithms trade extra computation for greatly improved numerical accuracy. They are especially valuable when summing large arrays of numbers with varying magnitudes, where naive summation could lose most of the low-order bits. Many scientific computing environments provide compensated sum routines for this reason. In deep learning, however, a different approach to improved precision is often taken: &#039;&#039;&#039;using a wider accumulator type&#039;&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
=== 3.4. Mixed-Precision Accumulation ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Mixed-precision accumulation&#039;&#039;&#039; refers to performing arithmetic in a higher precision than the input data. A common scenario is accumulating sums in 32-bit floats when the operands are 16-bit floats. Rather than implementing a compensation algorithm, one can simply use a wider accumulator so that intermediate rounding error is smaller. This is effectively what modern GPUs do for tensor operations: for example, NVIDIA &#039;&#039;&#039;Tensor Cores operate on FP16 input data with an FP32 accumulator&#039;&#039;&#039; (the 16-bit products are accumulated in 32-bit)&amp;lt;ref name=&amp;quot;ref6&amp;quot;/&amp;gt;. In this way, summing 2048 products in a dot product has far less precision loss than it would with a 16-bit accumulator. Hardware that supports mixed precision can give the best of both worlds: fast low-precision multiplies and high-precision accumulation.&lt;br /&gt;
&lt;br /&gt;
Software can also leverage mixed precision. For instance, one can sum an array of &amp;lt;code&amp;gt;float&amp;lt;/code&amp;gt; values into a &amp;lt;code&amp;gt;double&amp;lt;/code&amp;gt; variable, accumulating the result in double precision and then converting back to float at the end:&lt;br /&gt;
&lt;br /&gt;
This approach often yields a more accurate &amp;lt;code&amp;gt;result&amp;lt;/code&amp;gt; than summing in single precision directly, because the intermediate sum carries 52-bit precision. The final rounding to float happens only once at the end (minimizing overall rounding error). In fact, the above is one way to implement &amp;lt;code&amp;gt;math.fsum&amp;lt;/code&amp;gt; in Python (it internally does something similar with multiple partial sums in double precision).&lt;br /&gt;
&lt;br /&gt;
In machine learning training and inference, &#039;&#039;&#039;mixed precision&#039;&#039;&#039; is widely used. When using FP16 for speed, it&#039;s common to accumulate with FP32. For example, &#039;&#039;&#039;Google&#039;s TPUs use BFloat16 for multiplication but accumulate in FP32&#039;&#039;&#039; for matrix ops&amp;lt;ref name=&amp;quot;ref6&amp;quot;/&amp;gt;. AWS’s Inferentia (Neuron) hardware similarly supports FP16/BF16 matrix multiplication with FP32 accumulation&amp;lt;ref name=&amp;quot;ref7&amp;quot;/&amp;gt;. The motivation is that summing hundreds or thousands of half-precision values would otherwise introduce intolerable error or risk overflow. With an FP32 accumulator, the reduction error is drastically reduced (by about $2^{13}$ times, since FP32 has 13 more precision bits than FP16). Mixed-precision accumulation thus provides a simple and effective form of compensation: &#039;&#039;&#039;use a larger bucket to collect many small contributions&#039;&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
There is a spectrum of options: summing int8 or int16 integers might use 32-bit integers as accumulators for the same reason (to avoid overflow). Some BLAS libraries even offer &#039;&#039;extended precision accumulators&#039;&#039; for single precision dot products (accumulating in double). The downside of mixed precision is the extra memory and maybe throughput cost of the wider type. But on many platforms, the add latency is dominated by hardware design rather than bit-width, so using a 32-bit adder vs 16-bit has negligible speed impact if supported natively. The key is whether data movement or register pressure increases.&lt;br /&gt;
&lt;br /&gt;
In summary, &#039;&#039;&#039;using a higher precision accumulator is often the easiest way to improve numerical accuracy&#039;&#039;&#039; for reductions. It is a primary strategy in deep learning hardware to maintain stability while exploiting low precision for storage and initial computations.&lt;br /&gt;
&lt;br /&gt;
== Precision Formats in AI and Machine Learning ==&lt;br /&gt;
&lt;br /&gt;
Modern AI systems use a variety of numeric formats to balance speed, memory, and accuracy. We discuss common precisions and their roles:&lt;br /&gt;
&lt;br /&gt;
=== 4.1. FP32 and FP64 in Training and Inference ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;FP32 (single precision)&#039;&#039;&#039; has been the workhorse of deep learning for years. Most training scripts in frameworks like TensorFlow and PyTorch used FP32 by default, as it offers sufficient precision to propagate gradients and accumulate weight updates over millions of iterations without excessive error. With ~7 decimal digits of precision and a wide exponent range, FP32 can handle the dynamic range of activations and gradients in typical neural networks. In inference, FP32 is often more precision than strictly necessary for final predictions, but it has remained common because of its ease and the fact that many GPU operations were optimized for FP32.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;FP64 (double precision)&#039;&#039;&#039; is rarely used in deep learning model training or inference, except in specific scenarios requiring extreme precision. FP64 is common in scientific computing and certain types of numerical simulations. In neural networks, however, the extra precision is usually not needed for model accuracy—using doubles tends to yield negligible improvements in model metrics but at a large performance cost (since on GPUs FP64 throughput can be 1/2, 1/8, or even 1/32 of FP32 throughput depending on hardware). One exception might be when integrating neural nets into larger physics simulations or when solving ill-conditioned problems as part of the model. Some researchers use FP64 to diagnose numerical issues or ensure reproducibility. Generally though, deep learning has favored lower precision for efficiency, and FP64 is considered overkill for most networks&amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
That said, certain classical machine learning algorithms benefit from double precision. For example, computing the inverse of a matrix (as in a Gaussian Process regression or Kalman filter embedded in a model) can incur large rounding errors in single precision, so using FP64 can significantly improve the result&amp;lt;ref name=&amp;quot;ref1&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref1&amp;quot;/&amp;gt;. The ONNX standard supports a float64 tensor type for such needs. In ONNX Runtime, however, emphasis has been on FP32 performance, and some pipelines default to float even if the original model was double&amp;lt;ref name=&amp;quot;ref1&amp;quot;/&amp;gt;. If a model’s prediction computation involves operations that are numerically sensitive (like matrix inversion, Cholesky decomposition, or summing extremely large counts), then FP64 may be required to avoid noticeable discrepancies&amp;lt;ref name=&amp;quot;ref1&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref1&amp;quot;/&amp;gt;. A good practice is to test a model in both float and double if you suspect precision issues; significant differences indicate an ill-conditioned calculation that might need higher precision.&lt;br /&gt;
&lt;br /&gt;
In summary, FP32 remains the &#039;&#039;&#039;default&#039;&#039;&#039; format for most training and inference due to its balance of accuracy and efficiency&amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;, while FP64 is reserved for niche cases or precision-critical subroutines. FP32 typically achieves the same accuracy as FP64 for end-to-end deep learning tasks, because network training can often compensate for small noise, and other sources of error (like generalization error) dominate. Therefore, the community trend has been moving to &#039;&#039;&#039;lower&#039;&#039;&#039; precisions, not higher, to gain speed — provided the models still converge and perform well.&lt;br /&gt;
&lt;br /&gt;
=== 4.2. FP16 and BFloat16 in Deep Learning ===&lt;br /&gt;
&lt;br /&gt;
To further speed up neural network training and inference, &#039;&#039;&#039;half-precision (FP16)&#039;&#039;&#039; was adopted, especially with the advent of GPUs supporting FP16 math at twice the rate of FP32. Using FP16 reduces memory storage and bandwidth by 50% compared to FP32, which is significant for large models. However, FP16’s small exponent range and precision posed challenges: training a large network purely in FP16 can fail due to gradient underflow/overflow or weight updates not registering (because changes are below the 0.001 quantization step). To address this, frameworks introduced &#039;&#039;&#039;mixed-precision training&#039;&#039;&#039;, where compute-intensive parts (matrix multiplies, convolutions) run in FP16, but certain accumulations and model parameters remain in FP32. For instance, NVIDIA’s &#039;&#039;&#039;Tensor Core&#039;&#039;&#039; units require FP16 (or BF16) inputs but accumulate results in FP32, effectively ensuring that reductions are done in higher precision&amp;lt;ref name=&amp;quot;ref6&amp;quot;/&amp;gt;. Research showed that with proper loss scaling and mixed precision, models can train to FP32-level accuracy using mostly FP16 arithmetic, achieving significant speedups.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;BFloat16 (BF16)&#039;&#039;&#039; emerged as an alternative 16-bit format primarily from Google’s TPU training systems. BF16 has a larger exponent (8 bits, same as FP32) but only 7 fraction bits&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. This means BF16 can handle the &#039;&#039;&#039;same dynamic range&#039;&#039;&#039; as FP32 (no overflows where FP32 wouldn’t) but at the cost of even lower precision than FP16 (BF16 has about 2–3 decimal digits of precision vs FP16’s ~3–4). Despite its coarse precision, BF16 turns out to be effective for training deep networks because the algorithms (stochastic gradient descent, etc.) are somewhat robust to that noise, and having the wide range prevents catastrophic loss of signal (no gradient will overflow to Inf unless it was going to in FP32 anyway). Many training frameworks (PyTorch, TensorFlow) support BF16 training on hardware like &#039;&#039;&#039;TPU, NVIDIA A100+, Intel CPUs with AVX-512 BF16, etc&#039;&#039;&#039;. Typically, when using BF16, &#039;&#039;&#039;accumulations are still done in FP32&#039;&#039;&#039; (for example, a TPU v2/v3 multiplies BF16 matrices and accumulates into an FP32 result for the dot product&amp;lt;ref name=&amp;quot;ref6&amp;quot;/&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
In inference, both FP16 and BF16 are used to deploy models for faster speed or lower memory. FP16 is popular on NVIDIA GPUs (which have specialized FP16 units). BF16 is advantageous on TPUs and some CPUs that support it, and increasingly on GPUs (Ampere and newer GPUs also support BF16 arithmetic). The choice between FP16 and BF16 can depend on hardware: BF16’s advantage is easier software conversion (you can often take an FP32-trained model and cast weights to BF16 with minimal loss, since the dynamic range is intact), whereas FP16 might require adjusting the model or ensuring values fit in range (sometimes needing techniques like maintaining a master copy of weights in FP32 during training, or handling out-of-range values).&lt;br /&gt;
&lt;br /&gt;
For ONNX, both Float16 and BFloat16 types are part of the standard. ONNX models can be converted to FP16 (truncating weights and inserting cast nodes for computations) using tools like &amp;lt;code&amp;gt;float16_converter&amp;lt;/code&amp;gt; in ONNX Runtime&amp;lt;ref name=&amp;quot;ref8&amp;quot;/&amp;gt;. ONNX Runtime on GPUs will typically execute those FP16 ops with mixed precision on tensor cores (FP16 input, FP32 accumulate), whereas on CPU it may emulate FP16 or upcast to FP32 (as many CPU implementations do not natively support half-precision math)&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;. In fact, as an example, running an ONNX model in FP16 on a CPU might insert hidden Cast operations to FP32 behind the scenes, because the CPU kernel for that op only exists in FP32&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;. This ensures correctness (the computation is done in higher precision) but can reduce performance. We will revisit such backend-dependent behavior in Section 5.3.&lt;br /&gt;
&lt;br /&gt;
In summary, &#039;&#039;&#039;FP16 and BF16&#039;&#039;&#039; are now established as important low-precision formats in deep learning. FP16 offers more precision bits, which can matter for e.g. small differences, but has a limited range (care needed to avoid overflow). BF16 offers safety in range but less precision (which can slightly affect convergence or final accuracy in some cases). Both typically rely on FP32 for accumulation to achieve acceptable accuracy. When deploying models, one should choose a format supported efficiently by the target hardware and verify that the accuracy is not significantly degraded. Often, CNNs and transformers can run in FP16/BF16 with virtually no drop in score, but certain operations (like softmax, see Section 6.3) might need special handling.&lt;br /&gt;
&lt;br /&gt;
=== 4.3. FP8 Formats (E4M3, E5M2) and Scaling ===&lt;br /&gt;
&lt;br /&gt;
The push for ever lower precision has led to experimental &#039;&#039;&#039;8-bit floating point (FP8)&#039;&#039;&#039; formats for deep learning. In 2022, researchers from NVIDIA, Intel, and Arm proposed an FP8 standard with two variants: &#039;&#039;&#039;E4M3&#039;&#039;&#039; (4 exponent bits, 3 fraction bits) and &#039;&#039;&#039;E5M2&#039;&#039;&#039; (5 exponent bits, 2 fraction bits)&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;. Both have 1 sign bit, for a total of 8 bits. E5M2 has a wider exponent range (5 bits gives bias 15) but one fewer mantissa bit than E4M3. The idea is to use E4M3 for numbers that need more precision (like weights or activations around 1.0) and E5M2 for numbers that need more dynamic range (like gradient updates which can be very small or large). In fact, the study found that using E4M3 for forward activations/weights and E5M2 for backward gradients can allow training of large networks with FP8 to accuracy on par with FP16 training&amp;lt;ref name=&amp;quot;ref11&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
FP8 formats are &#039;&#039;not&#039;&#039; IEEE 754 standard (though IEEE is considering lower precision in its 2019 revision). Notably, the E4M3 format as used by NVIDIA does not allocate any bit-pattern for $+\infty$ or $-\infty$ – it has only NaN for invalid results, using that freed bit pattern to extend the representable range by one exponent value&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;. Essentially, E4M3 can represent numbers up to ~$2^{15}$ (around 3e4) instead of capping at infinity at $2^{16}$, by foregoing infinities&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;. Variations also exist regarding negative zero and how NaNs are handled&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;. For instance, GraphCore’s IPU uses FP8 formats that disallow negative zero and infinities, to maximize useable range&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Using 8-bit floats for neural networks requires additional techniques, chiefly &#039;&#039;&#039;scaling&#039;&#039;&#039;. Because 8-bit has very limited precision and range, one typically applies a power-of-two scale factor to groups of values to normalize them into an FP8-friendly range. This is akin to block floating-point or “microscaling” per layer or per tensor. For example, one might keep a FP32 scale for each channel or tensor, and represent values as $(FP8\_value) \times 2^{scale}$. The ONNX standard has introduced support for FP8 in version 1.15, including types for E4M3 and E5M2 (with variations FNUZ: finite-only and no negative zero)&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;, and even a type called &#039;&#039;&#039;E8M0&#039;&#039;&#039; which is an 8-bit exponent-only number used purely as a scale factor&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;. The E8M0 type has 8 exponent bits, no mantissa, effectively representing powers of two from $2^{-127}$ to $2^{128}$ for scaling purposes&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;. This indicates an approach where an FP8 tensor would be accompanied by an E8M0 scale (or scales) to interpret its value correctly.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Rounding&#039;&#039;&#039; becomes critical when casting values to FP8. The quantization error is much larger relative to FP16 or FP32. Studies have shown that choosing the right rounding mode (e.g., round-to-nearest, stochastic rounding, etc.) and clamping behavior (saturating to max instead of inf) can affect model accuracy&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;. ONNX added a &amp;lt;code&amp;gt;round_mode&amp;lt;/code&amp;gt; attribute to its Cast operator to allow controlling this for FP8&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;. In particular, one paper found that &#039;&#039;&#039;rounding up with saturation&#039;&#039;&#039; (round-to-nearest ties away from 0, and clamp overflows to max finite rather than inf) gave better accuracy in large language model training than other rounding modes&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
At present, FP8 is on the cutting edge. NVIDIA’s H100 GPU supports FP8 tensor cores (for both E4M3 and E5M2), and early results show minimal accuracy loss on some vision and NLP tasks when properly tuned. However, using FP8 requires fine-tuning hyperparameters like scaling factors for each layer and perhaps tweaking training procedures. ONNX’s inclusion of FP8 types means you can represent and transport models using FP8 weights or activations, but actual hardware support is limited and typically paired with calibration or dynamic scaling logic outside of ONNX graph (or using custom ONNX functions for scaling). It remains an &#039;&#039;&#039;open research area&#039;&#039;&#039; how to fully automate FP8 quantization for arbitrary models without accuracy loss.&lt;br /&gt;
&lt;br /&gt;
In summary, FP8 formats promise further compression of neural network computation and data. They are an order of magnitude more error-prone than FP16, but when combined with careful scaling, mixed usage of E4M3/E5M2, and maybe modified training techniques, they have shown surprising success. ONNX is actively evolving to support these ultra-low precision types, signaling their potential importance in future AI inference deployments.&lt;br /&gt;
&lt;br /&gt;
== Accumulation Semantics in ONNX ==&lt;br /&gt;
&lt;br /&gt;
One of the roles of the ONNX specification is to define the mathematical behavior of operators (Conv, MatMul, Sum, etc.) in a framework-agnostic way. However, ONNX mostly defines &#039;&#039;what&#039;&#039; an operator computes in exact arithmetic, not &#039;&#039;how&#039;&#039; the computation is carried out in finite precision. This leaves certain details – like accumulator precision or summation order – up to the implementation (runtime or hardware). We&#039;ll examine how ONNX treats accumulation and what guarantees (or lack thereof) are provided.&lt;br /&gt;
&lt;br /&gt;
=== 5.1. Operator Definitions and Numerical Guarantees ===&lt;br /&gt;
&lt;br /&gt;
Each ONNX operator is described as performing an ideal mathematical function on its inputs. For example, &amp;lt;code&amp;gt;MatMul&amp;lt;/code&amp;gt; is defined as the standard matrix product of two tensors, and &amp;lt;code&amp;gt;ReduceSum&amp;lt;/code&amp;gt; computes a summation along given axes. The ONNX spec, as of current versions, &#039;&#039;&#039;does not mandate the precision of intermediate calculations&#039;&#039;&#039; beyond the types of the inputs and outputs. If an operator’s inputs and outputs are float16, the spec implies the computation is done in float16 in a sense, but it does not forbid an implementation from using higher precision internally – it simply doesn&#039;t mention it. There is &#039;&#039;&#039;no explicit guarantee of identical bit-for-bit results&#039;&#039;&#039; across different ONNX backends for floating-point ops. Instead, results are expected to be numerically close, within tolerances one would normally expect from floating-point differences (due to different hardware or algorithms). The ONNX Backend Test suite typically uses a relative or absolute error tolerance when validating outputs for this reason.&lt;br /&gt;
&lt;br /&gt;
Because of this, important details like the use of extended precision or accumulation in higher precision are left ambiguous. This has led to discussions in the ONNX community about making such behavior more explicit. For example, a proposal was raised to allow specification of accumulation precision for certain ops like MatMul&amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;. The reasoning is that as lower-precision types (float16, bfloat16, float8) become more common, knowing or controlling whether an op internally accumulates in a higher precision is important for both accuracy and reproducibility&amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;. In June 2025, an issue on the ONNX GitHub suggested adding an attribute to MatMul (and similarly Gemm, Softmax, etc.) to specify the accumulator type&amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;. By default it would use input precision (status quo), but an attribute could request (for instance) 32-bit accumulation even if inputs are 16-bit&amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;. As of this writing, however, no such attribute exists in the official spec – it&#039;s an open issue. Thus, the numerical semantics are effectively: &#039;&#039;“compute the result as if in the same type as inputs, but actual implementations may use more precision internally as an optimization.”&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
From a guarantee standpoint, ONNX operators promise the mathematically correct result assuming real-number arithmetic, but they &#039;&#039;&#039;do not guarantee bit-exact results&#039;&#039;&#039; due to floating-point effects. There is an understanding that differences can arise from different summation orders, fused operations (e.g., an implementation might use an FMA (fused multiply-add) which has one rounding instead of two), or use of extended precision registers. ONNX does not formally define rounding modes either – it relies on IEEE 754 defaults, but if a backend used something like deterministic summation or alternative rounding, ONNX has no mechanism to express that.&lt;br /&gt;
&lt;br /&gt;
In summary, ONNX&#039;s current stance is largely &#039;&#039;&#039;implementation-dependent&#039;&#039;&#039; for numerical precision. It provides minimal explicit numerical guarantees beyond type constraints. This is why an ONNX model run on two different hardware or runtime libraries can have slightly different results, especially in reduced precision. The burden is on backends to produce results &amp;quot;close enough&amp;quot; to each other that they don&#039;t affect the model&#039;s decisions in any significant way.&lt;br /&gt;
&lt;br /&gt;
=== 5.2. MatMul, Conv, and Reduction Operators ===&lt;br /&gt;
&lt;br /&gt;
Consider ONNX &amp;lt;code&amp;gt;MatMul&amp;lt;/code&amp;gt;: it takes two tensors (often two matrices) and produces their matrix product. In pure math, $C_{ij} = \sum_k A_{ik} &#039;&#039; B_{kj}$. If A and B are float16, should that sum be computed in float16 or float32? As discussed, ONNX doesn’t specify – it simply says the output is a float16 matrix containing the product. In practice, many backends will &#039;&#039;promote&#039;&#039; the computation: for example, NVIDIA GPUs will do the multiplication and addition in a mixed FP16/FP32 mode by default (because of tensor cores), then convert the final result back to float16. ONNX Runtime on CPU might not support float16 multiplication natively, so it will insert cast ops to float32, perform the MatMul in float32, then cast back to float16&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;. From ONNX’s perspective, as long as the final result is within acceptable error of the “ideal” float16 computation, it’s fine. But note, an “ideal” float16 computation (where every multiply and add is rounded to FP16) might actually be &#039;&#039;less accurate* than what the backend did with FP32. This means that a backend using higher precision can produce a result that is not bit-equal to a pure FP16 implementation – it&#039;s actually closer to the true real number result. This is usually considered a good thing (improved accuracy), but it does highlight a lack of consistency: another backend might do everything in pure FP16 and get a slightly different answer.&lt;br /&gt;
&lt;br /&gt;
The same situation occurs for &#039;&#039;&#039;convolution&#039;&#039;&#039; (&amp;lt;code&amp;gt;Conv&amp;lt;/code&amp;gt; op) which is essentially a batched sum of products (like MatMul in sliding window), and for explicit reduction ops like &amp;lt;code&amp;gt;ReduceSum&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;ReduceMean&amp;lt;/code&amp;gt;, etc. If you have an ONNX &amp;lt;code&amp;gt;ReduceSum&amp;lt;/code&amp;gt; that sums 1000 float16 values, the spec doesn’t say how to sum them. A GPU kernel might sum in a tree using FP32 accumulators; a naive implementation might sum in order with FP16 and possibly overflow or underflow intermediate steps. Both are allowed as long as they output a float16 result that is “reasonable.” In practice, ONNX Runtime and others take care to avoid obviously bad behavior (like overflow) by using higher precision or by splitting the sum. But again, it’s not mandated by the ONNX standard explicitly.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Softmax&#039;&#039;&#039; is an interesting case: Softmax involves an internal sum of exponentials. ONNX defines the Softmax operator as $$\mathrm{Softmax}(x)_i = \frac{\exp(x_i)}{\sum_j \exp(x_j)},$$ typically applied along a specified axis. Directly computing this in float16 can be problematic for large vectors, as discussed in Section 6.3. Indeed, the issue of accumulation precision arises: should the sum $\sum_j \exp(x_j)$ be done in FP16 or can it be in FP32? Many implementations will do it in FP32 even if $x$ is FP16, because the dynamic range of $\exp(x)$ is large. The ONNX Softmax spec doesn’t detail this, but it expects the result to be a proper probability distribution (summing to 1.0 within normal floating error). A pure FP16 softmax on, say, 1000 elements could sum to something like 0.996 or 1.004 due to rounding, whereas FP32 accumulation would be much closer to 1.000. ONNX likely allows both; the small difference is usually inconsequential, but if it were large (say the FP16 sum overflowed to inf), that would be a correctness issue. A robust backend will prevent that (by using the common max-subtraction trick and higher precision accumulation). The ONNX issue 7072 indeed calls out Softmax as another op where explicit control of accumulation precision would be useful&amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
In summary, &#039;&#039;&#039;MatMul, Conv, and reduction ops&#039;&#039;&#039; in ONNX are defined mathematically but with &#039;&#039;implicit&#039;&#039; numerical behavior. Implementations commonly use higher precision for intermediate sums especially when input is low precision, but this is not standardized. Consequently, results can differ slightly between implementations. For critical applications, one must be aware of these differences. A model exported to ONNX with float16 weights might perform slightly differently on one runtime vs another if one does all float16 math and the other uses float32 accumulations. Both conform to ONNX. As a user, if you require a certain precision for accumulation, currently you have to ensure it by the way you export or run the model (for example, insert manual cast nodes in the ONNX graph to force certain ops to FP32). We may see future ONNX versions address this with attributes or metadata as mentioned, but for now these ops are effectively “compute with best effort accuracy”.&lt;br /&gt;
&lt;br /&gt;
=== 5.3. Backend and Hardware-Dependent Behavior ===&lt;br /&gt;
&lt;br /&gt;
Because ONNX is implemented by many backends (ONNX Runtime, TensorRT, CoreML, PyTorch JIT, etc.), and these in turn run on varied hardware (CPU, GPU, accelerators), the numeric behavior can be backend-dependent. Some examples:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;CPU backends:&#039;&#039;&#039; Many CPUs do not have native float16 vector arithmetic (with a few exceptions in recent CPUs). ONNX Runtime’s default CPU execution provider will often upcast float16 to float32, compute, then downcast&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;. This ensures accuracy but means that if you thought you were getting a full float16 pipeline, you’re not – the CPU is essentially ignoring the low precision in compute. On the other hand, x86 CPUs have extended 80-bit precision in x87 registers (though less used now) and 64-bit SIMD registers for doubles; some compiler optimizations might carry intermediate results in higher precision. This could make CPU results slightly different from a GPU’s if, say, a sum is done in 80-bit on CPU versus 32-bit on GPU. Generally, modern compilers avoid such extended precision unless explicitly asked, to improve reproducibility.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;GPU backends:&#039;&#039;&#039; GPUs often perform summations in parallel using tree reductions. The order of operations can change depending on how threads are scheduled. This means even on the same GPU, summing an array in one launch versus another might not give bit-identical results if the internal parallel reduction algorithm differs (though it should be deterministically the same given the same library version and input, but if you change GPU model or BLAS library, it might differ). Also, GPUs have fused multiply-add (FMA) instructions that do $(a*b + c)$ in one step with one rounding, which can produce slightly different results than doing the multiply and add separately (which would round twice). If an ONNX backend uses a fused kernel (like a GEMM library) it might yield different low-order bits than a simple loop.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Vendor libraries:&#039;&#039;&#039; ONNX Runtime can use vendor BLAS or DNN libraries (MKL, cuDNN, TensorRT, etc.). These libraries often implement their own strategies for numerical stability. For example, NVIDIA’s cuDNN might use FP32 accumulation for FP16 inputs by default. TensorRT (NVIDIA’s inference engine) will sometimes automatically promote precisions or insert scaling to maintain stability&amp;lt;ref name=&amp;quot;ref13&amp;quot;/&amp;gt;. On the other hand, if you explicitly request FP16 in TensorRT with no FP32 fallback, it might do the entire op in FP16 and you’d see more error. Some libraries offer knobs: TensorRT allows choosing FP32, FP16, or INT8 precision for each layer, and will use FP32 internally for safety in some cases unless forced otherwise.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Hardware-specific quirks:&#039;&#039;&#039; As another example, NVIDIA’s &#039;&#039;&#039;TensorFloat-32 (TF32)&#039;&#039;&#039; on Ampere GPUs is a precision mode where matrix multiplications use 10-bit mantissa (so about FP16 precision for the multiply) but keep 8-bit exponent (range like FP32). By default, CUDA libraries use TF32 for FP32 matrix multiplies on Ampere to speed them up, unless you disable it. This means if you run an ONNX model with FP32 on an A100 GPU, your matmuls might actually be lower precision than FP32 (though higher than FP16)&amp;lt;ref name=&amp;quot;ref6&amp;quot;/&amp;gt;. It’s an example of hardware choosing an intermediate precision for performance. From ONNX’s perspective, the output is still FP32 and (hopefully) within error tolerances, but a user might be surprised that it’s not a “true” FP32 computation internally. If exactness is needed, one has to turn off TF32.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Non-determinism:&#039;&#039;&#039; Some backends (especially on GPUs) allow non-deterministic execution for performance. For instance, atomic adds in parallel reductions can produce results that vary run-to-run because the order of accumulation is race-condition-dependent. ONNX’s spec doesn’t cover this explicitly, but it’s something to consider. In ONNX Runtime, there are settings for deterministic computing, but by default it may use all available performance optimizations, even if results vary at the 1e-7 level.&lt;br /&gt;
&lt;br /&gt;
In short, the numerical behavior under ONNX can depend on the combination of backend software and hardware capabilities. The lack of a strict spec for accumulation means implementations have freedom to use clever strategies (higher precision, reordering) to improve accuracy or speed. Usually this is positive (e.g. getting better accuracy than a naive approach). But it could also lead to slight inconsistencies. For example, you might find that exporting a PyTorch model to ONNX and running it with one engine yields outputs slightly different from PyTorch’s – not because ONNX is wrong, but because the summation or convolution was done differently (maybe PyTorch summed in FP32 as well, but maybe not in exactly the same way).&lt;br /&gt;
&lt;br /&gt;
The key takeaway is that ONNX &#039;&#039;by design&#039;&#039; abstracts away these details, which is good for portability but means you need to &#039;&#039;&#039;trust the backend&#039;&#039;&#039; or verify its precision behavior for sensitive models. If necessary, you can enforce certain behavior by inserting ops (for example, if you want to ensure FP32 accumulation, you could insert a cast to FP32 for inputs of a MatMul and cast the result back to FP16 – this way the ONNX graph itself mandates FP32 compute in between). This of course sacrifices some performance or device-specific magic.&lt;br /&gt;
&lt;br /&gt;
Looking forward, as mentioned, there are proposals to enrich ONNX’s specification regarding precision. This might include explicit flags for “exact summation” or “accumulate in int32” etc., which would make models more self-descriptive but also potentially less portable (if a backend doesn’t support that, what then?). For now, users should be aware of these backend differences. Most of the time, well-engineered ONNX runtimes yield results close enough that they don&#039;t affect the application (e.g., classification top-1 remains the same even if low-level sums differ by 1e-5). But for high-stakes numerical computations, testing and maybe adjusting the model or backend settings is prudent.&lt;br /&gt;
&lt;br /&gt;
== Numerical Stability in Deep Learning Models ==&lt;br /&gt;
&lt;br /&gt;
Deep learning computations involve many operations that, while theoretically well-defined, can be sensitive to numerical issues when implemented in finite precision. We&#039;ll discuss a few common scenarios in neural networks where numerical stability is a concern, and how precision and accumulation play a role.&lt;br /&gt;
&lt;br /&gt;
=== 6.1. Dot Products and Large-Scale Reductions ===&lt;br /&gt;
&lt;br /&gt;
Neural networks are full of dot products: every fully-connected layer computes $\mathbf{y} = W\mathbf{x}$, which is a series of inner products between weight vectors and the input. Convolutional layers compute weighted sums of local regions. Attention mechanisms compute large matrix products of query/key/value matrices. All these boil down to summing a lot of terms: $\sum_{k=1}^K a_k b_k$. When $K$ is large (it could be in the thousands or more for a single neuron in a fully connected layer of a large model), the potential for floating-point error in that sum increases.&lt;br /&gt;
&lt;br /&gt;
If using FP32, the error may still be negligible in many cases, but in lower precision like FP16, it can become significant. For example, consider a dot product of length 2048 in FP16. Each product is in FP16 (with ~3 decimal digits precision) and if accumulated in FP16, the worst-case error could be on the order of $2048 \times \varepsilon_{FP16} \approx 2048 \times 0.0009766 \approx 2$ (i.e., you could lose a couple units in the third decimal place relative accuracy in worst case). Even in random error accumulation, you might see error on the order of $\sqrt{2048} \times 0.0009766 \approx 0.044$ or 4.4% (though this is a rough estimate). That’s quite significant. Fortunately, as discussed, most hardware will accumulate such sums in a higher precision like FP32, dramatically reducing that error (~7 orders of magnitude smaller $\varepsilon$). But if it didn’t, the network’s outputs could get noisy. A fully-connected layer with many inputs might output slightly different results depending on summation order if done in low precision.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Large-scale reductions&#039;&#039;&#039; also occur in operations like global average pooling (summing all elements of a feature map) or computing the total loss by summing loss over batch samples. If one sums 100,000 values in FP32, the worst-case error could be on the order of 100,000 &#039;&#039; 1e-7 ≈ 1e-2 (1% uncertainty), though typical error would be much less. In FP16, summing 100k values is likely impossible without overflow: as noted, adding ~10^4 moderate numbers can overflow FP16&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. So large reductions &#039;&#039;must* be handled carefully (either in chunks or in higher precision). Frameworks generally do: for example, when performing a sum reduction on GPU, libraries break it into parallel blocks that accumulate in registers (usually 32-bit registers, even if inputs are 16-bit). &lt;br /&gt;
&lt;br /&gt;
Another aspect is that many dot products in neural nets involve values of varying magnitude. If the distribution of terms is such that some are much larger than others, the smaller terms might not contribute much if summed last. Ideally, one might sort by magnitude or use pairwise summation to improve that. Most BLAS libraries effectively do a pairwise strategy due to their parallel structure. So, while worst-case analyses are grim, practical implementations mitigate a lot of it.&lt;br /&gt;
&lt;br /&gt;
However, certain layers can suffer if not handled properly. One famous example is in RNNs or LSTMs: if one uses FP16 naive summation for the gating mechanism, errors could accumulate over many time steps and cause drift or instability. In modern practice, recurrent layers are often run in FP16 with FP32 accumulators to avoid this.&lt;br /&gt;
&lt;br /&gt;
In summary, &#039;&#039;&#039;dot products and large reductions need sufficient precision&#039;&#039;&#039; or robust algorithms. If you are implementing a custom kernel (say a custom ONNX op) and you have to sum a huge array, you should consider using a technique from Section 3 (pairwise summation, Kahan, or at least double accumulator if possible). Many deep learning computations, by virtue of hardware and library design, already do something like this under the hood. But it&#039;s always good to be aware, especially if pushing into lower precision territories.&lt;br /&gt;
&lt;br /&gt;
=== 6.2. Cancellation, Dynamic Range, and Conditioning ===&lt;br /&gt;
&lt;br /&gt;
We introduced &#039;&#039;cancellation&#039;&#039; earlier in the context of summation. In deep learning, cancellation can appear in a few places. One is in the computation of certain norms or variances. For instance, Batch Normalization computes mean and variance of activations. The variance formula $\mathrm{Var}(x) = E[x^2] - (E[x])^2$ is subject to catastrophic cancellation if $E[x]$ and $\sqrt{E[x^2]}$ are close in magnitude (which can happen if the variance is small compared to the mean). If implemented naively in single precision, you might get a negative variance due to numerical error (which would then be clamped to zero or sqrt would yield NaN). Robust implementations compute variance in a single pass or increase precision. Indeed, in many frameworks, batchnorm accumulation is done in FP32 even if the activations are FP16, precisely to avoid numerical issues in computing mean and variance. ONNX itself doesn’t specify this, but an ONNX backend is likely to do batchnorm stats in higher precision for safety. The &#039;&#039;conditioning&#039;&#039; of the variance calculation is poor when variance is tiny relative to mean, so it&#039;s a spot where higher precision is needed.&lt;br /&gt;
&lt;br /&gt;
Another example is in certain loss functions or regularizations. If you subtract two large numbers to get a small difference, you lose precision. Say you have two nearly identical predictions and you subtract them as part of some operation – if done in float16, you might get zero whereas float32 would have a slight difference. This could affect, say, a gradient if that difference was supposed to propagate information. Fortunately, neural nets often avoid directly subtracting large nearly equal numbers; but it can happen. In attention mechanisms, the &#039;&#039;score normalization&#039;&#039; (softmax) subtracts a max to avoid huge exponentials, which is good for range but if multiple scores are close to the max, you subtract numbers that are close. However, those subtractions are not catastrophic in the same way; they&#039;re intentional to reduce range and are typically fine in FP32 or even FP16.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Dynamic range&#039;&#039;&#039; issues are everywhere in deep learning. Activation functions like ReLU and exponential can produce very large or very small values. If subsequent computations don’t account for that, you get infinities or zeros. For example, consider a network output that produces a logit of 100 in FP16. $\exp(100)$ in FP16 overflows to infinity (since FP16 max ~ 6e4 and $e^{100} \sim 3.7e43$). If you then do softmax, that one infinity will dominate the sum and you might get a probability of 1 for that class (or NaNs if not handled). The standard softmax implementation subtracts the max logit, so in this case it would subtract 100 from all logits, making the largest exponent $\exp(0)=1$ and others tiny – this avoids overflow. But what about underflow? If you had a very negative logit, $\exp(-100)$ in FP16 would underflow to 0. In softmax, that just contributes ~0 probability, which is fine (it effectively is zero anyway). Underflow tends to be less dangerous than overflow in inference — underflow usually just means “this probability/neuron is effectively 0,” whereas overflow can poison the results with NaNs or Infs. &lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Conditioning&#039;&#039;&#039; refers to how sensitive a function is to input perturbations. Neural networks themselves as functions can be poorly conditioned (e.g., tiny changes in weights cause big changes in output for some architectures), but typically they are designed to be reasonably well-behaved. Certain internal computations like matrix inversion (if one does a pseudoinverse or something for a layer) can be extremely ill-conditioned if the matrix is near singular. That again would need double precision perhaps. In ML, one common ill-conditioned operation is the &#039;&#039;&#039;softmax denominator&#039;&#039;&#039;: $\sum_j e^{x_j}$ can be very large and also very sensitive to changes in the largest $x_j$. However, the softmax is more stable if computed carefully (max trick). Another is &#039;&#039;&#039;normalization&#039;&#039;&#039;: computing $\frac{x - \mu}{\sigma}$ for layer norm or batch norm – if $\sigma$ is very small, then the division blows up noise. In those cases, a small $\epsilon$ is usually added to $\sigma^2$ to prevent instability (this is a model-level fix, not a numeric one per se, but it acknowledges numeric limits).&lt;br /&gt;
&lt;br /&gt;
In training, &#039;&#039;&#039;optimizers&#039;&#039;&#039; can suffer from precision issues. For example, the Adam optimizer keeps a moving average of squared gradients; in half precision, these small updates might flush to zero or not change at some point. Some research found that certain adaptive optimizers break in FP16 because the small differences are lost (&amp;quot;Why FP16 training breaks NAdam&amp;quot; etc.). The solution is often to keep the optimizer states in FP32 while doing the bulk of compute in FP16. This again is a form of mixed precision.&lt;br /&gt;
&lt;br /&gt;
From an inference perspective, once the model is trained, the main concerns are avoiding overflow in activations and ensuring summations (like in softmax or reductions) are done with enough precision. The model’s weights are fixed, so we don&#039;t have to worry about cumulative error over iterations as in training, just the forward pass.&lt;br /&gt;
&lt;br /&gt;
=== 6.3. Normalization Layers and Softmax ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Normalization layers&#039;&#039;&#039; (BatchNorm, LayerNorm, etc.) and &#039;&#039;&#039;Softmax&#039;&#039;&#039; are specifically worth examining for numerical stability:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Batch Normalization:&#039;&#039;&#039; During inference, batchnorm applies an affine transform using precomputed mean and variance (from training). It’s basically: $y = \frac{x - \mu}{\sqrt{\sigma^2 + \epsilon}} \gamma + \beta$. This is a stable operation as long as $\epsilon$ is provided (to avoid division by zero). Since $\mu, \sigma^2$ are constants in inference, it reduces to linear ops which are fine. The main numerical work (computing mean/variance) happened during training or model calibration, and frameworks usually did that in FP32. So batchnorm at inference time is not a source of instability (it’s just a scale and shift of the data).&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Layer Normalization / Group Normalization:&#039;&#039;&#039; These compute mean/var on the fly for each sample (and possibly channel group). In inference, they still need to compute these statistics for each forward pass. If implemented in low precision, the variance calculation can suffer cancellation as mentioned. However, it&#039;s likely that inference engines compute the mean and variance in FP32 even if inputs are FP16 (especially since these are per-sample operations, not huge batch reductions – it&#039;s feasible to do in higher precision). If someone implemented layer norm purely in FP16, they might see issues when the variance is very small. Usually there’s an $\epsilon$ added which might dominate a tiny variance and thus mitigate catastrophic cancellation (the result might just output something mostly from $\epsilon$). In summary, normalization layers are usually fine as long as the implementation uses enough precision for the summation of squares.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Softmax:&#039;&#039;&#039; Softmax is notorious if done improperly. The safe algorithm: subtract the maximum input from all inputs (to bring the largest exponent to 1.0), exponentiate, sum them up, divide each exponent by the sum. The max subtraction handles overflow in $\exp()$. But what about the sum? As discussed, if there are $N$ elements in the softmax, the sum is at most $N$ (when all inputs equal the max). In classification, $N$ might be the number of classes. For ImageNet, $N=1000$, so worst sum ~1000 (which is fine in FP16, as 1000 &amp;lt;&amp;lt; 65504). For an extreme case like a language model, $N$ could be 50,000 (vocab size). Sum could be 50,000 in worst case (everyone equal probability). 50,000 is still below 65504, so actually an FP16 sum would &#039;&#039;not&#039;&#039; overflow in that scenario, and the rounding error on a sum of 50k identical terms would be maybe a few ULPs (which at 50k magnitude might be ~0.5 difference worst-case). So maybe softmax summation in FP16 is not catastrophic for output probabilities – you’d get essentially 1.0 after normalization instead of 0.99999 in some case. Usually fine.&lt;br /&gt;
&lt;br /&gt;
:The bigger issue is if one or a few terms dominate the softmax. If one term is much larger, the others underflow to 0 after the max subtraction. That’s fine: you get a one-hot distribution essentially. If the model expected a softer distribution, that could be a problem. But if the logits were really that far apart, even FP32 would give nearly one-hot. So not really a precision issue, more of a model property.&lt;br /&gt;
&lt;br /&gt;
:However, the &#039;&#039;&#039;stability during training&#039;&#039;&#039; is a big issue. The GitHub thread we saw (ColossalAI) mentioned training a 130B model where FP16 softmax in attention caused training to occasionally blow up&amp;lt;ref name=&amp;quot;ref14&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref14&amp;quot;/&amp;gt;. They observed attention scores in some heads ranging up to 1e4 or as low as 1e-3&amp;lt;ref name=&amp;quot;ref14&amp;quot;/&amp;gt;. 1e4 exponent in FP16 is definitely Inf (because $\exp(10^4)$ is astronomically huge, but even exp(100) is inf in half). The method CogView introduced (Precision Bottleneck Relaxation) involved subtracting the max &#039;&#039;per head&#039;&#039; to reduce range&amp;lt;ref name=&amp;quot;ref14&amp;quot;/&amp;gt;—which is basically the softmax max trick at a finer granularity or something similar. In the end, they found &#039;&#039;&#039;the simplest fix was to compute softmax in FP32&#039;&#039;&#039; for those layers&amp;lt;ref name=&amp;quot;ref14&amp;quot;/&amp;gt;. They reported that it had negligible speed impact but significantly improved stability&amp;lt;ref name=&amp;quot;ref14&amp;quot;/&amp;gt;. This highlights a general principle: certain operations like softmax (and sometimes matrix multiplications in attention) benefit from higher precision, and using FP32 in a few critical spots can save a mixed-precision training from crashing. In inference, using FP32 for softmax is also a safeguard if one is uncertain. It costs little because softmax is not a heavy compute compared to matrix multiplies.&lt;br /&gt;
&lt;br /&gt;
:ONNX does have a Softmax operator; if the input is float16, a backend might do exactly what they described: internally cast to float32 for the softmax computation, then cast output back to float16. Some inference engines (like DeepSpeed’s inference or others) keep attention softmax in FP32 even if rest is FP16, for robustness&amp;lt;ref name=&amp;quot;ref14&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Apart from softmax, other places where exponentials occur are in loss functions like cross-entropy (which uses log-softmax internally), or in activation functions like Sigmoid and Tanh (which are basically scaled logistic functions). Sigmoid($x)=1/(1+e^{-x})$. If $x$ is moderately large positive, $e^{-x}$ underflows to 0 in FP16 if $x&amp;gt;~11$. That gives a sigmoid output of 1.0 exactly. In FP32, $e^{-x}$ for $x=11$ is about $5e-5$, not underflow, so sigmoid ~0.99995. So FP16 will saturate some outputs to exactly 0 or 1 sooner than FP32 would. This can slightly change behaviors (e.g., a neuron might go to full saturation a bit early). Usually, this isn’t a huge deal, but it could make gradients zero earlier. In inference, we don’t care about gradients, just the output value. Getting exactly 1.0 vs 0.99995 probably has no practical difference for a classification output. But one should be aware that &#039;&#039;&#039;activation functions can saturate faster&#039;&#039;&#039; in lower precision due to limited range or precision. ReLU has no issue (it’s linear or 0), but exponential-based activations do. In practice, again, frameworks often simply compute those in the given precision and accept the slight difference. If it were a problem, one could do those in FP32 as well.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Summary:&#039;&#039;&#039; Numerical stability in inference is maintained by using robust computation methods: subtracting maxima in softmax/LogSumExp, adding epsilon in denominators for normalization, and using higher precision for summations or critical steps. Most of these are built into the algorithms. ONNX models assume these standard practices (they don&#039;t need to explicitly do them in the graph because the backend inherently will for ops like Softmax). It&#039;s wise when building or exporting models to ONNX to test them in the target precision and ensure there are no surprises (like an output becomes NaN or Inf due to precision). Usually if training was done in mixed precision, the model will have been vetted in that environment already.&lt;br /&gt;
&lt;br /&gt;
== Performance vs. Accuracy Trade-offs ==&lt;br /&gt;
&lt;br /&gt;
There is an inherent trade-off between numerical precision and computational performance (speed, memory usage) in AI systems. Choosing lower precision can dramatically increase throughput and reduce memory, but it may introduce arithmetic errors. Engineers must balance these considerations:&lt;br /&gt;
&lt;br /&gt;
=== 7.1. Throughput, Memory Bandwidth, and Precision ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Throughput (speed)&#039;&#039;&#039; is often higher for lower-precision operations. Modern hardware can execute more low-precision ops in parallel than high-precision ops. For example, an NVIDIA V100 GPU can perform 2× more half-precision FLOPs than single-precision per clock, and newer architectures with Tensor Cores can do 8× more (since a Tensor Core warp operation might perform 64 FP16 FMAs per cycle vs 16 FP32). Similarly, int8 operations can be even faster and more energy efficient. Lower precision numbers also use less memory bandwidth – fetching 16-bit data is twice as fast as 32-bit data from memory (assuming the same bus width), which can relieve memory bottlenecks. Many deep learning workloads are memory-bound, so halving the data size can nearly double throughput if compute is not the limiting factor. This is a huge incentive to use lower precision.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Memory and storage&#039;&#039;&#039;: Using FP16 or int8 weights reduces model size, which is important for deploying large models to memory-limited environments (like edge devices or GPUs with limited VRAM). It also means you can fit larger batch sizes in memory, or more models on a chip, etc. There is also a cascade effect: smaller data means better cache utilization, which further improves performance.&lt;br /&gt;
&lt;br /&gt;
However, &#039;&#039;&#039;accuracy&#039;&#039;&#039; suffers if precision is too low. We have discussed how rounding error and limited range can cause deviations in outputs. The question is: do those deviations matter for the application? Often for neural networks, a small amount of noise or error in intermediate calculations does not change the final prediction. Neural nets are somewhat error-tolerant due to their redundant distributed representations. This is why quantization and FP16 techniques can work at all. The final model accuracy (e.g. top-1 accuracy on ImageNet) might drop only 0.5% or less when using FP16 instead of FP32, or int8 instead of FP16, if done properly. For many use-cases, this loss is acceptable given the speed gains.&lt;br /&gt;
&lt;br /&gt;
If one goes too low (like naive FP8 or int4 without special care), accuracy might drop severely – perhaps making the model unusable. Thus the trade-off: as precision decreases, performance increases but at some point accuracy degrades beyond an acceptable threshold. Engineers aim to find the lowest precision that still preserves accuracy.&lt;br /&gt;
&lt;br /&gt;
There is also the consideration of &#039;&#039;&#039;accumulation strategy&#039;&#039;&#039; affecting performance: for instance, Kahan summation (compensated) is more accurate but ~4× slower than naive summation. If you need that accuracy, you pay a performance cost. In many AI inference scenarios, the slight extra accuracy from something like Kahan isn&#039;t needed, so it’s not used. But in a scenario where you sum millions of elements for a final result that is thresholded, maybe you do need it.&lt;br /&gt;
&lt;br /&gt;
Another dimension is &#039;&#039;&#039;determinism vs speed&#039;&#039;&#039;. Enforcing a deterministic reduction order (for reproducibility) might require disabling some parallelism, which could slow things down. If exact repeatability is critical (like certain scientific or financial applications of neural nets), one might sacrifice some performance to ensure the summation happens the same way every time. ONNX allows both modes depending on the runtime.&lt;br /&gt;
&lt;br /&gt;
From a memory perspective, if precision is higher than needed, you waste memory. For example, storing intermediate activations in FP32 when they could be FP16 uses 2× memory, which might mean lower batch size or more memory traffic. So there&#039;s strong motivation to keep things as low-precision as possible &#039;&#039;except&#039;&#039; where accuracy truly demands.&lt;br /&gt;
&lt;br /&gt;
In summary, the trade-off often manifests as &#039;&#039;diminishing returns&#039;&#039;: FP64 vs FP32 – huge slowdown for usually negligible accuracy gain in ML; FP32 vs FP16 – moderate speedup for usually minor accuracy hit; FP16 vs int8 – another big speedup but maybe more accuracy loss unless calibration is done; int8 vs int4 – speed gain but often big accuracy drop that might not be worth it for current models.&lt;br /&gt;
&lt;br /&gt;
Engineers use techniques like &#039;&#039;&#039;mixed precision&#039;&#039;&#039; to target high precision only where needed. For example, run most of the model in FP16, but keep a few sensitive layers in FP32. This yields most of the performance benefits while ensuring critical parts are accurate. We already saw softmax as one example. Another might be final output layer: sometimes using FP32 for the logits and softmax can be safer.&lt;br /&gt;
&lt;br /&gt;
Hardware is also trending to support mixed modes – like FP8 Tensor Cores accumulate in FP32, etc. So hardware tries to give accuracy cheaply.&lt;br /&gt;
&lt;br /&gt;
=== 7.2. Hardware Accelerators and Tensor Cores ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Hardware accelerators&#039;&#039;&#039; (GPUs, TPUs, FPGAs, ASICs like GraphCore IPU, etc.) have specific support for certain precisions. Leveraging these can provide massive speedups:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;NVIDIA Tensor Cores:&#039;&#039;&#039; These started in Volta (V100) with support for 4x4 matrix multiply-adds on FP16 inputs with FP32 accumulation&amp;lt;ref name=&amp;quot;ref6&amp;quot;/&amp;gt;. Using Tensor Cores can give up to 8x throughput compared to using regular FP32 cores. But to use them, the data needs to be FP16 (or now BF16 or FP8 on newer GPUs) and aligned to certain dimensions (multiples of 8 etc. in matrix sizes, which frameworks handle by padding if needed). If an ONNX model is all FP32, by default it would not use Tensor Cores unless the runtime automatically converts some ops to mixed precision (which some do via a flag or environment variable enabling TF32 or FP16). The introduction of &#039;&#039;&#039;TensorFloat-32 (TF32)&#039;&#039;&#039; in Ampere GPUs is essentially a compromise: treat FP32 inputs as two 16-bit significand halves (10-bit mantissa actually) so that it can run on Tensor Cores, giving ~8x speed, while still outputting FP32. TF32 has about the precision of FP16 but range of FP32, and it was chosen because it typically doesn’t hurt convnet accuracy. ONNX Runtime or others on Ampere will use TF32 by default for conv and matmul unless disabled, giving a free speed boost at minimal accuracy cost. If exact FP32 is needed, users have to turn it off (sacrificing speed).&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;TPU (Google):&#039;&#039;&#039; TPUs have native support for BF16 matrix ops with FP32 accumulators. They were designed specifically for the mixed precision approach in training. For inference, TPUs also support int8 and other quantized ops (e.g., via Edge TPU for lower-power). If exporting ONNX to run on a TPU (via some bridge), one might get automatic BF16 usage. BF16 has the advantage of no loss in range, so TPUs can more straightforwardly run models in BF16 without modifications.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;FPGAs and ASICs:&#039;&#039;&#039; Some custom accelerators might use fixed-point or block floating-point arithmetic for efficiency. They often treat the network in quantized terms. The ONNX quantization specification (QuantizeLinear/DequantizeLinear ops) exists for representing quantized models (INT8 etc.), which is another domain beyond floating-point, but it overlaps as a precision issue. In those systems, accumulators for INT8 × INT8 multiplies are usually 32-bit (to avoid overflow for dot products up to certain lengths). The trade-off there is between integer vs float representation, which can give big speedups on CPUs/DSAs via SIMD instructions (many CPUs have vector int8 multiply-add instructions now for inference).&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Memory bandwidth on hardware:&#039;&#039;&#039; Some accelerators include special high-bandwidth memory or cache specifically for lower precision. For example, an accelerator might fetch two FP16 values in one 32-bit memory transaction – effectively doubling memory bandwidth for half precision. On some hardware, using FP16 can even reduce memory &#039;&#039;latency&#039;&#039; if multiple values can be packed.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Parallelism and Vectorization:&#039;&#039;&#039; Many hardware units process data in fixed vector widths (e.g., 256-bit registers). That could be 8 floats at once or 16 half-floats at once in the same 256 bits. If your code is in FP16, you might use the full vector width (16 values) whereas FP32 might only use half (8 values). This means FP16 can make better use of vector units. Similarly, matrix units often have fixed tile sizes, and smaller data types allow larger tiles or more tiles concurrently.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Energy efficiency:&#039;&#039;&#039; Lower precision often means less energy per operation, which on large deployments (data centers or battery-powered devices) is a significant advantage. This isn&#039;t directly performance, but it&#039;s an important trade: you might choose a slightly lower precision to save power even if accuracy drops a tiny bit.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Software overhead:&#039;&#039;&#039; Using mixed precision on hardware involves some overhead of casting and moving data between precisions. For instance, if you have to frequently cast FP16 to FP32 and back, that adds instructions and memory copies. A well-designed accelerator (like GPUs with unified memory for FP16/32 or special hardware casting) minimize this. Still, the runtime might insert extra operations (as we saw, ONNX Runtime CPU inserted many Cast nodes converting FP16 to FP32 and back&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;, which actually &#039;&#039;slowed down&#039;&#039; that FP16 model by 4x vs FP32 on a CPU, because the CPU had no native FP16). So ironically, on some hardware, using lower precision in ONNX might degrade performance if not supported (the CPU case). Always ensure the target hardware actually supports the low precision you plan to use; otherwise, you&#039;ll pay overhead for emulation.&lt;br /&gt;
&lt;br /&gt;
To illustrate trade-off: Suppose you have a model that achieves 99% accuracy in FP32 and drops to 98.5% in FP16. If FP16 runs 2x faster, many would accept the 0.5% accuracy drop for double throughput. If int8 runs 4x faster but accuracy drops to 95%, that might be too much drop for some applications, but acceptable for others that value speed. It&#039;s application-dependent.&lt;br /&gt;
&lt;br /&gt;
In critical applications, one might maintain higher precision accumulators or run certain validation steps in high precision to ensure nothing drifted. In research, sometimes a model is trained in mixed precision but validated in double to ensure any small numeric differences don’t influence the reported result (rare, but for scientific computing integration maybe).&lt;br /&gt;
&lt;br /&gt;
In the ONNX world, performance-vs-accuracy is often managed via &#039;&#039;&#039;Execution Providers&#039;&#039;&#039; (EPs) that target specific hardware. For example, the TensorRT EP in ONNX Runtime can take an ONNX model and run it using NVIDIA&#039;s high-performance inference library, which will use FP16 or INT8 if allowed, to maximize speed. The user can configure the EP with precision flags. If the accuracy drop is too high, one might restrict it to FP32. If it&#039;s fine, allow FP16. Tools exist to measure the accuracy difference (like calibrating int8 quantization).&lt;br /&gt;
&lt;br /&gt;
To conclude, the trade-offs revolve around picking the lowest precision that yields acceptable accuracy, and utilizing hardware features fully. ONNX being a high-level spec abstracts this, but it&#039;s ultimately up to the deployment to decide and sometimes up to the exporter to incorporate quantization. ONNX’s role is to be able to &#039;&#039;represent&#039;&#039; these choices (they have added quantized types, FP8 types, etc., to allow the graph to include those when needed).&lt;br /&gt;
&lt;br /&gt;
== Best Practices and Design Guidelines ==&lt;br /&gt;
&lt;br /&gt;
Given the above insights, here are some best practices and guidelines for dealing with numerical precision and accumulators in AI inference (and training) design:&lt;br /&gt;
&lt;br /&gt;
=== 8.1. Choosing an Accumulator Precision ===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Match precision to data length and range:&#039;&#039;&#039; If an operation accumulates a large number of terms, consider using a higher precision accumulator than the input data. For example, for summing &amp;gt;1000 values of type FP16, an FP32 accumulator is recommended to avoid large rounding error or overflow. If summing &amp;gt; million values of FP32, consider double (FP64) accumulation if high accuracy is needed (though this is rare in DL). &#039;&#039;Rule of thumb:&#039;&#039; if $N \cdot \varepsilon$ is not tiny (say &amp;gt;1e-3), you may benefit from higher precision. For FP16 ($\varepsilon \approx 1e-3$), that threshold N is a few hundred. For FP32 ($\varepsilon \approx 1e-7$), N would be in the millions before 1e-3 error.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Use higher precision in critical layers:&#039;&#039;&#039; Identify if certain layers of the model are more sensitive to precision. For instance, the output layer or a layer that feeds into a very sharp decision boundary might require more precision. If a small change there could flip the prediction, you don&#039;t want excessive numerical error. Using FP32 for logits and softmax is a common practice to maintain classification confidence stability.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Follow hardware defaults, but override if needed:&#039;&#039;&#039; Many hardware accelerators have sensible defaults (like accumulate in FP32 for tensor ops). It&#039;s usually best to use those defaults as they were chosen to balance accuracy and speed. Only override if you have evidence of a problem. For example, if using ONNX Runtime on CPU, you discovered it upcasts to float for you – great. But if using some custom accelerator that does pure FP16, you might manually insert casts in the ONNX graph around certain ops to force higher precision on those ops.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Maintain precision for gradient accumulation (training):&#039;&#039;&#039; This is more training-oriented, but worth noting: when summing gradients over batches or updating weights, use higher precision (FP32 or even 64) for those accumulations. This ensures training doesn&#039;t diverge due to numeric error. Many frameworks keep a &amp;quot;master copy&amp;quot; of weights in FP32 when training in FP16&amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;. For inference, this translates to: any running statistics or counters in the model (like if you had a running average inside the model) should probably be in higher precision to avoid drift.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Be mindful of intermediate cast costs:&#039;&#039;&#039; If you do decide to cast to higher precision for an operation in ONNX, remember it incurs memory copies and compute overhead. Make sure it&#039;s worth it. For example, if you cast a huge tensor to double, that could slow things drastically. Usually FP16-&amp;gt;FP32 is fine (hardware handles it well), but FP32-&amp;gt;FP64 might be expensive on GPUs.&lt;br /&gt;
&lt;br /&gt;
=== 8.2. Safe Reduction Lengths and Heuristics ===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Chunk long reductions:&#039;&#039;&#039; If you must do a very long sum in low precision (due to memory constraints or hardware), chunk the sum into parts, sum each to a higher precision local accumulator, then sum the partial sums. This is essentially pairwise summation. Many libraries use a fixed chunk size heuristic (e.g., sum in blocks of 256 in registers, then add those up). This bounds the error growth. If implementing manually, you can choose a chunk size such that chunk_length &#039;&#039; ε is small (like 256 &#039;&#039; 1e-3 = 0.256 for FP16, hmm maybe choose 100).&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Sort by magnitude if possible:&#039;&#039;&#039; In some cases (not typical in DL, but possible in post-processing), if you have numbers varying by orders of magnitude, summing from smallest to largest improves accuracy. It prevents small ones from being swallowed by big ones. This is more relevant if you output a sum of mixed-scale values (like some algorithm mixing big and small contributions). It’s not always feasible in real-time inference to sort, but keep in mind.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Use compensation for critical sums:&#039;&#039;&#039; If you find a particular sum or difference in your inference computation is crucial (like computing a small residual from two close numbers), consider using Kahan summation or another compensated scheme just for that part. For example, if you had an ONNX custom function that needs to sum a lot of almost cancelling terms, you might implement it with compensation internally.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Heuristics for FP8/low-bit:&#039;&#039;&#039; When using extremely low precision like FP8, adopt the recommended practices: use scaling (don&#039;t feed raw data into FP8, normalize it first), and perhaps only use FP8 on portions of the network that are empirically robust. The industry is still establishing heuristics, but one might be: use FP8 for matrix multiplications, but accumulate in FP16 or FP32; keep normalization in FP16; use FP16 for first and last layer maybe. This is evolving, so keep an eye on literature.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Keep an eye on sum length in softmax and normalization:&#039;&#039;&#039; If you have extremely large class counts or group sizes (e.g., a softmax over 1e6 items, or layer norm over tens of thousands of features), realize this is a giant reduction. You may want to ensure that reduction is done in higher precision. ONNX Softmax by default will likely not handle a million-class case in FP16 well (the sum would overflow since 1e6 &amp;gt; 65504). So either do that on CPU in FP32 or break it down (could do hierarchical softmax, etc.). This is a corner case, but relevant for huge vocab language models. In practice, vocab ~50k is fine as we said.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Test numeric differences:&#039;&#039;&#039; A practical tip: after quantizing or reducing precision, run some test inputs through both the high-precision model and low-precision model. Compare outputs (and maybe some intermediate signals if possible). If differences are consistently small relative to the application tolerance, you&#039;re good. If you see occasional large differences or instabilities (like outputs become NaN or Inf in low precision), that&#039;s a red flag. Use that testing to guide where you might need to increase precision or change algorithms.&lt;br /&gt;
&lt;br /&gt;
=== 8.3. When Higher Precision Is Required ===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Ill-conditioned calculations:&#039;&#039;&#039; If your model or pipeline performs any operation known to be ill-conditioned (matrix inversion, subtraction of nearly equal quantities, computing a tiny difference of large numbers), you likely need higher precision for that part. For example, if your ONNX model includes a custom op to solve a linear system, use double precision in that op if possible (or at least float with iterative refinement). In ML, this might occur in something like a physics-informed neural network or a differentiable simulation within the model.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Strict accuracy requirements:&#039;&#039;&#039; Some applications (medical, aerospace, finance) might have strict tolerances. If an inference model’s output needs to be correct to 5 decimal places, you probably cannot use FP16, which has only 3-4 decimal digits precision. FP32 might be barely enough (~7 digits). FP64 might be warranted to guarantee that level of accuracy in worst case. Most deep learning applications (like image classification) don&#039;t demand that level of numeric precision in outputs – as long as the top class is correct, it doesn&#039;t matter if its probability was 0.901 vs 0.899. But if you&#039;re doing something like summing probabilities to make a risk estimate or doing a cumulative sum over many predictions, be careful to accumulate precisely.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Large sums or products in post-processing:&#039;&#039;&#039; Sometimes the neural net is fine, but the &#039;&#039;post-processing&#039;&#039; of results might need precision. E.g., summing up probabilities of many events, or computing confidence intervals, etc. Do those calculations in double if needed outside the model.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Preventing drift in long sequences:&#039;&#039;&#039; For sequential models (RNNs processing thousands of steps), rounding error can accumulate over steps. If each step has a small error, after 10000 steps it could add up. In such cases, using a higher precision for the state might reduce drift. Some practitioners use FP64 for the hidden state accumulation in very long sequence processing to maintain numerical stability (though this is not common due to speed loss). Alternatively, periodically renormalize or reset small errors.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Simulations or differential equations:&#039;&#039;&#039; If your ONNX model involves simulating something (like a differential equation solver inside a model, or an ODE/RNN hybrid), those often need double precision to be stable for long durations. Recognize those situations and allocate precision accordingly.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Verification and debugging:&#039;&#039;&#039; Use higher precision as a tool to verify correctness. For example, run your model in FP64 (if possible) and compare to FP32 to see if there are differences. If not, FP32 is fine. If yes, you might find a particular calculation losing too much info in FP32, implying maybe you should keep it in FP64 in production as well. This is rare in inference but can be part of rigorous validation.&lt;br /&gt;
&lt;br /&gt;
In ONNX, using higher precision may involve casting to &amp;lt;code&amp;gt;tensor(double)&amp;lt;/code&amp;gt; for certain ops. Not all runtimes support double on GPU though (some do via emulation or just slower path). ONNX CPU does support double for general ops. So be mindful of the target.&lt;br /&gt;
&lt;br /&gt;
Finally, &#039;&#039;&#039;document your precision choices&#039;&#039;&#039;. If you deliver an ONNX model that uses mixed types (float16 inputs, float32 accumulations, int8 weights, etc.), ensure that is clear to whoever maintains or uses it. Precision issues can be sneaky, so clarity helps.&lt;br /&gt;
&lt;br /&gt;
== Limitations and Open Problems ==&lt;br /&gt;
&lt;br /&gt;
Despite advances in handling numerical precision, there remain limitations and open challenges in this domain:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Lack of Precision Specification in ONNX:&#039;&#039;&#039; As noted, ONNX currently has no way to formally specify that an op &#039;&#039;should&#039;&#039; use a higher precision internally. This can lead to nondeterministic differences across platforms&amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;. While proposals exist to add attributes for this&amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;, it is not yet standard. Thus an ONNX model might behave slightly differently on two compliant backends, and there&#039;s no built-in mechanism to ensure identical results. This is a limitation for applications requiring cross-platform consistency. Solving it may involve extending ONNX (e.g., an attribute like &amp;lt;code&amp;gt;accumulatePrecision=&amp;quot;FLOAT32&amp;quot;&amp;lt;/code&amp;gt; for MatMul) or tightening the spec, but those come with backward compatibility and implementation burden concerns.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Reproducibility and Determinism:&#039;&#039;&#039; Even with the same precision, parallel computation can introduce nondeterminism (due to different summation orders). Ensuring bit-identical results across runs (important for some sensitive domains) is non-trivial. Frameworks often provide &amp;quot;deterministic mode&amp;quot; which might use fixed ordering at cost of speed. In ONNX, there&#039;s no global flag for this; it&#039;s up to the runtime. More research could be done on algorithms that produce bit-wise identical results independent of parallel execution order (perhaps using techniques like pairwise summation or consistent partitioning). Stochastic rounding is another area: sometimes using probabilistic rounding can reduce bias in accumulated error, but it introduces randomness – reconciling that with reproducibility is tricky.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Ultra-low Precision (FP8 and below):&#039;&#039;&#039; While FP8 is being explored, going to 4-bit floats or mixed schemes is an open challenge. The OpenAI community is investigating 4-bit (quantization-wise) for LLMs with some success, but a true 4-bit float (like E3M1) would be extremely limited. Perhaps hybrid schemes (like block floating point, log scales, etc.) will come. ONNX already has some support (they mention 4-bit in documentation placeholders&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;), but it&#039;s nascent. Research into training techniques, calibration, and new number formats (like &#039;&#039;&#039;posit numbers&#039;&#039;&#039; or tapering precision floats) is ongoing. If a new format proves useful, ONNX will need to adapt to represent it.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Quantization Noise and Model Robustness:&#039;&#039;&#039; Reducing precision can sometimes expose models to issues like adversarial vulnerability or fragility – e.g., a slight perturbation might flip a decision if the margin was small and precision loss nudged it. Ensuring model robustness under quantization is an open problem. It might require re-training the model with quantization in the loop (quantization-aware training) or designing models that have large &amp;quot;margin&amp;quot; between classes to tolerate quantization noise.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Dynamic range during training vs inference:&#039;&#039;&#039; In training, activations can have wider distributions (especially early in training). Models often learn to constrain their ranges (via activation functions saturating or normalization), which makes inference more stable in lower precision. But if one tries to train in FP8 from scratch, it&#039;s very hard. This gap between what&#039;s possible in a post-trained inference model vs training is an open area – how to train directly in low precision without significant accuracy loss. Solving that could simplify workflows (no need for high-precision training then quantize; just train low-prec end to end). Techniques like loss scaling, gradient clipping, etc., are partial solutions. The development of optimizers that are resilient to low precision is ongoing.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Hardware variability:&#039;&#039;&#039; As new hardware emerges (with new numeric capabilities or quirks), ensuring that ONNX models run consistently is an ongoing task. For instance, one device might implement a fused op that reduces rounding error, another does it separately. Even something like convolution might use a different algorithm (FFT-based vs direct) which has different numeric properties. Open problem: could ONNX allow specifying numeric tolerances per op or preferred algorithms? That may be overkill for most, but for some HPC ML use cases, maybe desired.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Testing and validation of numerics:&#039;&#039;&#039; There isn&#039;t a standardized set of &amp;quot;numeric compliance&amp;quot; tests for ONNX runtimes beyond simple correctness. Perhaps in the future, test suites might include challenging numeric scenarios (large sums, nearly singular matrices) to ensure a runtime handles them gracefully (maybe by switching precision). Right now, it&#039;s mostly on the user to test their model under expected conditions.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Combining quantization with floating point:&#039;&#039;&#039; Many production models use a mix (e.g., int8 for some layers, FP16 for others). ONNX can represent this (with QuantizeLinear ops or MixedPrecision through casting), but tooling to automatically find the optimal mix is still developing. It&#039;s somewhat trial-and-error or using heuristics. An open question is how to automate precision assignment: given a model and a target hardware, how to automatically choose which layers can be int8, which should be FP16, etc., to meet an accuracy target. This crosses into NAS (neural architecture search) territory or automated quantization. &lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Numerical debugging tools:&#039;&#039;&#039; When a model goes wrong due to precision, it&#039;s often hard to pinpoint where. Better tooling to analyze numerical error layer by layer could be useful. For instance, one could run a model in double and float and diff the intermediate results to see where the largest divergence happens. Doing this systematically for big nets is an open tooling challenge. Some frameworks have hooks for this, but not standardized.&lt;br /&gt;
&lt;br /&gt;
In conclusion, while we&#039;ve come far with using lower precision in AI without sacrificing much accuracy, the landscape is always evolving. The key open problem is how to push precision lower (for efficiency gains) without crossing the threshold where accuracy falls off a cliff. Each new generation of hardware and techniques (like FP8, novel quantization schemes) extends that boundary, but also brings new questions on how to manage the numerical errors. ONNX as an interchange will need to keep pace, possibly by adding more expressive power for these numeric considerations.&lt;br /&gt;
&lt;br /&gt;
== See Also ==&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;IEEE 754 Floating-Point&#039;&#039;&#039; – The standard defining binary16, binary32, binary64 formats and rounding behavior.&lt;br /&gt;
* &#039;&#039;&#039;Floating-Point Arithmetic&#039;&#039;&#039; – General topic on how floating-point math works, common issues (overflow, underflow, cancellation).&lt;br /&gt;
* &#039;&#039;&#039;Kahan Summation Algorithm&#039;&#039;&#039; – Algorithm for compensated summation to reduce numerical error&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;.&lt;br /&gt;
* &#039;&#039;&#039;BFloat16&#039;&#039;&#039; – A short-format float with 8-bit exponent, used in TPUs and some GPUs for training networks.&lt;br /&gt;
* &#039;&#039;&#039;Mixed Precision Training&#039;&#039;&#039; – Technique of using FP16 (or BF16) along with FP32 accumulators to accelerate training&amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;.&lt;br /&gt;
* &#039;&#039;&#039;Quantization in Deep Learning&#039;&#039;&#039; – Converting networks to lower bit integer arithmetic for inference (INT8 quantization and calibration techniques).&lt;br /&gt;
* &#039;&#039;&#039;ONNX Operator Specification&#039;&#039;&#039; – Official docs for ONNX ops, which describe their mathematical function but not the required numeric precision (a point of potential extension in future).&lt;br /&gt;
* &#039;&#039;&#039;CUDA Tensor Cores &amp;amp; TF32&#039;&#039;&#039; – NVIDIA documentation on how tensor cores perform mixed precision and the TF32 format for accelerating FP32.&lt;br /&gt;
* &#039;&#039;&#039;Pairwise Summation&#039;&#039;&#039; – Summation algorithm with logN error growth&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;, used in many numerical libraries to improve accuracy of reductions.&lt;br /&gt;
* &#039;&#039;&#039;Softmax Stability&#039;&#039;&#039; – Articles on numerically stable softmax and log-sum-exp implementations to avoid overflow.&lt;br /&gt;
* &#039;&#039;&#039;Performance Tuning for Low Precision&#039;&#039;&#039; – Guides (e.g., NVIDIA or Intel whitepapers) on using low precision and the trade-offs involved in various hardware.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;references&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref1&amp;quot;&amp;gt;Accelerate and simplify Scikit-learn model inference with ONNX Runtime - Microsoft Open Source Blog — opensource.microsoft.com — https://opensource.microsoft.com/blog/2020/12/17/accelerate-simplify-scikit-learn-model-inference-onnx-runtime&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref2&amp;quot;&amp;gt;Floating Point Precision: Understanding FP64, FP32, and FP16 in Large Language Models - DEV Community — dev.to — https://dev.to/lukehinds/floating-point-precision-understanding-fp64-fp32-and-fp16-in-large-language-models-3gk6&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref3&amp;quot;&amp;gt;Floating Point Precision and Its Limitations | by Umair Akbar | Medium — akbu.medium.com — https://akbu.medium.com/floating-point-precision-and-its-limitations-cfb7247d7789&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref4&amp;quot;&amp;gt;Kahan summation algorithm - Wikipedia — en.wikipedia.org — https://en.wikipedia.org/wiki/Kahan_summation_algorithm&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref5&amp;quot;&amp;gt;Pairwise summation - Wikipedia — en.wikipedia.org — https://en.wikipedia.org/wiki/Pairwise_summation&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref6&amp;quot;&amp;gt;Numerical behavior of NVIDIA tensor cores - PMC — pmc.ncbi.nlm.nih.gov — https://pmc.ncbi.nlm.nih.gov/articles/PMC7959640/&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref7&amp;quot;&amp;gt;Mixed precision and performance-accuracy tuning (neuron-cc) — AWS Neuron Documentation — awsdocs-neuron.readthedocs-hosted.com — https://awsdocs-neuron.readthedocs-hosted.com/en/latest/about-neuron/appnotes/neuron-cc/mixed-precision.html&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref8&amp;quot;&amp;gt;Create Float16 and Mixed Precision Models - ONNX Runtime — onnxruntime.ai — https://onnxruntime.ai/docs/performance/model-optimizations/float16.html&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref9&amp;quot;&amp;gt;[Performance] ONNX FP16 model is having performance bottle neck when compared to FP32 variant · Issue #25824 · microsoft/onnxruntime · GitHub — github.com — https://github.com/microsoft/onnxruntime/issues/25824&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref10&amp;quot;&amp;gt;Float stored in 8 bits - ONNX 1.21.0 documentation — onnx.ai — https://onnx.ai/onnx/technical/float8.html&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref11&amp;quot;&amp;gt;[2209.05433] FP8 Formats for Deep Learning — arxiv.org — https://arxiv.org/abs/2209.05433&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref12&amp;quot;&amp;gt;Should MatMul op allow specification of accumulation precision? · Issue #7072 · onnx/onnx · GitHub — github.com — https://github.com/onnx/onnx/issues/7072&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref13&amp;quot;&amp;gt;Quantize ONNX models | onnxruntime — onnxruntime.ai — https://onnxruntime.ai/docs/performance/model-optimizations/quantization.html&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref14&amp;quot;&amp;gt;Keep Attention Softmax FP32 during FP16/ZeRO Training · Issue #1485 · hpcaitech/ColossalAI · GitHub — github.com — https://github.com/hpcaitech/ColossalAI/issues/1485&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;/references&amp;gt;&lt;/div&gt;</summary>
		<author><name>Timo.stripf</name></author>
	</entry>
	<entry>
		<id>https://www.emmtrix.com/w139/index.php?title=Numerical_Precision_in_ONNX_and_AI_Inference&amp;diff=2896</id>
		<title>Numerical Precision in ONNX and AI Inference</title>
		<link rel="alternate" type="text/html" href="https://www.emmtrix.com/w139/index.php?title=Numerical_Precision_in_ONNX_and_AI_Inference&amp;diff=2896"/>
		<updated>2026-01-30T15:21:01Z</updated>

		<summary type="html">&lt;p&gt;Timo.stripf: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&#039;&#039;&#039;Open Neural Network Exchange (ONNX)&#039;&#039;&#039; is an open standard format for representing machine learning models and neural network computations across different frameworks and hardware&amp;lt;ref name=&amp;quot;ref1&amp;quot;/&amp;gt;. As models are exported and deployed via ONNX, the &#039;&#039;&#039;numerical precision&#039;&#039;&#039; of computations becomes critical. Deep learning inference involves a variety of floating-point operations, and small numerical discrepancies can accumulate and affect model accuracy or reproducibility. This article provides an in-depth overview of floating-point precision issues in numerical computing, with a focus on how they manifest in AI inference and the ONNX ecosystem. We cover floating-point formats and rounding behavior, accumulation strategies for summations, the use of low-precision formats (FP16, BFloat16, FP8) in modern AI, and how ONNX defines (or leaves unspecified) the precision of accumulators in operations. Strategies for maintaining numerical stability in deep neural networks are discussed, along with the trade-offs between computational performance and arithmetic accuracy. The goal is to give engineers and researchers a comprehensive, encyclopedic reference on numerical precision in ONNX-based AI inference systems.&lt;br /&gt;
&lt;br /&gt;
== Floating-Point Precision in Numerical Computing ==&lt;br /&gt;
&lt;br /&gt;
=== 2.1. IEEE 754 Floating-Point Formats ===&lt;br /&gt;
&lt;br /&gt;
Modern computers represent real numbers using the IEEE 754 floating-point standard, which defines formats like 32-bit &#039;&#039;single precision&#039;&#039; (FP32) and 64-bit &#039;&#039;double precision&#039;&#039; (FP64). In IEEE 754 binary formats, a floating-point number is composed of three fields: 1 sign bit, several exponent bits (with a bias), and several fraction (mantissa) bits. For example, FP32 consists of 1 sign bit, 8 exponent bits, and 23 fraction bits (24 bits of significand precision including the implicit leading 1)&amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;. This provides roughly 7 decimal digits of precision and an exponent range allowing values on the order of $10^{\pm38}$. FP64 allocates 1 sign bit, 11 exponent bits, and 52 fraction bits (53-bit precision), yielding about 15–16 decimal digits of precision and a vastly larger range (~$10^{\pm308}$)&amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;. These larger formats (especially FP64) are the &amp;quot;gold standard&amp;quot; for numerical accuracy in scientific computing, as they reduce round-off error in complex calculations&amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;. However, the increased precision comes at the cost of doubled storage and typically reduced computational throughput (e.g. many GPUs execute FP64 instructions at a much lower rate than FP32&amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
In addition to single and double precision, the IEEE 754-2008 standard introduced a 16-bit &#039;&#039;half precision&#039;&#039; format (FP16, or binary16). FP16 has 1 sign bit, 5 exponent bits, and 10 fraction bits (11-bit significand including the implicit 1)&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. This format covers a much smaller dynamic range (approximately $6.1\times10^{-5}$ to $6.5\times10^4$ for normalized values&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;) and offers only around 3 decimal digits of precision. Half precision was originally used in graphics, but it has gained popularity in machine learning for its speed and memory advantages (discussed later in Section 4). Another format relevant to AI is &#039;&#039;&#039;BFloat16&#039;&#039;&#039; (Brain Float 16), a 16-bit float with &#039;&#039;&#039;8 exponent bits and 7 fraction bits&#039;&#039;&#039;&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. BFloat16 sacrifices precision (only ~7 fraction bits ≈ 2–3 decimal digits) in order to have the &#039;&#039;&#039;same exponent range as FP32&#039;&#039;&#039;&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. This means BFloat16 can represent very large or very small numbers (∼$10^{\pm38}$, similar range as FP32) but with much less precision between representable values&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. BFloat16 was introduced for deep learning by Google (for TPUs) to allow reduced precision &#039;&#039;training&#039;&#039; without frequent overflow/underflow, relying on the wide range and accepting the coarse precision.&lt;br /&gt;
&lt;br /&gt;
Each floating-point format has a &#039;&#039;&#039;unit roundoff&#039;&#039;&#039; or machine precision, denoted $\epsilon$. This is effectively the spacing between 1.0 and the next representable number. For FP32, $\epsilon \approx 2^{-23} \approx 1.19\times10^{-7}$, corresponding to about 7–8 decimal digits of precision&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. In FP64, $\epsilon = 2^{-52} \approx 2.22\times10^{-16}$ (15–16 decimal digits). Half precision is much less precise: FP16 has $\epsilon = 2^{-10} \approx 9.76\times10^{-4}$&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;, and BFloat16 has $\epsilon = 2^{-7} = 7.8125\times10^{-3}$&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. These values quantify how finely the continuum of real numbers is quantized in each format.&lt;br /&gt;
&lt;br /&gt;
=== 2.2. Machine Epsilon and Rounding Error ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Machine epsilon ($\varepsilon$)&#039;&#039;&#039; is formally defined as the smallest positive number such that $1.0 + \varepsilon &amp;gt; 1.0$ in the floating-point format&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. It effectively measures the &#039;&#039;&#039;precision&#039;&#039;&#039; of the representation. In FP32, $\varepsilon = 2^{-23} \approx 1.19\times10^{-7}$, meaning any result is rounded to about 7 decimal digits. FP16’s $\varepsilon = 2^{-10} \approx 9.77\times10^{-4}$, over 1000× larger (so only ~3 decimal digits of precision), and BFloat16’s $\varepsilon = 2^{-7} \approx 7.8\times10^{-3}$, reflecting its mere ~8-bit precision&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. Rounding in IEEE 754 by default uses &#039;&#039;&#039;round-to-nearest (ties to even)&#039;&#039;&#039;, which ensures the rounding error for any single operation is at most half a unit in the last place (0.5 ULP). We can model a floating-point addition as: &lt;br /&gt;
&lt;br /&gt;
$$\text{fl}(a+b) = (a+b)\,(1 + \delta),$$ &lt;br /&gt;
&lt;br /&gt;
where $|\delta| \le \varepsilon$ for that format&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. Here $\varepsilon/2$ is the maximum &#039;&#039;&#039;relative error&#039;&#039;&#039; in one rounding (also called unit roundoff). For example, when adding two FP32 numbers, the result is accurate to within about $10^{-7}$ of the true sum (relative), whereas in BF16, the result can deviate by up to $\sim7.8\times10^{-3}$ (nearly 0.78% relative error per operation)&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. Thus, &#039;&#039;&#039;each arithmetic operation incurs a small rounding error&#039;&#039;&#039; (on the order of machine epsilon) that can propagate or accumulate through a sequence of calculations.&lt;br /&gt;
&lt;br /&gt;
Because floating-point representations have finite exponent range, we must also consider &#039;&#039;&#039;overflow&#039;&#039;&#039; and &#039;&#039;&#039;underflow&#039;&#039;&#039;. If a result’s magnitude exceeds the maximum representable value, it overflows to infinity; if it falls below the minimum normal (or minimum subnormal) value, it underflows to zero (or a denormalized tiny value). FP32 and BF16 share an exponent size (8 bits), so their range for normal numbers is roughly $[10^{-38}, 10^{38}]$ (approx $~\!3.4\times10^{38}$ max)&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. FP16’s 5-bit exponent allows max $\sim 6.55\times10^4$ and min normal $\sim 6.10\times10^{-5}$&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. In practice, &#039;&#039;&#039;underflow/overflow&#039;&#039;&#039; can occur in low-precision arithmetic if values are not appropriately scaled. For instance, summing a large number of moderately sized terms in FP16 could overflow where FP32 would not. As an example, summing on the order of $10^4$ values of magnitude ~$10^1$ (say, adding 10,000 terms around 10 each) would produce a total $\sim10^5$, which exceeds FP16’s max finite value ~$6.5\times10^4$ and would overflow to $+\infty$ in half precision&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. Underflow is usually a concern when subtracting nearly equal numbers (catastrophic cancellation yielding a tiny difference that might flush to zero in low precision).&lt;br /&gt;
&lt;br /&gt;
=== 2.3. Accumulation Error in Reductions ===&lt;br /&gt;
&lt;br /&gt;
Floating-point addition is &#039;&#039;&#039;not associative&#039;&#039;&#039;: $(a+b)+c$ can differ from $a+(b+c)$ due to rounding, and thus the order in which we sum a list of numbers can change the result slightly. When summing a large sequence of $N$ numbers $x_1, x_2, ..., x_N$, the &#039;&#039;&#039;accumulated rounding error&#039;&#039;&#039; can be significant. In the &#039;&#039;worst case&#039;&#039; (e.g., adding many positive and negative terms that cause maximal cancellation or always rounding one way), the error can grow on the order of $O(N \varepsilon)$ relative to the exact sum&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;. Simply summing $N$ numbers in sequence (the &#039;&#039;&#039;naive linear summation&#039;&#039;&#039;) has a worst-case error bound proportional to $N\,\varepsilon$&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. This worst-case occurs for adversarial arrangements of values; in more typical scenarios with rounding errors of random signs, one can model the error as a random walk, yielding an expected root-mean-square (RMS) error growth on the order of $O(\sqrt{N}\,\varepsilon)$&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;. Still, with very large $N$, even $\sqrt{N}$ times $\varepsilon$ can become large enough to matter.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Cancellation&#039;&#039;&#039; is a particular problem: if we add two nearly equal numbers of opposite sign, most leading digits cancel and the result loses significant bits of precision. For example, in single precision, subtracting two numbers that agree in the first 6 decimal digits will yield a result with only ~1 digit of accuracy left. Catastrophic cancellation can dramatically increase relative error. Summation is especially sensitive to cancellation if large positive and negative terms are summed naively. The &#039;&#039;&#039;condition number&#039;&#039;&#039; of a summation (the ratio of the sum of absolute values $\sum|x_i|$ to the absolute value of the true sum $|\sum x_i|$) indicates how sensitive the result is to perturbations&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;. If the numbers have mixed signs and nearly cancel out (making the condition number large), even an optimal summation order will have a large relative error. For instance, summing [1.0, $10^{100}$, 1.0, $-10^{100}$] in double precision has an exact sum of 2.0, but naive summation may yield 0.0 due to cancellation; an improved summation algorithm yields the correct 2.0&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
In practical terms, when summing a long list of floating-point numbers (such as accumulating a dot product or reducing a tensor), the &#039;&#039;&#039;accumulation error&#039;&#039;&#039; can become noticeable if no precautions are taken. Techniques to mitigate this error include carefully choosing the summation order or using higher precision for intermediate sums. We will explore these strategies in the next section.&lt;br /&gt;
&lt;br /&gt;
== Accumulators and Reduction Strategies ==&lt;br /&gt;
&lt;br /&gt;
When performing reductions (sums, dot products, etc.), the method of accumulation can greatly affect the accuracy of the result. Below, we discuss various strategies:&lt;br /&gt;
&lt;br /&gt;
=== 3.1. Naive Linear Accumulation ===&lt;br /&gt;
&lt;br /&gt;
The simplest accumulation strategy is to use a single accumulator and add each element sequentially. In C-like pseudocode:&lt;br /&gt;
&lt;br /&gt;
This &#039;&#039;&#039;naive linear summation&#039;&#039;&#039; adds values one by one to the running total. While straightforward and fast, it is prone to the rounding error accumulation described earlier. The partial sum &amp;lt;code&amp;gt;sum&amp;lt;/code&amp;gt; continually grows (in absolute value) as terms are added, so new small terms may fall outside its precision. For example, if &amp;lt;code&amp;gt;sum&amp;lt;/code&amp;gt; is large and positive, adding a comparatively tiny positive number may produce no change because the small term&#039;s significant digits vanish in the presence of the large sum (the increment is below the rounding threshold of &amp;lt;code&amp;gt;sum&amp;lt;/code&amp;gt;). Conversely, adding a small number of opposite sign can suffer from cancellation. The order of addition matters: summing from smallest magnitude to largest (ascending order) tends to yield a more accurate result than summing in descending order, because adding small terms first preserves their contributions before the sum becomes very large. The naive loop above implicitly sums in the given index order; if the data is unsorted, this order is essentially arbitrary.&lt;br /&gt;
&lt;br /&gt;
The performance of linear accumulation is excellent (minimal overhead), and for many applications the resulting error is within acceptable limits. However, for &#039;&#039;&#039;very large N&#039;&#039;&#039; or ill-conditioned sums, the inaccuracy can become a problem. In a reduction involving millions of elements, a single-precision sum could lose several digits of accuracy. In deep learning, this could translate to small discrepancies in layer outputs or aggregated gradients if done naively in low precision.&lt;br /&gt;
&lt;br /&gt;
=== 3.2. Pairwise and Tree-Based Reductions ===&lt;br /&gt;
&lt;br /&gt;
A better approach to summation is &#039;&#039;&#039;pairwise summation&#039;&#039;&#039;, which sums numbers in a binary tree structure rather than linearly. The idea is to recursively split the array into halves, sum each half, then add the two partial sums. This can be implemented as:&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
By always adding numbers of similar magnitudes first (small subsets are reduced to subtotals, then those are added, etc.), pairwise summation avoids the situation of a very large running total absorbing small additions until the final steps. The error of pairwise summation grows logarithmically with $N$ in the worst case (roughly $O(\varepsilon \log_2 N)$)&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;, instead of linearly. Intuitively, at each level of the tree, rounding error is constrained, and there are about $\log_2 N$ levels. In fact, the worst-case relative error bound for pairwise summation (base case of 1 element) is:&lt;br /&gt;
&lt;br /&gt;
\[ |E_N| \le \frac{\varepsilon \log_2 N}{1 - \varepsilon \log_2 N} \sum_{i=1}^N |x_i|, \]&lt;br /&gt;
&lt;br /&gt;
which for practical $N$ simplifies to on the order of $\varepsilon \log_2 N$ times the condition number of the sum&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;. In typical random-error scenarios, the error behaves even better (on the order of $\sqrt{\log N}$)&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;. The key is that &#039;&#039;&#039;pairwise (tree) reduction balances the accumulations&#039;&#039;&#039;, preventing one sum from overwhelming the others too early.&lt;br /&gt;
&lt;br /&gt;
From a performance standpoint, pairwise summation has the same $O(N)$ total additions as linear summation, just a different order. It is also highly amenable to parallelization: independent partial sums can be computed in parallel at each level of the tree. Many numerical libraries and BLAS implementations use a form of blocked or pairwise summation in dot products for improved accuracy at negligible performance cost. For example, NumPy and Julia perform partial pairwise summation by default for better numerical behavior&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=== 3.3. Compensated Summation (Kahan, Neumaier) ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Compensated summation&#039;&#039;&#039; is an alternative technique to improve summation accuracy by tracking the small “lost” quantities. The most famous algorithm is the &#039;&#039;&#039;Kahan summation algorithm&#039;&#039;&#039; (also called Kahan–Babuška)&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. Kahan summation uses an extra variable to accumulate the round-off error. In pseudocode:&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Here &amp;lt;code&amp;gt;c&amp;lt;/code&amp;gt; accumulates the tiny leftovers that would otherwise be dropped&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. Each iteration essentially subtracts the last error from the new addend (&amp;lt;code&amp;gt;y = x[i] - c&amp;lt;/code&amp;gt;) so that &amp;lt;code&amp;gt;t = sum + y&amp;lt;/code&amp;gt; includes the small bits from the previous operation. The new error is then stored in &amp;lt;code&amp;gt;c&amp;lt;/code&amp;gt; for the next iteration. The result is that the final &amp;lt;code&amp;gt;sum&amp;lt;/code&amp;gt; is as if computed with higher precision than the machine’s: Kahan’s algorithm can &#039;&#039;&#039;greatly reduce the error&#039;&#039;&#039; in summing a sequence of floats. In fact, with exact arithmetic for the compensation, Kahan summation yields a result accurate to within 1 or 2 ULP of the true sum, regardless of N (assuming the sequence of partial sums doesn’t overflow)&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. More precisely, Kahan summation achieves a &#039;&#039;&#039;worst-case error independent of N&#039;&#039;&#039; (error bound depends only on machine precision, not the number of terms)&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. In practice, using a double accumulator for summing single-precision numbers via Kahan can give near double-precision accuracy.&lt;br /&gt;
&lt;br /&gt;
An important subtlety is that the compensation variable &amp;lt;code&amp;gt;c&amp;lt;/code&amp;gt; should be kept in &#039;&#039;full precision&#039;&#039;. In the implementation above, we used &amp;lt;code&amp;gt;double&amp;lt;/code&amp;gt; for both sum and c. If summing FP32 values, one might use &amp;lt;code&amp;gt;float sum&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;float c&amp;lt;/code&amp;gt; (both 32-bit), which still improves accuracy, but using a higher precision for the accumulator and compensation (i.e. double) yields even better results. Many implementations of Kahan summation use the same type for sum and c, but it&#039;s assumed to be higher precision than the inputs when possible.&lt;br /&gt;
&lt;br /&gt;
Kahan’s algorithm carries a performance cost: each iteration does five arithmetic operations (two adds, one subtract, etc.) and has a data dependency, which can hinder parallelism or pipelining&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. Thus, it runs slower than naive summation (roughly 2–3× slower or more, depending on CPU pipeline). In loops where summation is a bottleneck, this may be significant. Nonetheless, for modest N or where maximum accuracy is required, compensated summation is extremely useful. In fact, Python’s &amp;lt;code&amp;gt;math.fsum&amp;lt;/code&amp;gt; function uses a variant of Kahan’s algorithm (actually a more advanced multi-part compensation) to sum iterables with high precision.&lt;br /&gt;
&lt;br /&gt;
There are variations and improvements on basic Kahan summation. &#039;&#039;&#039;Neumaier’s algorithm&#039;&#039;&#039; (sometimes called &#039;&#039;Kahan-Babuška-Neumaier&#039;&#039;) improves Kahan’s method by also handling cases where the next input is larger in magnitude than the running sum&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. In such cases, the original Kahan formula could suffer error; Neumaier’s version effectively swaps roles when a huge value comes in. Its pseudocode is:&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
This algorithm ensures the compensation covers both scenarios: when the sum is the bigger addend or when the new term is bigger&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. In the end, it adds the final &amp;lt;code&amp;gt;compensation&amp;lt;/code&amp;gt; once to the result. Neumaier’s method can produce the correct result in cases where basic Kahan fails. For instance, summing [1.0, $+10^{100}$, 1.0, $-10^{100}$] in double precision: regular Kahan summation might give 0.0 (losing the small terms), whereas Neumaier’s algorithm yields 2.0 exactly&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. There are also higher-order compensation methods (carrying multiple compensation terms, e.g., the &#039;&#039;&#039;Klein summation&#039;&#039;&#039; variant&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt; or using arbitrary precision for intermediate sums) that further reduce error at increased computational cost&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
In summary, compensated summation algorithms trade extra computation for greatly improved numerical accuracy. They are especially valuable when summing large arrays of numbers with varying magnitudes, where naive summation could lose most of the low-order bits. Many scientific computing environments provide compensated sum routines for this reason. In deep learning, however, a different approach to improved precision is often taken: &#039;&#039;&#039;using a wider accumulator type&#039;&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
=== 3.4. Mixed-Precision Accumulation ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Mixed-precision accumulation&#039;&#039;&#039; refers to performing arithmetic in a higher precision than the input data. A common scenario is accumulating sums in 32-bit floats when the operands are 16-bit floats. Rather than implementing a compensation algorithm, one can simply use a wider accumulator so that intermediate rounding error is smaller. This is effectively what modern GPUs do for tensor operations: for example, NVIDIA &#039;&#039;&#039;Tensor Cores operate on FP16 input data with an FP32 accumulator&#039;&#039;&#039; (the 16-bit products are accumulated in 32-bit)&amp;lt;ref name=&amp;quot;ref6&amp;quot;/&amp;gt;. In this way, summing 2048 products in a dot product has far less precision loss than it would with a 16-bit accumulator. Hardware that supports mixed precision can give the best of both worlds: fast low-precision multiplies and high-precision accumulation.&lt;br /&gt;
&lt;br /&gt;
Software can also leverage mixed precision. For instance, one can sum an array of &amp;lt;code&amp;gt;float&amp;lt;/code&amp;gt; values into a &amp;lt;code&amp;gt;double&amp;lt;/code&amp;gt; variable, accumulating the result in double precision and then converting back to float at the end:&lt;br /&gt;
&lt;br /&gt;
This approach often yields a more accurate &amp;lt;code&amp;gt;result&amp;lt;/code&amp;gt; than summing in single precision directly, because the intermediate sum carries 52-bit precision. The final rounding to float happens only once at the end (minimizing overall rounding error). In fact, the above is one way to implement &amp;lt;code&amp;gt;math.fsum&amp;lt;/code&amp;gt; in Python (it internally does something similar with multiple partial sums in double precision).&lt;br /&gt;
&lt;br /&gt;
In machine learning training and inference, &#039;&#039;&#039;mixed precision&#039;&#039;&#039; is widely used. When using FP16 for speed, it&#039;s common to accumulate with FP32. For example, &#039;&#039;&#039;Google&#039;s TPUs use BFloat16 for multiplication but accumulate in FP32&#039;&#039;&#039; for matrix ops&amp;lt;ref name=&amp;quot;ref6&amp;quot;/&amp;gt;. AWS’s Inferentia (Neuron) hardware similarly supports FP16/BF16 matrix multiplication with FP32 accumulation&amp;lt;ref name=&amp;quot;ref7&amp;quot;/&amp;gt;. The motivation is that summing hundreds or thousands of half-precision values would otherwise introduce intolerable error or risk overflow. With an FP32 accumulator, the reduction error is drastically reduced (by about $2^{13}$ times, since FP32 has 13 more precision bits than FP16). Mixed-precision accumulation thus provides a simple and effective form of compensation: &#039;&#039;&#039;use a larger bucket to collect many small contributions&#039;&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
There is a spectrum of options: summing int8 or int16 integers might use 32-bit integers as accumulators for the same reason (to avoid overflow). Some BLAS libraries even offer &#039;&#039;extended precision accumulators&#039;&#039; for single precision dot products (accumulating in double). The downside of mixed precision is the extra memory and maybe throughput cost of the wider type. But on many platforms, the add latency is dominated by hardware design rather than bit-width, so using a 32-bit adder vs 16-bit has negligible speed impact if supported natively. The key is whether data movement or register pressure increases.&lt;br /&gt;
&lt;br /&gt;
In summary, &#039;&#039;&#039;using a higher precision accumulator is often the easiest way to improve numerical accuracy&#039;&#039;&#039; for reductions. It is a primary strategy in deep learning hardware to maintain stability while exploiting low precision for storage and initial computations.&lt;br /&gt;
&lt;br /&gt;
== Precision Formats in AI and Machine Learning ==&lt;br /&gt;
&lt;br /&gt;
Modern AI systems use a variety of numeric formats to balance speed, memory, and accuracy. We discuss common precisions and their roles:&lt;br /&gt;
&lt;br /&gt;
=== 4.1. FP32 and FP64 in Training and Inference ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;FP32 (single precision)&#039;&#039;&#039; has been the workhorse of deep learning for years. Most training scripts in frameworks like TensorFlow and PyTorch used FP32 by default, as it offers sufficient precision to propagate gradients and accumulate weight updates over millions of iterations without excessive error. With ~7 decimal digits of precision and a wide exponent range, FP32 can handle the dynamic range of activations and gradients in typical neural networks. In inference, FP32 is often more precision than strictly necessary for final predictions, but it has remained common because of its ease and the fact that many GPU operations were optimized for FP32.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;FP64 (double precision)&#039;&#039;&#039; is rarely used in deep learning model training or inference, except in specific scenarios requiring extreme precision. FP64 is common in scientific computing and certain types of numerical simulations. In neural networks, however, the extra precision is usually not needed for model accuracy—using doubles tends to yield negligible improvements in model metrics but at a large performance cost (since on GPUs FP64 throughput can be 1/2, 1/8, or even 1/32 of FP32 throughput depending on hardware). One exception might be when integrating neural nets into larger physics simulations or when solving ill-conditioned problems as part of the model. Some researchers use FP64 to diagnose numerical issues or ensure reproducibility. Generally though, deep learning has favored lower precision for efficiency, and FP64 is considered overkill for most networks&amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
That said, certain classical machine learning algorithms benefit from double precision. For example, computing the inverse of a matrix (as in a Gaussian Process regression or Kalman filter embedded in a model) can incur large rounding errors in single precision, so using FP64 can significantly improve the result&amp;lt;ref name=&amp;quot;ref1&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref1&amp;quot;/&amp;gt;. The ONNX standard supports a float64 tensor type for such needs. In ONNX Runtime, however, emphasis has been on FP32 performance, and some pipelines default to float even if the original model was double&amp;lt;ref name=&amp;quot;ref1&amp;quot;/&amp;gt;. If a model’s prediction computation involves operations that are numerically sensitive (like matrix inversion, Cholesky decomposition, or summing extremely large counts), then FP64 may be required to avoid noticeable discrepancies&amp;lt;ref name=&amp;quot;ref1&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref1&amp;quot;/&amp;gt;. A good practice is to test a model in both float and double if you suspect precision issues; significant differences indicate an ill-conditioned calculation that might need higher precision.&lt;br /&gt;
&lt;br /&gt;
In summary, FP32 remains the &#039;&#039;&#039;default&#039;&#039;&#039; format for most training and inference due to its balance of accuracy and efficiency&amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;, while FP64 is reserved for niche cases or precision-critical subroutines. FP32 typically achieves the same accuracy as FP64 for end-to-end deep learning tasks, because network training can often compensate for small noise, and other sources of error (like generalization error) dominate. Therefore, the community trend has been moving to &#039;&#039;&#039;lower&#039;&#039;&#039; precisions, not higher, to gain speed — provided the models still converge and perform well.&lt;br /&gt;
&lt;br /&gt;
=== 4.2. FP16 and BFloat16 in Deep Learning ===&lt;br /&gt;
&lt;br /&gt;
To further speed up neural network training and inference, &#039;&#039;&#039;half-precision (FP16)&#039;&#039;&#039; was adopted, especially with the advent of GPUs supporting FP16 math at twice the rate of FP32. Using FP16 reduces memory storage and bandwidth by 50% compared to FP32, which is significant for large models. However, FP16’s small exponent range and precision posed challenges: training a large network purely in FP16 can fail due to gradient underflow/overflow or weight updates not registering (because changes are below the 0.001 quantization step). To address this, frameworks introduced &#039;&#039;&#039;mixed-precision training&#039;&#039;&#039;, where compute-intensive parts (matrix multiplies, convolutions) run in FP16, but certain accumulations and model parameters remain in FP32. For instance, NVIDIA’s &#039;&#039;&#039;Tensor Core&#039;&#039;&#039; units require FP16 (or BF16) inputs but accumulate results in FP32, effectively ensuring that reductions are done in higher precision&amp;lt;ref name=&amp;quot;ref6&amp;quot;/&amp;gt;. Research showed that with proper loss scaling and mixed precision, models can train to FP32-level accuracy using mostly FP16 arithmetic, achieving significant speedups.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;BFloat16 (BF16)&#039;&#039;&#039; emerged as an alternative 16-bit format primarily from Google’s TPU training systems. BF16 has a larger exponent (8 bits, same as FP32) but only 7 fraction bits&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. This means BF16 can handle the &#039;&#039;&#039;same dynamic range&#039;&#039;&#039; as FP32 (no overflows where FP32 wouldn’t) but at the cost of even lower precision than FP16 (BF16 has about 2–3 decimal digits of precision vs FP16’s ~3–4). Despite its coarse precision, BF16 turns out to be effective for training deep networks because the algorithms (stochastic gradient descent, etc.) are somewhat robust to that noise, and having the wide range prevents catastrophic loss of signal (no gradient will overflow to Inf unless it was going to in FP32 anyway). Many training frameworks (PyTorch, TensorFlow) support BF16 training on hardware like &#039;&#039;&#039;TPU, NVIDIA A100+, Intel CPUs with AVX-512 BF16, etc&#039;&#039;&#039;. Typically, when using BF16, &#039;&#039;&#039;accumulations are still done in FP32&#039;&#039;&#039; (for example, a TPU v2/v3 multiplies BF16 matrices and accumulates into an FP32 result for the dot product&amp;lt;ref name=&amp;quot;ref6&amp;quot;/&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
In inference, both FP16 and BF16 are used to deploy models for faster speed or lower memory. FP16 is popular on NVIDIA GPUs (which have specialized FP16 units). BF16 is advantageous on TPUs and some CPUs that support it, and increasingly on GPUs (Ampere and newer GPUs also support BF16 arithmetic). The choice between FP16 and BF16 can depend on hardware: BF16’s advantage is easier software conversion (you can often take an FP32-trained model and cast weights to BF16 with minimal loss, since the dynamic range is intact), whereas FP16 might require adjusting the model or ensuring values fit in range (sometimes needing techniques like maintaining a master copy of weights in FP32 during training, or handling out-of-range values).&lt;br /&gt;
&lt;br /&gt;
For ONNX, both Float16 and BFloat16 types are part of the standard. ONNX models can be converted to FP16 (truncating weights and inserting cast nodes for computations) using tools like &amp;lt;code&amp;gt;float16_converter&amp;lt;/code&amp;gt; in ONNX Runtime&amp;lt;ref name=&amp;quot;ref8&amp;quot;/&amp;gt;. ONNX Runtime on GPUs will typically execute those FP16 ops with mixed precision on tensor cores (FP16 input, FP32 accumulate), whereas on CPU it may emulate FP16 or upcast to FP32 (as many CPU implementations do not natively support half-precision math)&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;. In fact, as an example, running an ONNX model in FP16 on a CPU might insert hidden Cast operations to FP32 behind the scenes, because the CPU kernel for that op only exists in FP32&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;. This ensures correctness (the computation is done in higher precision) but can reduce performance. We will revisit such backend-dependent behavior in Section 5.3.&lt;br /&gt;
&lt;br /&gt;
In summary, &#039;&#039;&#039;FP16 and BF16&#039;&#039;&#039; are now established as important low-precision formats in deep learning. FP16 offers more precision bits, which can matter for e.g. small differences, but has a limited range (care needed to avoid overflow). BF16 offers safety in range but less precision (which can slightly affect convergence or final accuracy in some cases). Both typically rely on FP32 for accumulation to achieve acceptable accuracy. When deploying models, one should choose a format supported efficiently by the target hardware and verify that the accuracy is not significantly degraded. Often, CNNs and transformers can run in FP16/BF16 with virtually no drop in score, but certain operations (like softmax, see Section 6.3) might need special handling.&lt;br /&gt;
&lt;br /&gt;
=== 4.3. FP8 Formats (E4M3, E5M2) and Scaling ===&lt;br /&gt;
&lt;br /&gt;
The push for ever lower precision has led to experimental &#039;&#039;&#039;8-bit floating point (FP8)&#039;&#039;&#039; formats for deep learning. In 2022, researchers from NVIDIA, Intel, and Arm proposed an FP8 standard with two variants: &#039;&#039;&#039;E4M3&#039;&#039;&#039; (4 exponent bits, 3 fraction bits) and &#039;&#039;&#039;E5M2&#039;&#039;&#039; (5 exponent bits, 2 fraction bits)&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;. Both have 1 sign bit, for a total of 8 bits. E5M2 has a wider exponent range (5 bits gives bias 15) but one fewer mantissa bit than E4M3. The idea is to use E4M3 for numbers that need more precision (like weights or activations around 1.0) and E5M2 for numbers that need more dynamic range (like gradient updates which can be very small or large). In fact, the study found that using E4M3 for forward activations/weights and E5M2 for backward gradients can allow training of large networks with FP8 to accuracy on par with FP16 training&amp;lt;ref name=&amp;quot;ref11&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
FP8 formats are &#039;&#039;not&#039;&#039; IEEE 754 standard (though IEEE is considering lower precision in its 2019 revision). Notably, the E4M3 format as used by NVIDIA does not allocate any bit-pattern for $+\infty$ or $-\infty$ – it has only NaN for invalid results, using that freed bit pattern to extend the representable range by one exponent value&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;. Essentially, E4M3 can represent numbers up to ~$2^{15}$ (around 3e4) instead of capping at infinity at $2^{16}$, by foregoing infinities&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;. Variations also exist regarding negative zero and how NaNs are handled&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;. For instance, GraphCore’s IPU uses FP8 formats that disallow negative zero and infinities, to maximize useable range&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Using 8-bit floats for neural networks requires additional techniques, chiefly &#039;&#039;&#039;scaling&#039;&#039;&#039;. Because 8-bit has very limited precision and range, one typically applies a power-of-two scale factor to groups of values to normalize them into an FP8-friendly range. This is akin to block floating-point or “microscaling” per layer or per tensor. For example, one might keep a FP32 scale for each channel or tensor, and represent values as $(FP8\_value) \times 2^{scale}$. The ONNX standard has introduced support for FP8 in version 1.15, including types for E4M3 and E5M2 (with variations FNUZ: finite-only and no negative zero)&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;, and even a type called &#039;&#039;&#039;E8M0&#039;&#039;&#039; which is an 8-bit exponent-only number used purely as a scale factor&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;. The E8M0 type has 8 exponent bits, no mantissa, effectively representing powers of two from $2^{-127}$ to $2^{128}$ for scaling purposes&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;. This indicates an approach where an FP8 tensor would be accompanied by an E8M0 scale (or scales) to interpret its value correctly.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Rounding&#039;&#039;&#039; becomes critical when casting values to FP8. The quantization error is much larger relative to FP16 or FP32. Studies have shown that choosing the right rounding mode (e.g., round-to-nearest, stochastic rounding, etc.) and clamping behavior (saturating to max instead of inf) can affect model accuracy&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;. ONNX added a &amp;lt;code&amp;gt;round_mode&amp;lt;/code&amp;gt; attribute to its Cast operator to allow controlling this for FP8&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;. In particular, one paper found that &#039;&#039;&#039;rounding up with saturation&#039;&#039;&#039; (round-to-nearest ties away from 0, and clamp overflows to max finite rather than inf) gave better accuracy in large language model training than other rounding modes&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
At present, FP8 is on the cutting edge. NVIDIA’s H100 GPU supports FP8 tensor cores (for both E4M3 and E5M2), and early results show minimal accuracy loss on some vision and NLP tasks when properly tuned. However, using FP8 requires fine-tuning hyperparameters like scaling factors for each layer and perhaps tweaking training procedures. ONNX’s inclusion of FP8 types means you can represent and transport models using FP8 weights or activations, but actual hardware support is limited and typically paired with calibration or dynamic scaling logic outside of ONNX graph (or using custom ONNX functions for scaling). It remains an &#039;&#039;&#039;open research area&#039;&#039;&#039; how to fully automate FP8 quantization for arbitrary models without accuracy loss.&lt;br /&gt;
&lt;br /&gt;
In summary, FP8 formats promise further compression of neural network computation and data. They are an order of magnitude more error-prone than FP16, but when combined with careful scaling, mixed usage of E4M3/E5M2, and maybe modified training techniques, they have shown surprising success. ONNX is actively evolving to support these ultra-low precision types, signaling their potential importance in future AI inference deployments.&lt;br /&gt;
&lt;br /&gt;
== Accumulation Semantics in ONNX ==&lt;br /&gt;
&lt;br /&gt;
One of the roles of the ONNX specification is to define the mathematical behavior of operators (Conv, MatMul, Sum, etc.) in a framework-agnostic way. However, ONNX mostly defines &#039;&#039;what&#039;&#039; an operator computes in exact arithmetic, not &#039;&#039;how&#039;&#039; the computation is carried out in finite precision. This leaves certain details – like accumulator precision or summation order – up to the implementation (runtime or hardware). We&#039;ll examine how ONNX treats accumulation and what guarantees (or lack thereof) are provided.&lt;br /&gt;
&lt;br /&gt;
=== 5.1. Operator Definitions and Numerical Guarantees ===&lt;br /&gt;
&lt;br /&gt;
Each ONNX operator is described as performing an ideal mathematical function on its inputs. For example, &amp;lt;code&amp;gt;MatMul&amp;lt;/code&amp;gt; is defined as the standard matrix product of two tensors, and &amp;lt;code&amp;gt;ReduceSum&amp;lt;/code&amp;gt; computes a summation along given axes. The ONNX spec, as of current versions, &#039;&#039;&#039;does not mandate the precision of intermediate calculations&#039;&#039;&#039; beyond the types of the inputs and outputs. If an operator’s inputs and outputs are float16, the spec implies the computation is done in float16 in a sense, but it does not forbid an implementation from using higher precision internally – it simply doesn&#039;t mention it. There is &#039;&#039;&#039;no explicit guarantee of identical bit-for-bit results&#039;&#039;&#039; across different ONNX backends for floating-point ops. Instead, results are expected to be numerically close, within tolerances one would normally expect from floating-point differences (due to different hardware or algorithms). The ONNX Backend Test suite typically uses a relative or absolute error tolerance when validating outputs for this reason.&lt;br /&gt;
&lt;br /&gt;
Because of this, important details like the use of extended precision or accumulation in higher precision are left ambiguous. This has led to discussions in the ONNX community about making such behavior more explicit. For example, a proposal was raised to allow specification of accumulation precision for certain ops like MatMul&amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;. The reasoning is that as lower-precision types (float16, bfloat16, float8) become more common, knowing or controlling whether an op internally accumulates in a higher precision is important for both accuracy and reproducibility&amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;. In June 2025, an issue on the ONNX GitHub suggested adding an attribute to MatMul (and similarly Gemm, Softmax, etc.) to specify the accumulator type&amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;. By default it would use input precision (status quo), but an attribute could request (for instance) 32-bit accumulation even if inputs are 16-bit&amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;. As of this writing, however, no such attribute exists in the official spec – it&#039;s an open issue. Thus, the numerical semantics are effectively: &#039;&#039;“compute the result as if in the same type as inputs, but actual implementations may use more precision internally as an optimization.”&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
From a guarantee standpoint, ONNX operators promise the mathematically correct result assuming real-number arithmetic, but they &#039;&#039;&#039;do not guarantee bit-exact results&#039;&#039;&#039; due to floating-point effects. There is an understanding that differences can arise from different summation orders, fused operations (e.g., an implementation might use an FMA (fused multiply-add) which has one rounding instead of two), or use of extended precision registers. ONNX does not formally define rounding modes either – it relies on IEEE 754 defaults, but if a backend used something like deterministic summation or alternative rounding, ONNX has no mechanism to express that.&lt;br /&gt;
&lt;br /&gt;
In summary, ONNX&#039;s current stance is largely &#039;&#039;&#039;implementation-dependent&#039;&#039;&#039; for numerical precision. It provides minimal explicit numerical guarantees beyond type constraints. This is why an ONNX model run on two different hardware or runtime libraries can have slightly different results, especially in reduced precision. The burden is on backends to produce results &amp;quot;close enough&amp;quot; to each other that they don&#039;t affect the model&#039;s decisions in any significant way.&lt;br /&gt;
&lt;br /&gt;
=== 5.2. MatMul, Conv, and Reduction Operators ===&lt;br /&gt;
&lt;br /&gt;
Consider ONNX &amp;lt;code&amp;gt;MatMul&amp;lt;/code&amp;gt;: it takes two tensors (often two matrices) and produces their matrix product. In pure math, $C_{ij} = \sum_k A_{ik} &#039;&#039; B_{kj}$. If A and B are float16, should that sum be computed in float16 or float32? As discussed, ONNX doesn’t specify – it simply says the output is a float16 matrix containing the product. In practice, many backends will &#039;&#039;promote&#039;&#039; the computation: for example, NVIDIA GPUs will do the multiplication and addition in a mixed FP16/FP32 mode by default (because of tensor cores), then convert the final result back to float16. ONNX Runtime on CPU might not support float16 multiplication natively, so it will insert cast ops to float32, perform the MatMul in float32, then cast back to float16&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;. From ONNX’s perspective, as long as the final result is within acceptable error of the “ideal” float16 computation, it’s fine. But note, an “ideal” float16 computation (where every multiply and add is rounded to FP16) might actually be &#039;&#039;less accurate* than what the backend did with FP32. This means that a backend using higher precision can produce a result that is not bit-equal to a pure FP16 implementation – it&#039;s actually closer to the true real number result. This is usually considered a good thing (improved accuracy), but it does highlight a lack of consistency: another backend might do everything in pure FP16 and get a slightly different answer.&lt;br /&gt;
&lt;br /&gt;
The same situation occurs for &#039;&#039;&#039;convolution&#039;&#039;&#039; (&amp;lt;code&amp;gt;Conv&amp;lt;/code&amp;gt; op) which is essentially a batched sum of products (like MatMul in sliding window), and for explicit reduction ops like &amp;lt;code&amp;gt;ReduceSum&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;ReduceMean&amp;lt;/code&amp;gt;, etc. If you have an ONNX &amp;lt;code&amp;gt;ReduceSum&amp;lt;/code&amp;gt; that sums 1000 float16 values, the spec doesn’t say how to sum them. A GPU kernel might sum in a tree using FP32 accumulators; a naive implementation might sum in order with FP16 and possibly overflow or underflow intermediate steps. Both are allowed as long as they output a float16 result that is “reasonable.” In practice, ONNX Runtime and others take care to avoid obviously bad behavior (like overflow) by using higher precision or by splitting the sum. But again, it’s not mandated by the ONNX standard explicitly.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Softmax&#039;&#039;&#039; is an interesting case: Softmax involves an internal sum of exponentials. ONNX defines the Softmax operator as $$\mathrm{Softmax}(x)_i = \frac{\exp(x_i)}{\sum_j \exp(x_j)},$$ typically applied along a specified axis. Directly computing this in float16 can be problematic for large vectors, as discussed in Section 6.3. Indeed, the issue of accumulation precision arises: should the sum $\sum_j \exp(x_j)$ be done in FP16 or can it be in FP32? Many implementations will do it in FP32 even if $x$ is FP16, because the dynamic range of $\exp(x)$ is large. The ONNX Softmax spec doesn’t detail this, but it expects the result to be a proper probability distribution (summing to 1.0 within normal floating error). A pure FP16 softmax on, say, 1000 elements could sum to something like 0.996 or 1.004 due to rounding, whereas FP32 accumulation would be much closer to 1.000. ONNX likely allows both; the small difference is usually inconsequential, but if it were large (say the FP16 sum overflowed to inf), that would be a correctness issue. A robust backend will prevent that (by using the common max-subtraction trick and higher precision accumulation). The ONNX issue 7072 indeed calls out Softmax as another op where explicit control of accumulation precision would be useful&amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
In summary, &#039;&#039;&#039;MatMul, Conv, and reduction ops&#039;&#039;&#039; in ONNX are defined mathematically but with &#039;&#039;implicit&#039;&#039; numerical behavior. Implementations commonly use higher precision for intermediate sums especially when input is low precision, but this is not standardized. Consequently, results can differ slightly between implementations. For critical applications, one must be aware of these differences. A model exported to ONNX with float16 weights might perform slightly differently on one runtime vs another if one does all float16 math and the other uses float32 accumulations. Both conform to ONNX. As a user, if you require a certain precision for accumulation, currently you have to ensure it by the way you export or run the model (for example, insert manual cast nodes in the ONNX graph to force certain ops to FP32). We may see future ONNX versions address this with attributes or metadata as mentioned, but for now these ops are effectively “compute with best effort accuracy”.&lt;br /&gt;
&lt;br /&gt;
=== 5.3. Backend and Hardware-Dependent Behavior ===&lt;br /&gt;
&lt;br /&gt;
Because ONNX is implemented by many backends (ONNX Runtime, TensorRT, CoreML, PyTorch JIT, etc.), and these in turn run on varied hardware (CPU, GPU, accelerators), the numeric behavior can be backend-dependent. Some examples:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;CPU backends:&#039;&#039;&#039; Many CPUs do not have native float16 vector arithmetic (with a few exceptions in recent CPUs). ONNX Runtime’s default CPU execution provider will often upcast float16 to float32, compute, then downcast&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;. This ensures accuracy but means that if you thought you were getting a full float16 pipeline, you’re not – the CPU is essentially ignoring the low precision in compute. On the other hand, x86 CPUs have extended 80-bit precision in x87 registers (though less used now) and 64-bit SIMD registers for doubles; some compiler optimizations might carry intermediate results in higher precision. This could make CPU results slightly different from a GPU’s if, say, a sum is done in 80-bit on CPU versus 32-bit on GPU. Generally, modern compilers avoid such extended precision unless explicitly asked, to improve reproducibility.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;GPU backends:&#039;&#039;&#039; GPUs often perform summations in parallel using tree reductions. The order of operations can change depending on how threads are scheduled. This means even on the same GPU, summing an array in one launch versus another might not give bit-identical results if the internal parallel reduction algorithm differs (though it should be deterministically the same given the same library version and input, but if you change GPU model or BLAS library, it might differ). Also, GPUs have fused multiply-add (FMA) instructions that do $(a*b + c)$ in one step with one rounding, which can produce slightly different results than doing the multiply and add separately (which would round twice). If an ONNX backend uses a fused kernel (like a GEMM library) it might yield different low-order bits than a simple loop.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Vendor libraries:&#039;&#039;&#039; ONNX Runtime can use vendor BLAS or DNN libraries (MKL, cuDNN, TensorRT, etc.). These libraries often implement their own strategies for numerical stability. For example, NVIDIA’s cuDNN might use FP32 accumulation for FP16 inputs by default. TensorRT (NVIDIA’s inference engine) will sometimes automatically promote precisions or insert scaling to maintain stability&amp;lt;ref name=&amp;quot;ref13&amp;quot;/&amp;gt;. On the other hand, if you explicitly request FP16 in TensorRT with no FP32 fallback, it might do the entire op in FP16 and you’d see more error. Some libraries offer knobs: TensorRT allows choosing FP32, FP16, or INT8 precision for each layer, and will use FP32 internally for safety in some cases unless forced otherwise.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Hardware-specific quirks:&#039;&#039;&#039; As another example, NVIDIA’s &#039;&#039;&#039;TensorFloat-32 (TF32)&#039;&#039;&#039; on Ampere GPUs is a precision mode where matrix multiplications use 10-bit mantissa (so about FP16 precision for the multiply) but keep 8-bit exponent (range like FP32). By default, CUDA libraries use TF32 for FP32 matrix multiplies on Ampere to speed them up, unless you disable it. This means if you run an ONNX model with FP32 on an A100 GPU, your matmuls might actually be lower precision than FP32 (though higher than FP16)&amp;lt;ref name=&amp;quot;ref6&amp;quot;/&amp;gt;. It’s an example of hardware choosing an intermediate precision for performance. From ONNX’s perspective, the output is still FP32 and (hopefully) within error tolerances, but a user might be surprised that it’s not a “true” FP32 computation internally. If exactness is needed, one has to turn off TF32.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Non-determinism:&#039;&#039;&#039; Some backends (especially on GPUs) allow non-deterministic execution for performance. For instance, atomic adds in parallel reductions can produce results that vary run-to-run because the order of accumulation is race-condition-dependent. ONNX’s spec doesn’t cover this explicitly, but it’s something to consider. In ONNX Runtime, there are settings for deterministic computing, but by default it may use all available performance optimizations, even if results vary at the 1e-7 level.&lt;br /&gt;
&lt;br /&gt;
In short, the numerical behavior under ONNX can depend on the combination of backend software and hardware capabilities. The lack of a strict spec for accumulation means implementations have freedom to use clever strategies (higher precision, reordering) to improve accuracy or speed. Usually this is positive (e.g. getting better accuracy than a naive approach). But it could also lead to slight inconsistencies. For example, you might find that exporting a PyTorch model to ONNX and running it with one engine yields outputs slightly different from PyTorch’s – not because ONNX is wrong, but because the summation or convolution was done differently (maybe PyTorch summed in FP32 as well, but maybe not in exactly the same way).&lt;br /&gt;
&lt;br /&gt;
The key takeaway is that ONNX &#039;&#039;by design&#039;&#039; abstracts away these details, which is good for portability but means you need to &#039;&#039;&#039;trust the backend&#039;&#039;&#039; or verify its precision behavior for sensitive models. If necessary, you can enforce certain behavior by inserting ops (for example, if you want to ensure FP32 accumulation, you could insert a cast to FP32 for inputs of a MatMul and cast the result back to FP16 – this way the ONNX graph itself mandates FP32 compute in between). This of course sacrifices some performance or device-specific magic.&lt;br /&gt;
&lt;br /&gt;
Looking forward, as mentioned, there are proposals to enrich ONNX’s specification regarding precision. This might include explicit flags for “exact summation” or “accumulate in int32” etc., which would make models more self-descriptive but also potentially less portable (if a backend doesn’t support that, what then?). For now, users should be aware of these backend differences. Most of the time, well-engineered ONNX runtimes yield results close enough that they don&#039;t affect the application (e.g., classification top-1 remains the same even if low-level sums differ by 1e-5). But for high-stakes numerical computations, testing and maybe adjusting the model or backend settings is prudent.&lt;br /&gt;
&lt;br /&gt;
== Numerical Stability in Deep Learning Models ==&lt;br /&gt;
&lt;br /&gt;
Deep learning computations involve many operations that, while theoretically well-defined, can be sensitive to numerical issues when implemented in finite precision. We&#039;ll discuss a few common scenarios in neural networks where numerical stability is a concern, and how precision and accumulation play a role.&lt;br /&gt;
&lt;br /&gt;
=== 6.1. Dot Products and Large-Scale Reductions ===&lt;br /&gt;
&lt;br /&gt;
Neural networks are full of dot products: every fully-connected layer computes $\mathbf{y} = W\mathbf{x}$, which is a series of inner products between weight vectors and the input. Convolutional layers compute weighted sums of local regions. Attention mechanisms compute large matrix products of query/key/value matrices. All these boil down to summing a lot of terms: $\sum_{k=1}^K a_k b_k$. When $K$ is large (it could be in the thousands or more for a single neuron in a fully connected layer of a large model), the potential for floating-point error in that sum increases.&lt;br /&gt;
&lt;br /&gt;
If using FP32, the error may still be negligible in many cases, but in lower precision like FP16, it can become significant. For example, consider a dot product of length 2048 in FP16. Each product is in FP16 (with ~3 decimal digits precision) and if accumulated in FP16, the worst-case error could be on the order of $2048 \times \varepsilon_{FP16} \approx 2048 \times 0.0009766 \approx 2$ (i.e., you could lose a couple units in the third decimal place relative accuracy in worst case). Even in random error accumulation, you might see error on the order of $\sqrt{2048} \times 0.0009766 \approx 0.044$ or 4.4% (though this is a rough estimate). That’s quite significant. Fortunately, as discussed, most hardware will accumulate such sums in a higher precision like FP32, dramatically reducing that error (~7 orders of magnitude smaller $\varepsilon$). But if it didn’t, the network’s outputs could get noisy. A fully-connected layer with many inputs might output slightly different results depending on summation order if done in low precision.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Large-scale reductions&#039;&#039;&#039; also occur in operations like global average pooling (summing all elements of a feature map) or computing the total loss by summing loss over batch samples. If one sums 100,000 values in FP32, the worst-case error could be on the order of 100,000 &#039;&#039; 1e-7 ≈ 1e-2 (1% uncertainty), though typical error would be much less. In FP16, summing 100k values is likely impossible without overflow: as noted, adding ~10^4 moderate numbers can overflow FP16&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. So large reductions &#039;&#039;must* be handled carefully (either in chunks or in higher precision). Frameworks generally do: for example, when performing a sum reduction on GPU, libraries break it into parallel blocks that accumulate in registers (usually 32-bit registers, even if inputs are 16-bit). &lt;br /&gt;
&lt;br /&gt;
Another aspect is that many dot products in neural nets involve values of varying magnitude. If the distribution of terms is such that some are much larger than others, the smaller terms might not contribute much if summed last. Ideally, one might sort by magnitude or use pairwise summation to improve that. Most BLAS libraries effectively do a pairwise strategy due to their parallel structure. So, while worst-case analyses are grim, practical implementations mitigate a lot of it.&lt;br /&gt;
&lt;br /&gt;
However, certain layers can suffer if not handled properly. One famous example is in RNNs or LSTMs: if one uses FP16 naive summation for the gating mechanism, errors could accumulate over many time steps and cause drift or instability. In modern practice, recurrent layers are often run in FP16 with FP32 accumulators to avoid this.&lt;br /&gt;
&lt;br /&gt;
In summary, &#039;&#039;&#039;dot products and large reductions need sufficient precision&#039;&#039;&#039; or robust algorithms. If you are implementing a custom kernel (say a custom ONNX op) and you have to sum a huge array, you should consider using a technique from Section 3 (pairwise summation, Kahan, or at least double accumulator if possible). Many deep learning computations, by virtue of hardware and library design, already do something like this under the hood. But it&#039;s always good to be aware, especially if pushing into lower precision territories.&lt;br /&gt;
&lt;br /&gt;
=== 6.2. Cancellation, Dynamic Range, and Conditioning ===&lt;br /&gt;
&lt;br /&gt;
We introduced &#039;&#039;cancellation&#039;&#039; earlier in the context of summation. In deep learning, cancellation can appear in a few places. One is in the computation of certain norms or variances. For instance, Batch Normalization computes mean and variance of activations. The variance formula $\mathrm{Var}(x) = E[x^2] - (E[x])^2$ is subject to catastrophic cancellation if $E[x]$ and $\sqrt{E[x^2]}$ are close in magnitude (which can happen if the variance is small compared to the mean). If implemented naively in single precision, you might get a negative variance due to numerical error (which would then be clamped to zero or sqrt would yield NaN). Robust implementations compute variance in a single pass or increase precision. Indeed, in many frameworks, batchnorm accumulation is done in FP32 even if the activations are FP16, precisely to avoid numerical issues in computing mean and variance. ONNX itself doesn’t specify this, but an ONNX backend is likely to do batchnorm stats in higher precision for safety. The &#039;&#039;conditioning&#039;&#039; of the variance calculation is poor when variance is tiny relative to mean, so it&#039;s a spot where higher precision is needed.&lt;br /&gt;
&lt;br /&gt;
Another example is in certain loss functions or regularizations. If you subtract two large numbers to get a small difference, you lose precision. Say you have two nearly identical predictions and you subtract them as part of some operation – if done in float16, you might get zero whereas float32 would have a slight difference. This could affect, say, a gradient if that difference was supposed to propagate information. Fortunately, neural nets often avoid directly subtracting large nearly equal numbers; but it can happen. In attention mechanisms, the &#039;&#039;score normalization&#039;&#039; (softmax) subtracts a max to avoid huge exponentials, which is good for range but if multiple scores are close to the max, you subtract numbers that are close. However, those subtractions are not catastrophic in the same way; they&#039;re intentional to reduce range and are typically fine in FP32 or even FP16.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Dynamic range&#039;&#039;&#039; issues are everywhere in deep learning. Activation functions like ReLU and exponential can produce very large or very small values. If subsequent computations don’t account for that, you get infinities or zeros. For example, consider a network output that produces a logit of 100 in FP16. $\exp(100)$ in FP16 overflows to infinity (since FP16 max ~ 6e4 and $e^{100} \sim 3.7e43$). If you then do softmax, that one infinity will dominate the sum and you might get a probability of 1 for that class (or NaNs if not handled). The standard softmax implementation subtracts the max logit, so in this case it would subtract 100 from all logits, making the largest exponent $\exp(0)=1$ and others tiny – this avoids overflow. But what about underflow? If you had a very negative logit, $\exp(-100)$ in FP16 would underflow to 0. In softmax, that just contributes ~0 probability, which is fine (it effectively is zero anyway). Underflow tends to be less dangerous than overflow in inference — underflow usually just means “this probability/neuron is effectively 0,” whereas overflow can poison the results with NaNs or Infs. &lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Conditioning&#039;&#039;&#039; refers to how sensitive a function is to input perturbations. Neural networks themselves as functions can be poorly conditioned (e.g., tiny changes in weights cause big changes in output for some architectures), but typically they are designed to be reasonably well-behaved. Certain internal computations like matrix inversion (if one does a pseudoinverse or something for a layer) can be extremely ill-conditioned if the matrix is near singular. That again would need double precision perhaps. In ML, one common ill-conditioned operation is the &#039;&#039;&#039;softmax denominator&#039;&#039;&#039;: $\sum_j e^{x_j}$ can be very large and also very sensitive to changes in the largest $x_j$. However, the softmax is more stable if computed carefully (max trick). Another is &#039;&#039;&#039;normalization&#039;&#039;&#039;: computing $\frac{x - \mu}{\sigma}$ for layer norm or batch norm – if $\sigma$ is very small, then the division blows up noise. In those cases, a small $\epsilon$ is usually added to $\sigma^2$ to prevent instability (this is a model-level fix, not a numeric one per se, but it acknowledges numeric limits).&lt;br /&gt;
&lt;br /&gt;
In training, &#039;&#039;&#039;optimizers&#039;&#039;&#039; can suffer from precision issues. For example, the Adam optimizer keeps a moving average of squared gradients; in half precision, these small updates might flush to zero or not change at some point. Some research found that certain adaptive optimizers break in FP16 because the small differences are lost (&amp;quot;Why FP16 training breaks NAdam&amp;quot; etc.). The solution is often to keep the optimizer states in FP32 while doing the bulk of compute in FP16. This again is a form of mixed precision.&lt;br /&gt;
&lt;br /&gt;
From an inference perspective, once the model is trained, the main concerns are avoiding overflow in activations and ensuring summations (like in softmax or reductions) are done with enough precision. The model’s weights are fixed, so we don&#039;t have to worry about cumulative error over iterations as in training, just the forward pass.&lt;br /&gt;
&lt;br /&gt;
=== 6.3. Normalization Layers and Softmax ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Normalization layers&#039;&#039;&#039; (BatchNorm, LayerNorm, etc.) and &#039;&#039;&#039;Softmax&#039;&#039;&#039; are specifically worth examining for numerical stability:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Batch Normalization:&#039;&#039;&#039; During inference, batchnorm applies an affine transform using precomputed mean and variance (from training). It’s basically: $y = \frac{x - \mu}{\sqrt{\sigma^2 + \epsilon}} \gamma + \beta$. This is a stable operation as long as $\epsilon$ is provided (to avoid division by zero). Since $\mu, \sigma^2$ are constants in inference, it reduces to linear ops which are fine. The main numerical work (computing mean/variance) happened during training or model calibration, and frameworks usually did that in FP32. So batchnorm at inference time is not a source of instability (it’s just a scale and shift of the data).&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Layer Normalization / Group Normalization:&#039;&#039;&#039; These compute mean/var on the fly for each sample (and possibly channel group). In inference, they still need to compute these statistics for each forward pass. If implemented in low precision, the variance calculation can suffer cancellation as mentioned. However, it&#039;s likely that inference engines compute the mean and variance in FP32 even if inputs are FP16 (especially since these are per-sample operations, not huge batch reductions – it&#039;s feasible to do in higher precision). If someone implemented layer norm purely in FP16, they might see issues when the variance is very small. Usually there’s an $\epsilon$ added which might dominate a tiny variance and thus mitigate catastrophic cancellation (the result might just output something mostly from $\epsilon$). In summary, normalization layers are usually fine as long as the implementation uses enough precision for the summation of squares.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Softmax:&#039;&#039;&#039; Softmax is notorious if done improperly. The safe algorithm: subtract the maximum input from all inputs (to bring the largest exponent to 1.0), exponentiate, sum them up, divide each exponent by the sum. The max subtraction handles overflow in $\exp()$. But what about the sum? As discussed, if there are $N$ elements in the softmax, the sum is at most $N$ (when all inputs equal the max). In classification, $N$ might be the number of classes. For ImageNet, $N=1000$, so worst sum ~1000 (which is fine in FP16, as 1000 &amp;lt;&amp;lt; 65504). For an extreme case like a language model, $N$ could be 50,000 (vocab size). Sum could be 50,000 in worst case (everyone equal probability). 50,000 is still below 65504, so actually an FP16 sum would &#039;&#039;not&#039;&#039; overflow in that scenario, and the rounding error on a sum of 50k identical terms would be maybe a few ULPs (which at 50k magnitude might be ~0.5 difference worst-case). So maybe softmax summation in FP16 is not catastrophic for output probabilities – you’d get essentially 1.0 after normalization instead of 0.99999 in some case. Usually fine.&lt;br /&gt;
&lt;br /&gt;
:The bigger issue is if one or a few terms dominate the softmax. If one term is much larger, the others underflow to 0 after the max subtraction. That’s fine: you get a one-hot distribution essentially. If the model expected a softer distribution, that could be a problem. But if the logits were really that far apart, even FP32 would give nearly one-hot. So not really a precision issue, more of a model property.&lt;br /&gt;
&lt;br /&gt;
:However, the &#039;&#039;&#039;stability during training&#039;&#039;&#039; is a big issue. The GitHub thread we saw (ColossalAI) mentioned training a 130B model where FP16 softmax in attention caused training to occasionally blow up&amp;lt;ref name=&amp;quot;ref14&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref14&amp;quot;/&amp;gt;. They observed attention scores in some heads ranging up to 1e4 or as low as 1e-3&amp;lt;ref name=&amp;quot;ref14&amp;quot;/&amp;gt;. 1e4 exponent in FP16 is definitely Inf (because $\exp(10^4)$ is astronomically huge, but even exp(100) is inf in half). The method CogView introduced (Precision Bottleneck Relaxation) involved subtracting the max &#039;&#039;per head&#039;&#039; to reduce range&amp;lt;ref name=&amp;quot;ref14&amp;quot;/&amp;gt;—which is basically the softmax max trick at a finer granularity or something similar. In the end, they found &#039;&#039;&#039;the simplest fix was to compute softmax in FP32&#039;&#039;&#039; for those layers&amp;lt;ref name=&amp;quot;ref14&amp;quot;/&amp;gt;. They reported that it had negligible speed impact but significantly improved stability&amp;lt;ref name=&amp;quot;ref14&amp;quot;/&amp;gt;. This highlights a general principle: certain operations like softmax (and sometimes matrix multiplications in attention) benefit from higher precision, and using FP32 in a few critical spots can save a mixed-precision training from crashing. In inference, using FP32 for softmax is also a safeguard if one is uncertain. It costs little because softmax is not a heavy compute compared to matrix multiplies.&lt;br /&gt;
&lt;br /&gt;
:ONNX does have a Softmax operator; if the input is float16, a backend might do exactly what they described: internally cast to float32 for the softmax computation, then cast output back to float16. Some inference engines (like DeepSpeed’s inference or others) keep attention softmax in FP32 even if rest is FP16, for robustness&amp;lt;ref name=&amp;quot;ref14&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Apart from softmax, other places where exponentials occur are in loss functions like cross-entropy (which uses log-softmax internally), or in activation functions like Sigmoid and Tanh (which are basically scaled logistic functions). Sigmoid($x)=1/(1+e^{-x})$. If $x$ is moderately large positive, $e^{-x}$ underflows to 0 in FP16 if $x&amp;gt;~11$. That gives a sigmoid output of 1.0 exactly. In FP32, $e^{-x}$ for $x=11$ is about $5e-5$, not underflow, so sigmoid ~0.99995. So FP16 will saturate some outputs to exactly 0 or 1 sooner than FP32 would. This can slightly change behaviors (e.g., a neuron might go to full saturation a bit early). Usually, this isn’t a huge deal, but it could make gradients zero earlier. In inference, we don’t care about gradients, just the output value. Getting exactly 1.0 vs 0.99995 probably has no practical difference for a classification output. But one should be aware that &#039;&#039;&#039;activation functions can saturate faster&#039;&#039;&#039; in lower precision due to limited range or precision. ReLU has no issue (it’s linear or 0), but exponential-based activations do. In practice, again, frameworks often simply compute those in the given precision and accept the slight difference. If it were a problem, one could do those in FP32 as well.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Summary:&#039;&#039;&#039; Numerical stability in inference is maintained by using robust computation methods: subtracting maxima in softmax/LogSumExp, adding epsilon in denominators for normalization, and using higher precision for summations or critical steps. Most of these are built into the algorithms. ONNX models assume these standard practices (they don&#039;t need to explicitly do them in the graph because the backend inherently will for ops like Softmax). It&#039;s wise when building or exporting models to ONNX to test them in the target precision and ensure there are no surprises (like an output becomes NaN or Inf due to precision). Usually if training was done in mixed precision, the model will have been vetted in that environment already.&lt;br /&gt;
&lt;br /&gt;
== Performance vs. Accuracy Trade-offs ==&lt;br /&gt;
&lt;br /&gt;
There is an inherent trade-off between numerical precision and computational performance (speed, memory usage) in AI systems. Choosing lower precision can dramatically increase throughput and reduce memory, but it may introduce arithmetic errors. Engineers must balance these considerations:&lt;br /&gt;
&lt;br /&gt;
=== 7.1. Throughput, Memory Bandwidth, and Precision ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Throughput (speed)&#039;&#039;&#039; is often higher for lower-precision operations. Modern hardware can execute more low-precision ops in parallel than high-precision ops. For example, an NVIDIA V100 GPU can perform 2× more half-precision FLOPs than single-precision per clock, and newer architectures with Tensor Cores can do 8× more (since a Tensor Core warp operation might perform 64 FP16 FMAs per cycle vs 16 FP32). Similarly, int8 operations can be even faster and more energy efficient. Lower precision numbers also use less memory bandwidth – fetching 16-bit data is twice as fast as 32-bit data from memory (assuming the same bus width), which can relieve memory bottlenecks. Many deep learning workloads are memory-bound, so halving the data size can nearly double throughput if compute is not the limiting factor. This is a huge incentive to use lower precision.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Memory and storage&#039;&#039;&#039;: Using FP16 or int8 weights reduces model size, which is important for deploying large models to memory-limited environments (like edge devices or GPUs with limited VRAM). It also means you can fit larger batch sizes in memory, or more models on a chip, etc. There is also a cascade effect: smaller data means better cache utilization, which further improves performance.&lt;br /&gt;
&lt;br /&gt;
However, &#039;&#039;&#039;accuracy&#039;&#039;&#039; suffers if precision is too low. We have discussed how rounding error and limited range can cause deviations in outputs. The question is: do those deviations matter for the application? Often for neural networks, a small amount of noise or error in intermediate calculations does not change the final prediction. Neural nets are somewhat error-tolerant due to their redundant distributed representations. This is why quantization and FP16 techniques can work at all. The final model accuracy (e.g. top-1 accuracy on ImageNet) might drop only 0.5% or less when using FP16 instead of FP32, or int8 instead of FP16, if done properly. For many use-cases, this loss is acceptable given the speed gains.&lt;br /&gt;
&lt;br /&gt;
If one goes too low (like naive FP8 or int4 without special care), accuracy might drop severely – perhaps making the model unusable. Thus the trade-off: as precision decreases, performance increases but at some point accuracy degrades beyond an acceptable threshold. Engineers aim to find the lowest precision that still preserves accuracy.&lt;br /&gt;
&lt;br /&gt;
There is also the consideration of &#039;&#039;&#039;accumulation strategy&#039;&#039;&#039; affecting performance: for instance, Kahan summation (compensated) is more accurate but ~4× slower than naive summation. If you need that accuracy, you pay a performance cost. In many AI inference scenarios, the slight extra accuracy from something like Kahan isn&#039;t needed, so it’s not used. But in a scenario where you sum millions of elements for a final result that is thresholded, maybe you do need it.&lt;br /&gt;
&lt;br /&gt;
Another dimension is &#039;&#039;&#039;determinism vs speed&#039;&#039;&#039;. Enforcing a deterministic reduction order (for reproducibility) might require disabling some parallelism, which could slow things down. If exact repeatability is critical (like certain scientific or financial applications of neural nets), one might sacrifice some performance to ensure the summation happens the same way every time. ONNX allows both modes depending on the runtime.&lt;br /&gt;
&lt;br /&gt;
From a memory perspective, if precision is higher than needed, you waste memory. For example, storing intermediate activations in FP32 when they could be FP16 uses 2× memory, which might mean lower batch size or more memory traffic. So there&#039;s strong motivation to keep things as low-precision as possible &#039;&#039;except&#039;&#039; where accuracy truly demands.&lt;br /&gt;
&lt;br /&gt;
In summary, the trade-off often manifests as &#039;&#039;diminishing returns&#039;&#039;: FP64 vs FP32 – huge slowdown for usually negligible accuracy gain in ML; FP32 vs FP16 – moderate speedup for usually minor accuracy hit; FP16 vs int8 – another big speedup but maybe more accuracy loss unless calibration is done; int8 vs int4 – speed gain but often big accuracy drop that might not be worth it for current models.&lt;br /&gt;
&lt;br /&gt;
Engineers use techniques like &#039;&#039;&#039;mixed precision&#039;&#039;&#039; to target high precision only where needed. For example, run most of the model in FP16, but keep a few sensitive layers in FP32. This yields most of the performance benefits while ensuring critical parts are accurate. We already saw softmax as one example. Another might be final output layer: sometimes using FP32 for the logits and softmax can be safer.&lt;br /&gt;
&lt;br /&gt;
Hardware is also trending to support mixed modes – like FP8 Tensor Cores accumulate in FP32, etc. So hardware tries to give accuracy cheaply.&lt;br /&gt;
&lt;br /&gt;
=== 7.2. Hardware Accelerators and Tensor Cores ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Hardware accelerators&#039;&#039;&#039; (GPUs, TPUs, FPGAs, ASICs like GraphCore IPU, etc.) have specific support for certain precisions. Leveraging these can provide massive speedups:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;NVIDIA Tensor Cores:&#039;&#039;&#039; These started in Volta (V100) with support for 4x4 matrix multiply-adds on FP16 inputs with FP32 accumulation&amp;lt;ref name=&amp;quot;ref6&amp;quot;/&amp;gt;. Using Tensor Cores can give up to 8x throughput compared to using regular FP32 cores. But to use them, the data needs to be FP16 (or now BF16 or FP8 on newer GPUs) and aligned to certain dimensions (multiples of 8 etc. in matrix sizes, which frameworks handle by padding if needed). If an ONNX model is all FP32, by default it would not use Tensor Cores unless the runtime automatically converts some ops to mixed precision (which some do via a flag or environment variable enabling TF32 or FP16). The introduction of &#039;&#039;&#039;TensorFloat-32 (TF32)&#039;&#039;&#039; in Ampere GPUs is essentially a compromise: treat FP32 inputs as two 16-bit significand halves (10-bit mantissa actually) so that it can run on Tensor Cores, giving ~8x speed, while still outputting FP32. TF32 has about the precision of FP16 but range of FP32, and it was chosen because it typically doesn’t hurt convnet accuracy. ONNX Runtime or others on Ampere will use TF32 by default for conv and matmul unless disabled, giving a free speed boost at minimal accuracy cost. If exact FP32 is needed, users have to turn it off (sacrificing speed).&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;TPU (Google):&#039;&#039;&#039; TPUs have native support for BF16 matrix ops with FP32 accumulators. They were designed specifically for the mixed precision approach in training. For inference, TPUs also support int8 and other quantized ops (e.g., via Edge TPU for lower-power). If exporting ONNX to run on a TPU (via some bridge), one might get automatic BF16 usage. BF16 has the advantage of no loss in range, so TPUs can more straightforwardly run models in BF16 without modifications.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;FPGAs and ASICs:&#039;&#039;&#039; Some custom accelerators might use fixed-point or block floating-point arithmetic for efficiency. They often treat the network in quantized terms. The ONNX quantization specification (QuantizeLinear/DequantizeLinear ops) exists for representing quantized models (INT8 etc.), which is another domain beyond floating-point, but it overlaps as a precision issue. In those systems, accumulators for INT8 × INT8 multiplies are usually 32-bit (to avoid overflow for dot products up to certain lengths). The trade-off there is between integer vs float representation, which can give big speedups on CPUs/DSAs via SIMD instructions (many CPUs have vector int8 multiply-add instructions now for inference).&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Memory bandwidth on hardware:&#039;&#039;&#039; Some accelerators include special high-bandwidth memory or cache specifically for lower precision. For example, an accelerator might fetch two FP16 values in one 32-bit memory transaction – effectively doubling memory bandwidth for half precision. On some hardware, using FP16 can even reduce memory &#039;&#039;latency&#039;&#039; if multiple values can be packed.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Parallelism and Vectorization:&#039;&#039;&#039; Many hardware units process data in fixed vector widths (e.g., 256-bit registers). That could be 8 floats at once or 16 half-floats at once in the same 256 bits. If your code is in FP16, you might use the full vector width (16 values) whereas FP32 might only use half (8 values). This means FP16 can make better use of vector units. Similarly, matrix units often have fixed tile sizes, and smaller data types allow larger tiles or more tiles concurrently.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Energy efficiency:&#039;&#039;&#039; Lower precision often means less energy per operation, which on large deployments (data centers or battery-powered devices) is a significant advantage. This isn&#039;t directly performance, but it&#039;s an important trade: you might choose a slightly lower precision to save power even if accuracy drops a tiny bit.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Software overhead:&#039;&#039;&#039; Using mixed precision on hardware involves some overhead of casting and moving data between precisions. For instance, if you have to frequently cast FP16 to FP32 and back, that adds instructions and memory copies. A well-designed accelerator (like GPUs with unified memory for FP16/32 or special hardware casting) minimize this. Still, the runtime might insert extra operations (as we saw, ONNX Runtime CPU inserted many Cast nodes converting FP16 to FP32 and back&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;, which actually &#039;&#039;slowed down&#039;&#039; that FP16 model by 4x vs FP32 on a CPU, because the CPU had no native FP16). So ironically, on some hardware, using lower precision in ONNX might degrade performance if not supported (the CPU case). Always ensure the target hardware actually supports the low precision you plan to use; otherwise, you&#039;ll pay overhead for emulation.&lt;br /&gt;
&lt;br /&gt;
To illustrate trade-off: Suppose you have a model that achieves 99% accuracy in FP32 and drops to 98.5% in FP16. If FP16 runs 2x faster, many would accept the 0.5% accuracy drop for double throughput. If int8 runs 4x faster but accuracy drops to 95%, that might be too much drop for some applications, but acceptable for others that value speed. It&#039;s application-dependent.&lt;br /&gt;
&lt;br /&gt;
In critical applications, one might maintain higher precision accumulators or run certain validation steps in high precision to ensure nothing drifted. In research, sometimes a model is trained in mixed precision but validated in double to ensure any small numeric differences don’t influence the reported result (rare, but for scientific computing integration maybe).&lt;br /&gt;
&lt;br /&gt;
In the ONNX world, performance-vs-accuracy is often managed via &#039;&#039;&#039;Execution Providers&#039;&#039;&#039; (EPs) that target specific hardware. For example, the TensorRT EP in ONNX Runtime can take an ONNX model and run it using NVIDIA&#039;s high-performance inference library, which will use FP16 or INT8 if allowed, to maximize speed. The user can configure the EP with precision flags. If the accuracy drop is too high, one might restrict it to FP32. If it&#039;s fine, allow FP16. Tools exist to measure the accuracy difference (like calibrating int8 quantization).&lt;br /&gt;
&lt;br /&gt;
To conclude, the trade-offs revolve around picking the lowest precision that yields acceptable accuracy, and utilizing hardware features fully. ONNX being a high-level spec abstracts this, but it&#039;s ultimately up to the deployment to decide and sometimes up to the exporter to incorporate quantization. ONNX’s role is to be able to &#039;&#039;represent&#039;&#039; these choices (they have added quantized types, FP8 types, etc., to allow the graph to include those when needed).&lt;br /&gt;
&lt;br /&gt;
== Best Practices and Design Guidelines ==&lt;br /&gt;
&lt;br /&gt;
Given the above insights, here are some best practices and guidelines for dealing with numerical precision and accumulators in AI inference (and training) design:&lt;br /&gt;
&lt;br /&gt;
=== 8.1. Choosing an Accumulator Precision ===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Match precision to data length and range:&#039;&#039;&#039; If an operation accumulates a large number of terms, consider using a higher precision accumulator than the input data. For example, for summing &amp;gt;1000 values of type FP16, an FP32 accumulator is recommended to avoid large rounding error or overflow. If summing &amp;gt; million values of FP32, consider double (FP64) accumulation if high accuracy is needed (though this is rare in DL). &#039;&#039;Rule of thumb:&#039;&#039; if $N \cdot \varepsilon$ is not tiny (say &amp;gt;1e-3), you may benefit from higher precision. For FP16 ($\varepsilon \approx 1e-3$), that threshold N is a few hundred. For FP32 ($\varepsilon \approx 1e-7$), N would be in the millions before 1e-3 error.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Use higher precision in critical layers:&#039;&#039;&#039; Identify if certain layers of the model are more sensitive to precision. For instance, the output layer or a layer that feeds into a very sharp decision boundary might require more precision. If a small change there could flip the prediction, you don&#039;t want excessive numerical error. Using FP32 for logits and softmax is a common practice to maintain classification confidence stability.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Follow hardware defaults, but override if needed:&#039;&#039;&#039; Many hardware accelerators have sensible defaults (like accumulate in FP32 for tensor ops). It&#039;s usually best to use those defaults as they were chosen to balance accuracy and speed. Only override if you have evidence of a problem. For example, if using ONNX Runtime on CPU, you discovered it upcasts to float for you – great. But if using some custom accelerator that does pure FP16, you might manually insert casts in the ONNX graph around certain ops to force higher precision on those ops.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Maintain precision for gradient accumulation (training):&#039;&#039;&#039; This is more training-oriented, but worth noting: when summing gradients over batches or updating weights, use higher precision (FP32 or even 64) for those accumulations. This ensures training doesn&#039;t diverge due to numeric error. Many frameworks keep a &amp;quot;master copy&amp;quot; of weights in FP32 when training in FP16&amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;. For inference, this translates to: any running statistics or counters in the model (like if you had a running average inside the model) should probably be in higher precision to avoid drift.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Be mindful of intermediate cast costs:&#039;&#039;&#039; If you do decide to cast to higher precision for an operation in ONNX, remember it incurs memory copies and compute overhead. Make sure it&#039;s worth it. For example, if you cast a huge tensor to double, that could slow things drastically. Usually FP16-&amp;gt;FP32 is fine (hardware handles it well), but FP32-&amp;gt;FP64 might be expensive on GPUs.&lt;br /&gt;
&lt;br /&gt;
=== 8.2. Safe Reduction Lengths and Heuristics ===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Chunk long reductions:&#039;&#039;&#039; If you must do a very long sum in low precision (due to memory constraints or hardware), chunk the sum into parts, sum each to a higher precision local accumulator, then sum the partial sums. This is essentially pairwise summation. Many libraries use a fixed chunk size heuristic (e.g., sum in blocks of 256 in registers, then add those up). This bounds the error growth. If implementing manually, you can choose a chunk size such that chunk_length &#039;&#039; ε is small (like 256 &#039;&#039; 1e-3 = 0.256 for FP16, hmm maybe choose 100).&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Sort by magnitude if possible:&#039;&#039;&#039; In some cases (not typical in DL, but possible in post-processing), if you have numbers varying by orders of magnitude, summing from smallest to largest improves accuracy. It prevents small ones from being swallowed by big ones. This is more relevant if you output a sum of mixed-scale values (like some algorithm mixing big and small contributions). It’s not always feasible in real-time inference to sort, but keep in mind.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Use compensation for critical sums:&#039;&#039;&#039; If you find a particular sum or difference in your inference computation is crucial (like computing a small residual from two close numbers), consider using Kahan summation or another compensated scheme just for that part. For example, if you had an ONNX custom function that needs to sum a lot of almost cancelling terms, you might implement it with compensation internally.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Heuristics for FP8/low-bit:&#039;&#039;&#039; When using extremely low precision like FP8, adopt the recommended practices: use scaling (don&#039;t feed raw data into FP8, normalize it first), and perhaps only use FP8 on portions of the network that are empirically robust. The industry is still establishing heuristics, but one might be: use FP8 for matrix multiplications, but accumulate in FP16 or FP32; keep normalization in FP16; use FP16 for first and last layer maybe. This is evolving, so keep an eye on literature.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Keep an eye on sum length in softmax and normalization:&#039;&#039;&#039; If you have extremely large class counts or group sizes (e.g., a softmax over 1e6 items, or layer norm over tens of thousands of features), realize this is a giant reduction. You may want to ensure that reduction is done in higher precision. ONNX Softmax by default will likely not handle a million-class case in FP16 well (the sum would overflow since 1e6 &amp;gt; 65504). So either do that on CPU in FP32 or break it down (could do hierarchical softmax, etc.). This is a corner case, but relevant for huge vocab language models. In practice, vocab ~50k is fine as we said.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Test numeric differences:&#039;&#039;&#039; A practical tip: after quantizing or reducing precision, run some test inputs through both the high-precision model and low-precision model. Compare outputs (and maybe some intermediate signals if possible). If differences are consistently small relative to the application tolerance, you&#039;re good. If you see occasional large differences or instabilities (like outputs become NaN or Inf in low precision), that&#039;s a red flag. Use that testing to guide where you might need to increase precision or change algorithms.&lt;br /&gt;
&lt;br /&gt;
=== 8.3. When Higher Precision Is Required ===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Ill-conditioned calculations:&#039;&#039;&#039; If your model or pipeline performs any operation known to be ill-conditioned (matrix inversion, subtraction of nearly equal quantities, computing a tiny difference of large numbers), you likely need higher precision for that part. For example, if your ONNX model includes a custom op to solve a linear system, use double precision in that op if possible (or at least float with iterative refinement). In ML, this might occur in something like a physics-informed neural network or a differentiable simulation within the model.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Strict accuracy requirements:&#039;&#039;&#039; Some applications (medical, aerospace, finance) might have strict tolerances. If an inference model’s output needs to be correct to 5 decimal places, you probably cannot use FP16, which has only 3-4 decimal digits precision. FP32 might be barely enough (~7 digits). FP64 might be warranted to guarantee that level of accuracy in worst case. Most deep learning applications (like image classification) don&#039;t demand that level of numeric precision in outputs – as long as the top class is correct, it doesn&#039;t matter if its probability was 0.901 vs 0.899. But if you&#039;re doing something like summing probabilities to make a risk estimate or doing a cumulative sum over many predictions, be careful to accumulate precisely.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Large sums or products in post-processing:&#039;&#039;&#039; Sometimes the neural net is fine, but the &#039;&#039;post-processing&#039;&#039; of results might need precision. E.g., summing up probabilities of many events, or computing confidence intervals, etc. Do those calculations in double if needed outside the model.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Preventing drift in long sequences:&#039;&#039;&#039; For sequential models (RNNs processing thousands of steps), rounding error can accumulate over steps. If each step has a small error, after 10000 steps it could add up. In such cases, using a higher precision for the state might reduce drift. Some practitioners use FP64 for the hidden state accumulation in very long sequence processing to maintain numerical stability (though this is not common due to speed loss). Alternatively, periodically renormalize or reset small errors.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Simulations or differential equations:&#039;&#039;&#039; If your ONNX model involves simulating something (like a differential equation solver inside a model, or an ODE/RNN hybrid), those often need double precision to be stable for long durations. Recognize those situations and allocate precision accordingly.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Verification and debugging:&#039;&#039;&#039; Use higher precision as a tool to verify correctness. For example, run your model in FP64 (if possible) and compare to FP32 to see if there are differences. If not, FP32 is fine. If yes, you might find a particular calculation losing too much info in FP32, implying maybe you should keep it in FP64 in production as well. This is rare in inference but can be part of rigorous validation.&lt;br /&gt;
&lt;br /&gt;
In ONNX, using higher precision may involve casting to &amp;lt;code&amp;gt;tensor(double)&amp;lt;/code&amp;gt; for certain ops. Not all runtimes support double on GPU though (some do via emulation or just slower path). ONNX CPU does support double for general ops. So be mindful of the target.&lt;br /&gt;
&lt;br /&gt;
Finally, &#039;&#039;&#039;document your precision choices&#039;&#039;&#039;. If you deliver an ONNX model that uses mixed types (float16 inputs, float32 accumulations, int8 weights, etc.), ensure that is clear to whoever maintains or uses it. Precision issues can be sneaky, so clarity helps.&lt;br /&gt;
&lt;br /&gt;
== Limitations and Open Problems ==&lt;br /&gt;
&lt;br /&gt;
Despite advances in handling numerical precision, there remain limitations and open challenges in this domain:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Lack of Precision Specification in ONNX:&#039;&#039;&#039; As noted, ONNX currently has no way to formally specify that an op &#039;&#039;should&#039;&#039; use a higher precision internally. This can lead to nondeterministic differences across platforms&amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;. While proposals exist to add attributes for this&amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;, it is not yet standard. Thus an ONNX model might behave slightly differently on two compliant backends, and there&#039;s no built-in mechanism to ensure identical results. This is a limitation for applications requiring cross-platform consistency. Solving it may involve extending ONNX (e.g., an attribute like &amp;lt;code&amp;gt;accumulatePrecision=&amp;quot;FLOAT32&amp;quot;&amp;lt;/code&amp;gt; for MatMul) or tightening the spec, but those come with backward compatibility and implementation burden concerns.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Reproducibility and Determinism:&#039;&#039;&#039; Even with the same precision, parallel computation can introduce nondeterminism (due to different summation orders). Ensuring bit-identical results across runs (important for some sensitive domains) is non-trivial. Frameworks often provide &amp;quot;deterministic mode&amp;quot; which might use fixed ordering at cost of speed. In ONNX, there&#039;s no global flag for this; it&#039;s up to the runtime. More research could be done on algorithms that produce bit-wise identical results independent of parallel execution order (perhaps using techniques like pairwise summation or consistent partitioning). Stochastic rounding is another area: sometimes using probabilistic rounding can reduce bias in accumulated error, but it introduces randomness – reconciling that with reproducibility is tricky.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Ultra-low Precision (FP8 and below):&#039;&#039;&#039; While FP8 is being explored, going to 4-bit floats or mixed schemes is an open challenge. The OpenAI community is investigating 4-bit (quantization-wise) for LLMs with some success, but a true 4-bit float (like E3M1) would be extremely limited. Perhaps hybrid schemes (like block floating point, log scales, etc.) will come. ONNX already has some support (they mention 4-bit in documentation placeholders&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;), but it&#039;s nascent. Research into training techniques, calibration, and new number formats (like &#039;&#039;&#039;posit numbers&#039;&#039;&#039; or tapering precision floats) is ongoing. If a new format proves useful, ONNX will need to adapt to represent it.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Quantization Noise and Model Robustness:&#039;&#039;&#039; Reducing precision can sometimes expose models to issues like adversarial vulnerability or fragility – e.g., a slight perturbation might flip a decision if the margin was small and precision loss nudged it. Ensuring model robustness under quantization is an open problem. It might require re-training the model with quantization in the loop (quantization-aware training) or designing models that have large &amp;quot;margin&amp;quot; between classes to tolerate quantization noise.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Dynamic range during training vs inference:&#039;&#039;&#039; In training, activations can have wider distributions (especially early in training). Models often learn to constrain their ranges (via activation functions saturating or normalization), which makes inference more stable in lower precision. But if one tries to train in FP8 from scratch, it&#039;s very hard. This gap between what&#039;s possible in a post-trained inference model vs training is an open area – how to train directly in low precision without significant accuracy loss. Solving that could simplify workflows (no need for high-precision training then quantize; just train low-prec end to end). Techniques like loss scaling, gradient clipping, etc., are partial solutions. The development of optimizers that are resilient to low precision is ongoing.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Hardware variability:&#039;&#039;&#039; As new hardware emerges (with new numeric capabilities or quirks), ensuring that ONNX models run consistently is an ongoing task. For instance, one device might implement a fused op that reduces rounding error, another does it separately. Even something like convolution might use a different algorithm (FFT-based vs direct) which has different numeric properties. Open problem: could ONNX allow specifying numeric tolerances per op or preferred algorithms? That may be overkill for most, but for some HPC ML use cases, maybe desired.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Testing and validation of numerics:&#039;&#039;&#039; There isn&#039;t a standardized set of &amp;quot;numeric compliance&amp;quot; tests for ONNX runtimes beyond simple correctness. Perhaps in the future, test suites might include challenging numeric scenarios (large sums, nearly singular matrices) to ensure a runtime handles them gracefully (maybe by switching precision). Right now, it&#039;s mostly on the user to test their model under expected conditions.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Combining quantization with floating point:&#039;&#039;&#039; Many production models use a mix (e.g., int8 for some layers, FP16 for others). ONNX can represent this (with QuantizeLinear ops or MixedPrecision through casting), but tooling to automatically find the optimal mix is still developing. It&#039;s somewhat trial-and-error or using heuristics. An open question is how to automate precision assignment: given a model and a target hardware, how to automatically choose which layers can be int8, which should be FP16, etc., to meet an accuracy target. This crosses into NAS (neural architecture search) territory or automated quantization. &lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Numerical debugging tools:&#039;&#039;&#039; When a model goes wrong due to precision, it&#039;s often hard to pinpoint where. Better tooling to analyze numerical error layer by layer could be useful. For instance, one could run a model in double and float and diff the intermediate results to see where the largest divergence happens. Doing this systematically for big nets is an open tooling challenge. Some frameworks have hooks for this, but not standardized.&lt;br /&gt;
&lt;br /&gt;
In conclusion, while we&#039;ve come far with using lower precision in AI without sacrificing much accuracy, the landscape is always evolving. The key open problem is how to push precision lower (for efficiency gains) without crossing the threshold where accuracy falls off a cliff. Each new generation of hardware and techniques (like FP8, novel quantization schemes) extends that boundary, but also brings new questions on how to manage the numerical errors. ONNX as an interchange will need to keep pace, possibly by adding more expressive power for these numeric considerations.&lt;br /&gt;
&lt;br /&gt;
== See Also ==&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;IEEE 754 Floating-Point&#039;&#039;&#039; – The standard defining binary16, binary32, binary64 formats and rounding behavior.&lt;br /&gt;
* &#039;&#039;&#039;Floating-Point Arithmetic&#039;&#039;&#039; – General topic on how floating-point math works, common issues (overflow, underflow, cancellation).&lt;br /&gt;
* &#039;&#039;&#039;Kahan Summation Algorithm&#039;&#039;&#039; – Algorithm for compensated summation to reduce numerical error&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;.&lt;br /&gt;
* &#039;&#039;&#039;BFloat16&#039;&#039;&#039; – A short-format float with 8-bit exponent, used in TPUs and some GPUs for training networks.&lt;br /&gt;
* &#039;&#039;&#039;Mixed Precision Training&#039;&#039;&#039; – Technique of using FP16 (or BF16) along with FP32 accumulators to accelerate training&amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;.&lt;br /&gt;
* &#039;&#039;&#039;Quantization in Deep Learning&#039;&#039;&#039; – Converting networks to lower bit integer arithmetic for inference (INT8 quantization and calibration techniques).&lt;br /&gt;
* &#039;&#039;&#039;ONNX Operator Specification&#039;&#039;&#039; – Official docs for ONNX ops, which describe their mathematical function but not the required numeric precision (a point of potential extension in future).&lt;br /&gt;
* &#039;&#039;&#039;CUDA Tensor Cores &amp;amp; TF32&#039;&#039;&#039; – NVIDIA documentation on how tensor cores perform mixed precision and the TF32 format for accelerating FP32.&lt;br /&gt;
* &#039;&#039;&#039;Pairwise Summation&#039;&#039;&#039; – Summation algorithm with logN error growth&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;, used in many numerical libraries to improve accuracy of reductions.&lt;br /&gt;
* &#039;&#039;&#039;Softmax Stability&#039;&#039;&#039; – Articles on numerically stable softmax and log-sum-exp implementations to avoid overflow.&lt;br /&gt;
* &#039;&#039;&#039;Performance Tuning for Low Precision&#039;&#039;&#039; – Guides (e.g., NVIDIA or Intel whitepapers) on using low precision and the trade-offs involved in various hardware.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;references&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref1&amp;quot;&amp;gt;Accelerate and simplify Scikit-learn model inference with ONNX Runtime - Microsoft Open Source Blog — opensource.microsoft.com — https://opensource.microsoft.com/blog/2020/12/17/accelerate-simplify-scikit-learn-model-inference-onnx-runtime&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref2&amp;quot;&amp;gt;Floating Point Precision: Understanding FP64, FP32, and FP16 in Large Language Models - DEV Community — dev.to — https://dev.to/lukehinds/floating-point-precision-understanding-fp64-fp32-and-fp16-in-large-language-models-3gk6&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref3&amp;quot;&amp;gt;Floating Point Precision and Its Limitations | by Umair Akbar | Medium — akbu.medium.com — https://akbu.medium.com/floating-point-precision-and-its-limitations-cfb7247d7789&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref4&amp;quot;&amp;gt;Kahan summation algorithm - Wikipedia — en.wikipedia.org — https://en.wikipedia.org/wiki/Kahan_summation_algorithm&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref5&amp;quot;&amp;gt;Pairwise summation - Wikipedia — en.wikipedia.org — https://en.wikipedia.org/wiki/Pairwise_summation&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref6&amp;quot;&amp;gt;Numerical behavior of NVIDIA tensor cores - PMC — pmc.ncbi.nlm.nih.gov — https://pmc.ncbi.nlm.nih.gov/articles/PMC7959640/&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref7&amp;quot;&amp;gt;Mixed precision and performance-accuracy tuning (neuron-cc) — AWS Neuron Documentation — awsdocs-neuron.readthedocs-hosted.com — https://awsdocs-neuron.readthedocs-hosted.com/en/latest/about-neuron/appnotes/neuron-cc/mixed-precision.html&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref8&amp;quot;&amp;gt;Create Float16 and Mixed Precision Models - ONNX Runtime — onnxruntime.ai — https://onnxruntime.ai/docs/performance/model-optimizations/float16.html&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref9&amp;quot;&amp;gt;[Performance] ONNX FP16 model is having performance bottle neck when compared to FP32 variant · Issue #25824 · microsoft/onnxruntime · GitHub — github.com — https://github.com/microsoft/onnxruntime/issues/25824&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref10&amp;quot;&amp;gt;Float stored in 8 bits - ONNX 1.21.0 documentation — onnx.ai — https://onnx.ai/onnx/technical/float8.html&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref11&amp;quot;&amp;gt;[2209.05433] FP8 Formats for Deep Learning — arxiv.org — https://arxiv.org/abs/2209.05433&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref12&amp;quot;&amp;gt;Should MatMul op allow specification of accumulation precision? · Issue #7072 · onnx/onnx · GitHub — github.com — https://github.com/onnx/onnx/issues/7072&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref13&amp;quot;&amp;gt;Quantize ONNX models | onnxruntime — onnxruntime.ai — https://onnxruntime.ai/docs/performance/model-optimizations/quantization.html&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref14&amp;quot;&amp;gt;Keep Attention Softmax FP32 during FP16/ZeRO Training · Issue #1485 · hpcaitech/ColossalAI · GitHub — github.com — https://github.com/hpcaitech/ColossalAI/issues/1485&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;/references&amp;gt;&lt;/div&gt;</summary>
		<author><name>Timo.stripf</name></author>
	</entry>
	<entry>
		<id>https://www.emmtrix.com/w139/index.php?title=Numerical_Precision_in_ONNX_and_AI_Inference&amp;diff=2895</id>
		<title>Numerical Precision in ONNX and AI Inference</title>
		<link rel="alternate" type="text/html" href="https://www.emmtrix.com/w139/index.php?title=Numerical_Precision_in_ONNX_and_AI_Inference&amp;diff=2895"/>
		<updated>2026-01-30T15:20:47Z</updated>

		<summary type="html">&lt;p&gt;Timo.stripf: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&#039;&#039;&#039;Open Neural Network Exchange (ONNX)&#039;&#039;&#039; is an open standard format for representing machine learning models and neural network computations across different frameworks and hardware&amp;lt;ref name=&amp;quot;ref1&amp;quot;/&amp;gt;. As models are exported and deployed via ONNX, the &#039;&#039;&#039;numerical precision&#039;&#039;&#039; of computations becomes critical. Deep learning inference involves a variety of floating-point operations, and small numerical discrepancies can accumulate and affect model accuracy or reproducibility. This article provides an in-depth overview of floating-point precision issues in numerical computing, with a focus on how they manifest in AI inference and the ONNX ecosystem. We cover floating-point formats and rounding behavior, accumulation strategies for summations, the use of low-precision formats (FP16, BFloat16, FP8) in modern AI, and how ONNX defines (or leaves unspecified) the precision of accumulators in operations. Strategies for maintaining numerical stability in deep neural networks are discussed, along with the trade-offs between computational performance and arithmetic accuracy. The goal is to give engineers and researchers a comprehensive, encyclopedic reference on numerical precision in ONNX-based AI inference systems.&lt;br /&gt;
&lt;br /&gt;
== Floating-Point Precision in Numerical Computing ==&lt;br /&gt;
&lt;br /&gt;
=== 2.1. IEEE 754 Floating-Point Formats ===&lt;br /&gt;
&lt;br /&gt;
Modern computers represent real numbers using the IEEE 754 floating-point standard, which defines formats like 32-bit &#039;&#039;single precision&#039;&#039; (FP32) and 64-bit &#039;&#039;double precision&#039;&#039; (FP64). In IEEE 754 binary formats, a floating-point number is composed of three fields: 1 sign bit, several exponent bits (with a bias), and several fraction (mantissa) bits. For example, FP32 consists of 1 sign bit, 8 exponent bits, and 23 fraction bits (24 bits of significand precision including the implicit leading 1)&amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;. This provides roughly 7 decimal digits of precision and an exponent range allowing values on the order of $10^{\pm38}$. FP64 allocates 1 sign bit, 11 exponent bits, and 52 fraction bits (53-bit precision), yielding about 15–16 decimal digits of precision and a vastly larger range (~$10^{\pm308}$)&amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;. These larger formats (especially FP64) are the &amp;quot;gold standard&amp;quot; for numerical accuracy in scientific computing, as they reduce round-off error in complex calculations&amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;. However, the increased precision comes at the cost of doubled storage and typically reduced computational throughput (e.g. many GPUs execute FP64 instructions at a much lower rate than FP32&amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
In addition to single and double precision, the IEEE 754-2008 standard introduced a 16-bit &#039;&#039;half precision&#039;&#039; format (FP16, or binary16). FP16 has 1 sign bit, 5 exponent bits, and 10 fraction bits (11-bit significand including the implicit 1)&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. This format covers a much smaller dynamic range (approximately $6.1\times10^{-5}$ to $6.5\times10^4$ for normalized values&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;) and offers only around 3 decimal digits of precision. Half precision was originally used in graphics, but it has gained popularity in machine learning for its speed and memory advantages (discussed later in Section 4). Another format relevant to AI is &#039;&#039;&#039;BFloat16&#039;&#039;&#039; (Brain Float 16), a 16-bit float with &#039;&#039;&#039;8 exponent bits and 7 fraction bits&#039;&#039;&#039;&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. BFloat16 sacrifices precision (only ~7 fraction bits ≈ 2–3 decimal digits) in order to have the &#039;&#039;&#039;same exponent range as FP32&#039;&#039;&#039;&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. This means BFloat16 can represent very large or very small numbers (∼$10^{\pm38}$, similar range as FP32) but with much less precision between representable values&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. BFloat16 was introduced for deep learning by Google (for TPUs) to allow reduced precision &#039;&#039;training&#039;&#039; without frequent overflow/underflow, relying on the wide range and accepting the coarse precision.&lt;br /&gt;
&lt;br /&gt;
Each floating-point format has a &#039;&#039;&#039;unit roundoff&#039;&#039;&#039; or machine precision, denoted $\epsilon$. This is effectively the spacing between 1.0 and the next representable number. For FP32, $\epsilon \approx 2^{-23} \approx 1.19\times10^{-7}$, corresponding to about 7–8 decimal digits of precision&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. In FP64, $\epsilon = 2^{-52} \approx 2.22\times10^{-16}$ (15–16 decimal digits). Half precision is much less precise: FP16 has $\epsilon = 2^{-10} \approx 9.76\times10^{-4}$&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;, and BFloat16 has $\epsilon = 2^{-7} = 7.8125\times10^{-3}$&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. These values quantify how finely the continuum of real numbers is quantized in each format.&lt;br /&gt;
&lt;br /&gt;
=== 2.2. Machine Epsilon and Rounding Error ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Machine epsilon ($\varepsilon$)&#039;&#039;&#039; is formally defined as the smallest positive number such that $1.0 + \varepsilon &amp;gt; 1.0$ in the floating-point format&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. It effectively measures the &#039;&#039;&#039;precision&#039;&#039;&#039; of the representation. In FP32, $\varepsilon = 2^{-23} \approx 1.19\times10^{-7}$, meaning any result is rounded to about 7 decimal digits. FP16’s $\varepsilon = 2^{-10} \approx 9.77\times10^{-4}$, over 1000× larger (so only ~3 decimal digits of precision), and BFloat16’s $\varepsilon = 2^{-7} \approx 7.8\times10^{-3}$, reflecting its mere ~8-bit precision&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. Rounding in IEEE 754 by default uses &#039;&#039;&#039;round-to-nearest (ties to even)&#039;&#039;&#039;, which ensures the rounding error for any single operation is at most half a unit in the last place (0.5 ULP). We can model a floating-point addition as: &lt;br /&gt;
&lt;br /&gt;
$$\text{fl}(a+b) = (a+b)\,(1 + \delta),$$ &lt;br /&gt;
&lt;br /&gt;
where $|\delta| \le \varepsilon$ for that format&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. Here $\varepsilon/2$ is the maximum &#039;&#039;&#039;relative error&#039;&#039;&#039; in one rounding (also called unit roundoff). For example, when adding two FP32 numbers, the result is accurate to within about $10^{-7}$ of the true sum (relative), whereas in BF16, the result can deviate by up to $\sim7.8\times10^{-3}$ (nearly 0.78% relative error per operation)&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. Thus, &#039;&#039;&#039;each arithmetic operation incurs a small rounding error&#039;&#039;&#039; (on the order of machine epsilon) that can propagate or accumulate through a sequence of calculations.&lt;br /&gt;
&lt;br /&gt;
Because floating-point representations have finite exponent range, we must also consider &#039;&#039;&#039;overflow&#039;&#039;&#039; and &#039;&#039;&#039;underflow&#039;&#039;&#039;. If a result’s magnitude exceeds the maximum representable value, it overflows to infinity; if it falls below the minimum normal (or minimum subnormal) value, it underflows to zero (or a denormalized tiny value). FP32 and BF16 share an exponent size (8 bits), so their range for normal numbers is roughly $[10^{-38}, 10^{38}]$ (approx $~\!3.4\times10^{38}$ max)&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. FP16’s 5-bit exponent allows max $\sim 6.55\times10^4$ and min normal $\sim 6.10\times10^{-5}$&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. In practice, &#039;&#039;&#039;underflow/overflow&#039;&#039;&#039; can occur in low-precision arithmetic if values are not appropriately scaled. For instance, summing a large number of moderately sized terms in FP16 could overflow where FP32 would not. As an example, summing on the order of $10^4$ values of magnitude ~$10^1$ (say, adding 10,000 terms around 10 each) would produce a total $\sim10^5$, which exceeds FP16’s max finite value ~$6.5\times10^4$ and would overflow to $+\infty$ in half precision&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. Underflow is usually a concern when subtracting nearly equal numbers (catastrophic cancellation yielding a tiny difference that might flush to zero in low precision).&lt;br /&gt;
&lt;br /&gt;
=== 2.3. Accumulation Error in Reductions ===&lt;br /&gt;
&lt;br /&gt;
Floating-point addition is &#039;&#039;&#039;not associative&#039;&#039;&#039;: $(a+b)+c$ can differ from $a+(b+c)$ due to rounding, and thus the order in which we sum a list of numbers can change the result slightly. When summing a large sequence of $N$ numbers $x_1, x_2, ..., x_N$, the &#039;&#039;&#039;accumulated rounding error&#039;&#039;&#039; can be significant. In the &#039;&#039;worst case&#039;&#039; (e.g., adding many positive and negative terms that cause maximal cancellation or always rounding one way), the error can grow on the order of $O(N \varepsilon)$ relative to the exact sum&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;. Simply summing $N$ numbers in sequence (the &#039;&#039;&#039;naive linear summation&#039;&#039;&#039;) has a worst-case error bound proportional to $N\,\varepsilon$&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. This worst-case occurs for adversarial arrangements of values; in more typical scenarios with rounding errors of random signs, one can model the error as a random walk, yielding an expected root-mean-square (RMS) error growth on the order of $O(\sqrt{N}\,\varepsilon)$&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;. Still, with very large $N$, even $\sqrt{N}$ times $\varepsilon$ can become large enough to matter.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Cancellation&#039;&#039;&#039; is a particular problem: if we add two nearly equal numbers of opposite sign, most leading digits cancel and the result loses significant bits of precision. For example, in single precision, subtracting two numbers that agree in the first 6 decimal digits will yield a result with only ~1 digit of accuracy left. Catastrophic cancellation can dramatically increase relative error. Summation is especially sensitive to cancellation if large positive and negative terms are summed naively. The &#039;&#039;&#039;condition number&#039;&#039;&#039; of a summation (the ratio of the sum of absolute values $\sum|x_i|$ to the absolute value of the true sum $|\sum x_i|$) indicates how sensitive the result is to perturbations&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;. If the numbers have mixed signs and nearly cancel out (making the condition number large), even an optimal summation order will have a large relative error. For instance, summing [1.0, $10^{100}$, 1.0, $-10^{100}$] in double precision has an exact sum of 2.0, but naive summation may yield 0.0 due to cancellation; an improved summation algorithm yields the correct 2.0&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
In practical terms, when summing a long list of floating-point numbers (such as accumulating a dot product or reducing a tensor), the &#039;&#039;&#039;accumulation error&#039;&#039;&#039; can become noticeable if no precautions are taken. Techniques to mitigate this error include carefully choosing the summation order or using higher precision for intermediate sums. We will explore these strategies in the next section.&lt;br /&gt;
&lt;br /&gt;
== Accumulators and Reduction Strategies ==&lt;br /&gt;
&lt;br /&gt;
When performing reductions (sums, dot products, etc.), the method of accumulation can greatly affect the accuracy of the result. Below, we discuss various strategies:&lt;br /&gt;
&lt;br /&gt;
=== 3.1. Naive Linear Accumulation ===&lt;br /&gt;
&lt;br /&gt;
The simplest accumulation strategy is to use a single accumulator and add each element sequentially. In C-like pseudocode:&lt;br /&gt;
&lt;br /&gt;
This &#039;&#039;&#039;naive linear summation&#039;&#039;&#039; adds values one by one to the running total. While straightforward and fast, it is prone to the rounding error accumulation described earlier. The partial sum &amp;lt;code&amp;gt;sum&amp;lt;/code&amp;gt; continually grows (in absolute value) as terms are added, so new small terms may fall outside its precision. For example, if &amp;lt;code&amp;gt;sum&amp;lt;/code&amp;gt; is large and positive, adding a comparatively tiny positive number may produce no change because the small term&#039;s significant digits vanish in the presence of the large sum (the increment is below the rounding threshold of &amp;lt;code&amp;gt;sum&amp;lt;/code&amp;gt;). Conversely, adding a small number of opposite sign can suffer from cancellation. The order of addition matters: summing from smallest magnitude to largest (ascending order) tends to yield a more accurate result than summing in descending order, because adding small terms first preserves their contributions before the sum becomes very large. The naive loop above implicitly sums in the given index order; if the data is unsorted, this order is essentially arbitrary.&lt;br /&gt;
&lt;br /&gt;
The performance of linear accumulation is excellent (minimal overhead), and for many applications the resulting error is within acceptable limits. However, for &#039;&#039;&#039;very large N&#039;&#039;&#039; or ill-conditioned sums, the inaccuracy can become a problem. In a reduction involving millions of elements, a single-precision sum could lose several digits of accuracy. In deep learning, this could translate to small discrepancies in layer outputs or aggregated gradients if done naively in low precision.&lt;br /&gt;
&lt;br /&gt;
=== 3.2. Pairwise and Tree-Based Reductions ===&lt;br /&gt;
&lt;br /&gt;
A better approach to summation is &#039;&#039;&#039;pairwise summation&#039;&#039;&#039;, which sums numbers in a binary tree structure rather than linearly. The idea is to recursively split the array into halves, sum each half, then add the two partial sums. This can be implemented as:&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
By always adding numbers of similar magnitudes first (small subsets are reduced to subtotals, then those are added, etc.), pairwise summation avoids the situation of a very large running total absorbing small additions until the final steps. The error of pairwise summation grows logarithmically with $N$ in the worst case (roughly $O(\varepsilon \log_2 N)$)&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;, instead of linearly. Intuitively, at each level of the tree, rounding error is constrained, and there are about $\log_2 N$ levels. In fact, the worst-case relative error bound for pairwise summation (base case of 1 element) is:&lt;br /&gt;
&lt;br /&gt;
\[ |E_N| \le \frac{\varepsilon \log_2 N}{1 - \varepsilon \log_2 N} \sum_{i=1}^N |x_i|, \]&lt;br /&gt;
&lt;br /&gt;
which for practical $N$ simplifies to on the order of $\varepsilon \log_2 N$ times the condition number of the sum&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;. In typical random-error scenarios, the error behaves even better (on the order of $\sqrt{\log N}$)&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;. The key is that &#039;&#039;&#039;pairwise (tree) reduction balances the accumulations&#039;&#039;&#039;, preventing one sum from overwhelming the others too early.&lt;br /&gt;
&lt;br /&gt;
From a performance standpoint, pairwise summation has the same $O(N)$ total additions as linear summation, just a different order. It is also highly amenable to parallelization: independent partial sums can be computed in parallel at each level of the tree. Many numerical libraries and BLAS implementations use a form of blocked or pairwise summation in dot products for improved accuracy at negligible performance cost. For example, NumPy and Julia perform partial pairwise summation by default for better numerical behavior&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=== 3.3. Compensated Summation (Kahan, Neumaier) ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Compensated summation&#039;&#039;&#039; is an alternative technique to improve summation accuracy by tracking the small “lost” quantities. The most famous algorithm is the &#039;&#039;&#039;Kahan summation algorithm&#039;&#039;&#039; (also called Kahan–Babuška)&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. Kahan summation uses an extra variable to accumulate the round-off error. In pseudocode:&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Here &amp;lt;code&amp;gt;c&amp;lt;/code&amp;gt; accumulates the tiny leftovers that would otherwise be dropped&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. Each iteration essentially subtracts the last error from the new addend (&amp;lt;code&amp;gt;y = x[i] - c&amp;lt;/code&amp;gt;) so that &amp;lt;code&amp;gt;t = sum + y&amp;lt;/code&amp;gt; includes the small bits from the previous operation. The new error is then stored in &amp;lt;code&amp;gt;c&amp;lt;/code&amp;gt; for the next iteration. The result is that the final &amp;lt;code&amp;gt;sum&amp;lt;/code&amp;gt; is as if computed with higher precision than the machine’s: Kahan’s algorithm can &#039;&#039;&#039;greatly reduce the error&#039;&#039;&#039; in summing a sequence of floats. In fact, with exact arithmetic for the compensation, Kahan summation yields a result accurate to within 1 or 2 ULP of the true sum, regardless of N (assuming the sequence of partial sums doesn’t overflow)&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. More precisely, Kahan summation achieves a &#039;&#039;&#039;worst-case error independent of N&#039;&#039;&#039; (error bound depends only on machine precision, not the number of terms)&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. In practice, using a double accumulator for summing single-precision numbers via Kahan can give near double-precision accuracy.&lt;br /&gt;
&lt;br /&gt;
An important subtlety is that the compensation variable &amp;lt;code&amp;gt;c&amp;lt;/code&amp;gt; should be kept in &#039;&#039;full precision&#039;&#039;. In the implementation above, we used &amp;lt;code&amp;gt;double&amp;lt;/code&amp;gt; for both sum and c. If summing FP32 values, one might use &amp;lt;code&amp;gt;float sum&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;float c&amp;lt;/code&amp;gt; (both 32-bit), which still improves accuracy, but using a higher precision for the accumulator and compensation (i.e. double) yields even better results. Many implementations of Kahan summation use the same type for sum and c, but it&#039;s assumed to be higher precision than the inputs when possible.&lt;br /&gt;
&lt;br /&gt;
Kahan’s algorithm carries a performance cost: each iteration does five arithmetic operations (two adds, one subtract, etc.) and has a data dependency, which can hinder parallelism or pipelining&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. Thus, it runs slower than naive summation (roughly 2–3× slower or more, depending on CPU pipeline). In loops where summation is a bottleneck, this may be significant. Nonetheless, for modest N or where maximum accuracy is required, compensated summation is extremely useful. In fact, Python’s &amp;lt;code&amp;gt;math.fsum&amp;lt;/code&amp;gt; function uses a variant of Kahan’s algorithm (actually a more advanced multi-part compensation) to sum iterables with high precision.&lt;br /&gt;
&lt;br /&gt;
There are variations and improvements on basic Kahan summation. &#039;&#039;&#039;Neumaier’s algorithm&#039;&#039;&#039; (sometimes called &#039;&#039;Kahan-Babuška-Neumaier&#039;&#039;) improves Kahan’s method by also handling cases where the next input is larger in magnitude than the running sum&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. In such cases, the original Kahan formula could suffer error; Neumaier’s version effectively swaps roles when a huge value comes in. Its pseudocode is:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight&amp;gt;&lt;br /&gt;
function NeumaierSum(array):&lt;br /&gt;
    sum = 0.0&lt;br /&gt;
    compensation = 0.0&lt;br /&gt;
    for each x in array:&lt;br /&gt;
        t = sum + x&lt;br /&gt;
        if (fabs(sum) &amp;gt;= fabs(x))&lt;br /&gt;
            compensation += (sum - t) + x;&lt;br /&gt;
        else&lt;br /&gt;
            compensation += (x - t) + sum;&lt;br /&gt;
        sum = t;&lt;br /&gt;
    return sum + compensation;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This algorithm ensures the compensation covers both scenarios: when the sum is the bigger addend or when the new term is bigger&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. In the end, it adds the final &amp;lt;code&amp;gt;compensation&amp;lt;/code&amp;gt; once to the result. Neumaier’s method can produce the correct result in cases where basic Kahan fails. For instance, summing [1.0, $+10^{100}$, 1.0, $-10^{100}$] in double precision: regular Kahan summation might give 0.0 (losing the small terms), whereas Neumaier’s algorithm yields 2.0 exactly&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. There are also higher-order compensation methods (carrying multiple compensation terms, e.g., the &#039;&#039;&#039;Klein summation&#039;&#039;&#039; variant&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt; or using arbitrary precision for intermediate sums) that further reduce error at increased computational cost&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
In summary, compensated summation algorithms trade extra computation for greatly improved numerical accuracy. They are especially valuable when summing large arrays of numbers with varying magnitudes, where naive summation could lose most of the low-order bits. Many scientific computing environments provide compensated sum routines for this reason. In deep learning, however, a different approach to improved precision is often taken: &#039;&#039;&#039;using a wider accumulator type&#039;&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
=== 3.4. Mixed-Precision Accumulation ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Mixed-precision accumulation&#039;&#039;&#039; refers to performing arithmetic in a higher precision than the input data. A common scenario is accumulating sums in 32-bit floats when the operands are 16-bit floats. Rather than implementing a compensation algorithm, one can simply use a wider accumulator so that intermediate rounding error is smaller. This is effectively what modern GPUs do for tensor operations: for example, NVIDIA &#039;&#039;&#039;Tensor Cores operate on FP16 input data with an FP32 accumulator&#039;&#039;&#039; (the 16-bit products are accumulated in 32-bit)&amp;lt;ref name=&amp;quot;ref6&amp;quot;/&amp;gt;. In this way, summing 2048 products in a dot product has far less precision loss than it would with a 16-bit accumulator. Hardware that supports mixed precision can give the best of both worlds: fast low-precision multiplies and high-precision accumulation.&lt;br /&gt;
&lt;br /&gt;
Software can also leverage mixed precision. For instance, one can sum an array of &amp;lt;code&amp;gt;float&amp;lt;/code&amp;gt; values into a &amp;lt;code&amp;gt;double&amp;lt;/code&amp;gt; variable, accumulating the result in double precision and then converting back to float at the end:&lt;br /&gt;
&lt;br /&gt;
This approach often yields a more accurate &amp;lt;code&amp;gt;result&amp;lt;/code&amp;gt; than summing in single precision directly, because the intermediate sum carries 52-bit precision. The final rounding to float happens only once at the end (minimizing overall rounding error). In fact, the above is one way to implement &amp;lt;code&amp;gt;math.fsum&amp;lt;/code&amp;gt; in Python (it internally does something similar with multiple partial sums in double precision).&lt;br /&gt;
&lt;br /&gt;
In machine learning training and inference, &#039;&#039;&#039;mixed precision&#039;&#039;&#039; is widely used. When using FP16 for speed, it&#039;s common to accumulate with FP32. For example, &#039;&#039;&#039;Google&#039;s TPUs use BFloat16 for multiplication but accumulate in FP32&#039;&#039;&#039; for matrix ops&amp;lt;ref name=&amp;quot;ref6&amp;quot;/&amp;gt;. AWS’s Inferentia (Neuron) hardware similarly supports FP16/BF16 matrix multiplication with FP32 accumulation&amp;lt;ref name=&amp;quot;ref7&amp;quot;/&amp;gt;. The motivation is that summing hundreds or thousands of half-precision values would otherwise introduce intolerable error or risk overflow. With an FP32 accumulator, the reduction error is drastically reduced (by about $2^{13}$ times, since FP32 has 13 more precision bits than FP16). Mixed-precision accumulation thus provides a simple and effective form of compensation: &#039;&#039;&#039;use a larger bucket to collect many small contributions&#039;&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
There is a spectrum of options: summing int8 or int16 integers might use 32-bit integers as accumulators for the same reason (to avoid overflow). Some BLAS libraries even offer &#039;&#039;extended precision accumulators&#039;&#039; for single precision dot products (accumulating in double). The downside of mixed precision is the extra memory and maybe throughput cost of the wider type. But on many platforms, the add latency is dominated by hardware design rather than bit-width, so using a 32-bit adder vs 16-bit has negligible speed impact if supported natively. The key is whether data movement or register pressure increases.&lt;br /&gt;
&lt;br /&gt;
In summary, &#039;&#039;&#039;using a higher precision accumulator is often the easiest way to improve numerical accuracy&#039;&#039;&#039; for reductions. It is a primary strategy in deep learning hardware to maintain stability while exploiting low precision for storage and initial computations.&lt;br /&gt;
&lt;br /&gt;
== Precision Formats in AI and Machine Learning ==&lt;br /&gt;
&lt;br /&gt;
Modern AI systems use a variety of numeric formats to balance speed, memory, and accuracy. We discuss common precisions and their roles:&lt;br /&gt;
&lt;br /&gt;
=== 4.1. FP32 and FP64 in Training and Inference ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;FP32 (single precision)&#039;&#039;&#039; has been the workhorse of deep learning for years. Most training scripts in frameworks like TensorFlow and PyTorch used FP32 by default, as it offers sufficient precision to propagate gradients and accumulate weight updates over millions of iterations without excessive error. With ~7 decimal digits of precision and a wide exponent range, FP32 can handle the dynamic range of activations and gradients in typical neural networks. In inference, FP32 is often more precision than strictly necessary for final predictions, but it has remained common because of its ease and the fact that many GPU operations were optimized for FP32.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;FP64 (double precision)&#039;&#039;&#039; is rarely used in deep learning model training or inference, except in specific scenarios requiring extreme precision. FP64 is common in scientific computing and certain types of numerical simulations. In neural networks, however, the extra precision is usually not needed for model accuracy—using doubles tends to yield negligible improvements in model metrics but at a large performance cost (since on GPUs FP64 throughput can be 1/2, 1/8, or even 1/32 of FP32 throughput depending on hardware). One exception might be when integrating neural nets into larger physics simulations or when solving ill-conditioned problems as part of the model. Some researchers use FP64 to diagnose numerical issues or ensure reproducibility. Generally though, deep learning has favored lower precision for efficiency, and FP64 is considered overkill for most networks&amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
That said, certain classical machine learning algorithms benefit from double precision. For example, computing the inverse of a matrix (as in a Gaussian Process regression or Kalman filter embedded in a model) can incur large rounding errors in single precision, so using FP64 can significantly improve the result&amp;lt;ref name=&amp;quot;ref1&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref1&amp;quot;/&amp;gt;. The ONNX standard supports a float64 tensor type for such needs. In ONNX Runtime, however, emphasis has been on FP32 performance, and some pipelines default to float even if the original model was double&amp;lt;ref name=&amp;quot;ref1&amp;quot;/&amp;gt;. If a model’s prediction computation involves operations that are numerically sensitive (like matrix inversion, Cholesky decomposition, or summing extremely large counts), then FP64 may be required to avoid noticeable discrepancies&amp;lt;ref name=&amp;quot;ref1&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref1&amp;quot;/&amp;gt;. A good practice is to test a model in both float and double if you suspect precision issues; significant differences indicate an ill-conditioned calculation that might need higher precision.&lt;br /&gt;
&lt;br /&gt;
In summary, FP32 remains the &#039;&#039;&#039;default&#039;&#039;&#039; format for most training and inference due to its balance of accuracy and efficiency&amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;, while FP64 is reserved for niche cases or precision-critical subroutines. FP32 typically achieves the same accuracy as FP64 for end-to-end deep learning tasks, because network training can often compensate for small noise, and other sources of error (like generalization error) dominate. Therefore, the community trend has been moving to &#039;&#039;&#039;lower&#039;&#039;&#039; precisions, not higher, to gain speed — provided the models still converge and perform well.&lt;br /&gt;
&lt;br /&gt;
=== 4.2. FP16 and BFloat16 in Deep Learning ===&lt;br /&gt;
&lt;br /&gt;
To further speed up neural network training and inference, &#039;&#039;&#039;half-precision (FP16)&#039;&#039;&#039; was adopted, especially with the advent of GPUs supporting FP16 math at twice the rate of FP32. Using FP16 reduces memory storage and bandwidth by 50% compared to FP32, which is significant for large models. However, FP16’s small exponent range and precision posed challenges: training a large network purely in FP16 can fail due to gradient underflow/overflow or weight updates not registering (because changes are below the 0.001 quantization step). To address this, frameworks introduced &#039;&#039;&#039;mixed-precision training&#039;&#039;&#039;, where compute-intensive parts (matrix multiplies, convolutions) run in FP16, but certain accumulations and model parameters remain in FP32. For instance, NVIDIA’s &#039;&#039;&#039;Tensor Core&#039;&#039;&#039; units require FP16 (or BF16) inputs but accumulate results in FP32, effectively ensuring that reductions are done in higher precision&amp;lt;ref name=&amp;quot;ref6&amp;quot;/&amp;gt;. Research showed that with proper loss scaling and mixed precision, models can train to FP32-level accuracy using mostly FP16 arithmetic, achieving significant speedups.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;BFloat16 (BF16)&#039;&#039;&#039; emerged as an alternative 16-bit format primarily from Google’s TPU training systems. BF16 has a larger exponent (8 bits, same as FP32) but only 7 fraction bits&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. This means BF16 can handle the &#039;&#039;&#039;same dynamic range&#039;&#039;&#039; as FP32 (no overflows where FP32 wouldn’t) but at the cost of even lower precision than FP16 (BF16 has about 2–3 decimal digits of precision vs FP16’s ~3–4). Despite its coarse precision, BF16 turns out to be effective for training deep networks because the algorithms (stochastic gradient descent, etc.) are somewhat robust to that noise, and having the wide range prevents catastrophic loss of signal (no gradient will overflow to Inf unless it was going to in FP32 anyway). Many training frameworks (PyTorch, TensorFlow) support BF16 training on hardware like &#039;&#039;&#039;TPU, NVIDIA A100+, Intel CPUs with AVX-512 BF16, etc&#039;&#039;&#039;. Typically, when using BF16, &#039;&#039;&#039;accumulations are still done in FP32&#039;&#039;&#039; (for example, a TPU v2/v3 multiplies BF16 matrices and accumulates into an FP32 result for the dot product&amp;lt;ref name=&amp;quot;ref6&amp;quot;/&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
In inference, both FP16 and BF16 are used to deploy models for faster speed or lower memory. FP16 is popular on NVIDIA GPUs (which have specialized FP16 units). BF16 is advantageous on TPUs and some CPUs that support it, and increasingly on GPUs (Ampere and newer GPUs also support BF16 arithmetic). The choice between FP16 and BF16 can depend on hardware: BF16’s advantage is easier software conversion (you can often take an FP32-trained model and cast weights to BF16 with minimal loss, since the dynamic range is intact), whereas FP16 might require adjusting the model or ensuring values fit in range (sometimes needing techniques like maintaining a master copy of weights in FP32 during training, or handling out-of-range values).&lt;br /&gt;
&lt;br /&gt;
For ONNX, both Float16 and BFloat16 types are part of the standard. ONNX models can be converted to FP16 (truncating weights and inserting cast nodes for computations) using tools like &amp;lt;code&amp;gt;float16_converter&amp;lt;/code&amp;gt; in ONNX Runtime&amp;lt;ref name=&amp;quot;ref8&amp;quot;/&amp;gt;. ONNX Runtime on GPUs will typically execute those FP16 ops with mixed precision on tensor cores (FP16 input, FP32 accumulate), whereas on CPU it may emulate FP16 or upcast to FP32 (as many CPU implementations do not natively support half-precision math)&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;. In fact, as an example, running an ONNX model in FP16 on a CPU might insert hidden Cast operations to FP32 behind the scenes, because the CPU kernel for that op only exists in FP32&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;. This ensures correctness (the computation is done in higher precision) but can reduce performance. We will revisit such backend-dependent behavior in Section 5.3.&lt;br /&gt;
&lt;br /&gt;
In summary, &#039;&#039;&#039;FP16 and BF16&#039;&#039;&#039; are now established as important low-precision formats in deep learning. FP16 offers more precision bits, which can matter for e.g. small differences, but has a limited range (care needed to avoid overflow). BF16 offers safety in range but less precision (which can slightly affect convergence or final accuracy in some cases). Both typically rely on FP32 for accumulation to achieve acceptable accuracy. When deploying models, one should choose a format supported efficiently by the target hardware and verify that the accuracy is not significantly degraded. Often, CNNs and transformers can run in FP16/BF16 with virtually no drop in score, but certain operations (like softmax, see Section 6.3) might need special handling.&lt;br /&gt;
&lt;br /&gt;
=== 4.3. FP8 Formats (E4M3, E5M2) and Scaling ===&lt;br /&gt;
&lt;br /&gt;
The push for ever lower precision has led to experimental &#039;&#039;&#039;8-bit floating point (FP8)&#039;&#039;&#039; formats for deep learning. In 2022, researchers from NVIDIA, Intel, and Arm proposed an FP8 standard with two variants: &#039;&#039;&#039;E4M3&#039;&#039;&#039; (4 exponent bits, 3 fraction bits) and &#039;&#039;&#039;E5M2&#039;&#039;&#039; (5 exponent bits, 2 fraction bits)&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;. Both have 1 sign bit, for a total of 8 bits. E5M2 has a wider exponent range (5 bits gives bias 15) but one fewer mantissa bit than E4M3. The idea is to use E4M3 for numbers that need more precision (like weights or activations around 1.0) and E5M2 for numbers that need more dynamic range (like gradient updates which can be very small or large). In fact, the study found that using E4M3 for forward activations/weights and E5M2 for backward gradients can allow training of large networks with FP8 to accuracy on par with FP16 training&amp;lt;ref name=&amp;quot;ref11&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
FP8 formats are &#039;&#039;not&#039;&#039; IEEE 754 standard (though IEEE is considering lower precision in its 2019 revision). Notably, the E4M3 format as used by NVIDIA does not allocate any bit-pattern for $+\infty$ or $-\infty$ – it has only NaN for invalid results, using that freed bit pattern to extend the representable range by one exponent value&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;. Essentially, E4M3 can represent numbers up to ~$2^{15}$ (around 3e4) instead of capping at infinity at $2^{16}$, by foregoing infinities&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;. Variations also exist regarding negative zero and how NaNs are handled&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;. For instance, GraphCore’s IPU uses FP8 formats that disallow negative zero and infinities, to maximize useable range&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Using 8-bit floats for neural networks requires additional techniques, chiefly &#039;&#039;&#039;scaling&#039;&#039;&#039;. Because 8-bit has very limited precision and range, one typically applies a power-of-two scale factor to groups of values to normalize them into an FP8-friendly range. This is akin to block floating-point or “microscaling” per layer or per tensor. For example, one might keep a FP32 scale for each channel or tensor, and represent values as $(FP8\_value) \times 2^{scale}$. The ONNX standard has introduced support for FP8 in version 1.15, including types for E4M3 and E5M2 (with variations FNUZ: finite-only and no negative zero)&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;, and even a type called &#039;&#039;&#039;E8M0&#039;&#039;&#039; which is an 8-bit exponent-only number used purely as a scale factor&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;. The E8M0 type has 8 exponent bits, no mantissa, effectively representing powers of two from $2^{-127}$ to $2^{128}$ for scaling purposes&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;. This indicates an approach where an FP8 tensor would be accompanied by an E8M0 scale (or scales) to interpret its value correctly.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Rounding&#039;&#039;&#039; becomes critical when casting values to FP8. The quantization error is much larger relative to FP16 or FP32. Studies have shown that choosing the right rounding mode (e.g., round-to-nearest, stochastic rounding, etc.) and clamping behavior (saturating to max instead of inf) can affect model accuracy&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;. ONNX added a &amp;lt;code&amp;gt;round_mode&amp;lt;/code&amp;gt; attribute to its Cast operator to allow controlling this for FP8&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;. In particular, one paper found that &#039;&#039;&#039;rounding up with saturation&#039;&#039;&#039; (round-to-nearest ties away from 0, and clamp overflows to max finite rather than inf) gave better accuracy in large language model training than other rounding modes&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
At present, FP8 is on the cutting edge. NVIDIA’s H100 GPU supports FP8 tensor cores (for both E4M3 and E5M2), and early results show minimal accuracy loss on some vision and NLP tasks when properly tuned. However, using FP8 requires fine-tuning hyperparameters like scaling factors for each layer and perhaps tweaking training procedures. ONNX’s inclusion of FP8 types means you can represent and transport models using FP8 weights or activations, but actual hardware support is limited and typically paired with calibration or dynamic scaling logic outside of ONNX graph (or using custom ONNX functions for scaling). It remains an &#039;&#039;&#039;open research area&#039;&#039;&#039; how to fully automate FP8 quantization for arbitrary models without accuracy loss.&lt;br /&gt;
&lt;br /&gt;
In summary, FP8 formats promise further compression of neural network computation and data. They are an order of magnitude more error-prone than FP16, but when combined with careful scaling, mixed usage of E4M3/E5M2, and maybe modified training techniques, they have shown surprising success. ONNX is actively evolving to support these ultra-low precision types, signaling their potential importance in future AI inference deployments.&lt;br /&gt;
&lt;br /&gt;
== Accumulation Semantics in ONNX ==&lt;br /&gt;
&lt;br /&gt;
One of the roles of the ONNX specification is to define the mathematical behavior of operators (Conv, MatMul, Sum, etc.) in a framework-agnostic way. However, ONNX mostly defines &#039;&#039;what&#039;&#039; an operator computes in exact arithmetic, not &#039;&#039;how&#039;&#039; the computation is carried out in finite precision. This leaves certain details – like accumulator precision or summation order – up to the implementation (runtime or hardware). We&#039;ll examine how ONNX treats accumulation and what guarantees (or lack thereof) are provided.&lt;br /&gt;
&lt;br /&gt;
=== 5.1. Operator Definitions and Numerical Guarantees ===&lt;br /&gt;
&lt;br /&gt;
Each ONNX operator is described as performing an ideal mathematical function on its inputs. For example, &amp;lt;code&amp;gt;MatMul&amp;lt;/code&amp;gt; is defined as the standard matrix product of two tensors, and &amp;lt;code&amp;gt;ReduceSum&amp;lt;/code&amp;gt; computes a summation along given axes. The ONNX spec, as of current versions, &#039;&#039;&#039;does not mandate the precision of intermediate calculations&#039;&#039;&#039; beyond the types of the inputs and outputs. If an operator’s inputs and outputs are float16, the spec implies the computation is done in float16 in a sense, but it does not forbid an implementation from using higher precision internally – it simply doesn&#039;t mention it. There is &#039;&#039;&#039;no explicit guarantee of identical bit-for-bit results&#039;&#039;&#039; across different ONNX backends for floating-point ops. Instead, results are expected to be numerically close, within tolerances one would normally expect from floating-point differences (due to different hardware or algorithms). The ONNX Backend Test suite typically uses a relative or absolute error tolerance when validating outputs for this reason.&lt;br /&gt;
&lt;br /&gt;
Because of this, important details like the use of extended precision or accumulation in higher precision are left ambiguous. This has led to discussions in the ONNX community about making such behavior more explicit. For example, a proposal was raised to allow specification of accumulation precision for certain ops like MatMul&amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;. The reasoning is that as lower-precision types (float16, bfloat16, float8) become more common, knowing or controlling whether an op internally accumulates in a higher precision is important for both accuracy and reproducibility&amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;. In June 2025, an issue on the ONNX GitHub suggested adding an attribute to MatMul (and similarly Gemm, Softmax, etc.) to specify the accumulator type&amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;. By default it would use input precision (status quo), but an attribute could request (for instance) 32-bit accumulation even if inputs are 16-bit&amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;. As of this writing, however, no such attribute exists in the official spec – it&#039;s an open issue. Thus, the numerical semantics are effectively: &#039;&#039;“compute the result as if in the same type as inputs, but actual implementations may use more precision internally as an optimization.”&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
From a guarantee standpoint, ONNX operators promise the mathematically correct result assuming real-number arithmetic, but they &#039;&#039;&#039;do not guarantee bit-exact results&#039;&#039;&#039; due to floating-point effects. There is an understanding that differences can arise from different summation orders, fused operations (e.g., an implementation might use an FMA (fused multiply-add) which has one rounding instead of two), or use of extended precision registers. ONNX does not formally define rounding modes either – it relies on IEEE 754 defaults, but if a backend used something like deterministic summation or alternative rounding, ONNX has no mechanism to express that.&lt;br /&gt;
&lt;br /&gt;
In summary, ONNX&#039;s current stance is largely &#039;&#039;&#039;implementation-dependent&#039;&#039;&#039; for numerical precision. It provides minimal explicit numerical guarantees beyond type constraints. This is why an ONNX model run on two different hardware or runtime libraries can have slightly different results, especially in reduced precision. The burden is on backends to produce results &amp;quot;close enough&amp;quot; to each other that they don&#039;t affect the model&#039;s decisions in any significant way.&lt;br /&gt;
&lt;br /&gt;
=== 5.2. MatMul, Conv, and Reduction Operators ===&lt;br /&gt;
&lt;br /&gt;
Consider ONNX &amp;lt;code&amp;gt;MatMul&amp;lt;/code&amp;gt;: it takes two tensors (often two matrices) and produces their matrix product. In pure math, $C_{ij} = \sum_k A_{ik} &#039;&#039; B_{kj}$. If A and B are float16, should that sum be computed in float16 or float32? As discussed, ONNX doesn’t specify – it simply says the output is a float16 matrix containing the product. In practice, many backends will &#039;&#039;promote&#039;&#039; the computation: for example, NVIDIA GPUs will do the multiplication and addition in a mixed FP16/FP32 mode by default (because of tensor cores), then convert the final result back to float16. ONNX Runtime on CPU might not support float16 multiplication natively, so it will insert cast ops to float32, perform the MatMul in float32, then cast back to float16&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;. From ONNX’s perspective, as long as the final result is within acceptable error of the “ideal” float16 computation, it’s fine. But note, an “ideal” float16 computation (where every multiply and add is rounded to FP16) might actually be &#039;&#039;less accurate* than what the backend did with FP32. This means that a backend using higher precision can produce a result that is not bit-equal to a pure FP16 implementation – it&#039;s actually closer to the true real number result. This is usually considered a good thing (improved accuracy), but it does highlight a lack of consistency: another backend might do everything in pure FP16 and get a slightly different answer.&lt;br /&gt;
&lt;br /&gt;
The same situation occurs for &#039;&#039;&#039;convolution&#039;&#039;&#039; (&amp;lt;code&amp;gt;Conv&amp;lt;/code&amp;gt; op) which is essentially a batched sum of products (like MatMul in sliding window), and for explicit reduction ops like &amp;lt;code&amp;gt;ReduceSum&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;ReduceMean&amp;lt;/code&amp;gt;, etc. If you have an ONNX &amp;lt;code&amp;gt;ReduceSum&amp;lt;/code&amp;gt; that sums 1000 float16 values, the spec doesn’t say how to sum them. A GPU kernel might sum in a tree using FP32 accumulators; a naive implementation might sum in order with FP16 and possibly overflow or underflow intermediate steps. Both are allowed as long as they output a float16 result that is “reasonable.” In practice, ONNX Runtime and others take care to avoid obviously bad behavior (like overflow) by using higher precision or by splitting the sum. But again, it’s not mandated by the ONNX standard explicitly.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Softmax&#039;&#039;&#039; is an interesting case: Softmax involves an internal sum of exponentials. ONNX defines the Softmax operator as $$\mathrm{Softmax}(x)_i = \frac{\exp(x_i)}{\sum_j \exp(x_j)},$$ typically applied along a specified axis. Directly computing this in float16 can be problematic for large vectors, as discussed in Section 6.3. Indeed, the issue of accumulation precision arises: should the sum $\sum_j \exp(x_j)$ be done in FP16 or can it be in FP32? Many implementations will do it in FP32 even if $x$ is FP16, because the dynamic range of $\exp(x)$ is large. The ONNX Softmax spec doesn’t detail this, but it expects the result to be a proper probability distribution (summing to 1.0 within normal floating error). A pure FP16 softmax on, say, 1000 elements could sum to something like 0.996 or 1.004 due to rounding, whereas FP32 accumulation would be much closer to 1.000. ONNX likely allows both; the small difference is usually inconsequential, but if it were large (say the FP16 sum overflowed to inf), that would be a correctness issue. A robust backend will prevent that (by using the common max-subtraction trick and higher precision accumulation). The ONNX issue 7072 indeed calls out Softmax as another op where explicit control of accumulation precision would be useful&amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
In summary, &#039;&#039;&#039;MatMul, Conv, and reduction ops&#039;&#039;&#039; in ONNX are defined mathematically but with &#039;&#039;implicit&#039;&#039; numerical behavior. Implementations commonly use higher precision for intermediate sums especially when input is low precision, but this is not standardized. Consequently, results can differ slightly between implementations. For critical applications, one must be aware of these differences. A model exported to ONNX with float16 weights might perform slightly differently on one runtime vs another if one does all float16 math and the other uses float32 accumulations. Both conform to ONNX. As a user, if you require a certain precision for accumulation, currently you have to ensure it by the way you export or run the model (for example, insert manual cast nodes in the ONNX graph to force certain ops to FP32). We may see future ONNX versions address this with attributes or metadata as mentioned, but for now these ops are effectively “compute with best effort accuracy”.&lt;br /&gt;
&lt;br /&gt;
=== 5.3. Backend and Hardware-Dependent Behavior ===&lt;br /&gt;
&lt;br /&gt;
Because ONNX is implemented by many backends (ONNX Runtime, TensorRT, CoreML, PyTorch JIT, etc.), and these in turn run on varied hardware (CPU, GPU, accelerators), the numeric behavior can be backend-dependent. Some examples:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;CPU backends:&#039;&#039;&#039; Many CPUs do not have native float16 vector arithmetic (with a few exceptions in recent CPUs). ONNX Runtime’s default CPU execution provider will often upcast float16 to float32, compute, then downcast&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;. This ensures accuracy but means that if you thought you were getting a full float16 pipeline, you’re not – the CPU is essentially ignoring the low precision in compute. On the other hand, x86 CPUs have extended 80-bit precision in x87 registers (though less used now) and 64-bit SIMD registers for doubles; some compiler optimizations might carry intermediate results in higher precision. This could make CPU results slightly different from a GPU’s if, say, a sum is done in 80-bit on CPU versus 32-bit on GPU. Generally, modern compilers avoid such extended precision unless explicitly asked, to improve reproducibility.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;GPU backends:&#039;&#039;&#039; GPUs often perform summations in parallel using tree reductions. The order of operations can change depending on how threads are scheduled. This means even on the same GPU, summing an array in one launch versus another might not give bit-identical results if the internal parallel reduction algorithm differs (though it should be deterministically the same given the same library version and input, but if you change GPU model or BLAS library, it might differ). Also, GPUs have fused multiply-add (FMA) instructions that do $(a*b + c)$ in one step with one rounding, which can produce slightly different results than doing the multiply and add separately (which would round twice). If an ONNX backend uses a fused kernel (like a GEMM library) it might yield different low-order bits than a simple loop.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Vendor libraries:&#039;&#039;&#039; ONNX Runtime can use vendor BLAS or DNN libraries (MKL, cuDNN, TensorRT, etc.). These libraries often implement their own strategies for numerical stability. For example, NVIDIA’s cuDNN might use FP32 accumulation for FP16 inputs by default. TensorRT (NVIDIA’s inference engine) will sometimes automatically promote precisions or insert scaling to maintain stability&amp;lt;ref name=&amp;quot;ref13&amp;quot;/&amp;gt;. On the other hand, if you explicitly request FP16 in TensorRT with no FP32 fallback, it might do the entire op in FP16 and you’d see more error. Some libraries offer knobs: TensorRT allows choosing FP32, FP16, or INT8 precision for each layer, and will use FP32 internally for safety in some cases unless forced otherwise.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Hardware-specific quirks:&#039;&#039;&#039; As another example, NVIDIA’s &#039;&#039;&#039;TensorFloat-32 (TF32)&#039;&#039;&#039; on Ampere GPUs is a precision mode where matrix multiplications use 10-bit mantissa (so about FP16 precision for the multiply) but keep 8-bit exponent (range like FP32). By default, CUDA libraries use TF32 for FP32 matrix multiplies on Ampere to speed them up, unless you disable it. This means if you run an ONNX model with FP32 on an A100 GPU, your matmuls might actually be lower precision than FP32 (though higher than FP16)&amp;lt;ref name=&amp;quot;ref6&amp;quot;/&amp;gt;. It’s an example of hardware choosing an intermediate precision for performance. From ONNX’s perspective, the output is still FP32 and (hopefully) within error tolerances, but a user might be surprised that it’s not a “true” FP32 computation internally. If exactness is needed, one has to turn off TF32.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Non-determinism:&#039;&#039;&#039; Some backends (especially on GPUs) allow non-deterministic execution for performance. For instance, atomic adds in parallel reductions can produce results that vary run-to-run because the order of accumulation is race-condition-dependent. ONNX’s spec doesn’t cover this explicitly, but it’s something to consider. In ONNX Runtime, there are settings for deterministic computing, but by default it may use all available performance optimizations, even if results vary at the 1e-7 level.&lt;br /&gt;
&lt;br /&gt;
In short, the numerical behavior under ONNX can depend on the combination of backend software and hardware capabilities. The lack of a strict spec for accumulation means implementations have freedom to use clever strategies (higher precision, reordering) to improve accuracy or speed. Usually this is positive (e.g. getting better accuracy than a naive approach). But it could also lead to slight inconsistencies. For example, you might find that exporting a PyTorch model to ONNX and running it with one engine yields outputs slightly different from PyTorch’s – not because ONNX is wrong, but because the summation or convolution was done differently (maybe PyTorch summed in FP32 as well, but maybe not in exactly the same way).&lt;br /&gt;
&lt;br /&gt;
The key takeaway is that ONNX &#039;&#039;by design&#039;&#039; abstracts away these details, which is good for portability but means you need to &#039;&#039;&#039;trust the backend&#039;&#039;&#039; or verify its precision behavior for sensitive models. If necessary, you can enforce certain behavior by inserting ops (for example, if you want to ensure FP32 accumulation, you could insert a cast to FP32 for inputs of a MatMul and cast the result back to FP16 – this way the ONNX graph itself mandates FP32 compute in between). This of course sacrifices some performance or device-specific magic.&lt;br /&gt;
&lt;br /&gt;
Looking forward, as mentioned, there are proposals to enrich ONNX’s specification regarding precision. This might include explicit flags for “exact summation” or “accumulate in int32” etc., which would make models more self-descriptive but also potentially less portable (if a backend doesn’t support that, what then?). For now, users should be aware of these backend differences. Most of the time, well-engineered ONNX runtimes yield results close enough that they don&#039;t affect the application (e.g., classification top-1 remains the same even if low-level sums differ by 1e-5). But for high-stakes numerical computations, testing and maybe adjusting the model or backend settings is prudent.&lt;br /&gt;
&lt;br /&gt;
== Numerical Stability in Deep Learning Models ==&lt;br /&gt;
&lt;br /&gt;
Deep learning computations involve many operations that, while theoretically well-defined, can be sensitive to numerical issues when implemented in finite precision. We&#039;ll discuss a few common scenarios in neural networks where numerical stability is a concern, and how precision and accumulation play a role.&lt;br /&gt;
&lt;br /&gt;
=== 6.1. Dot Products and Large-Scale Reductions ===&lt;br /&gt;
&lt;br /&gt;
Neural networks are full of dot products: every fully-connected layer computes $\mathbf{y} = W\mathbf{x}$, which is a series of inner products between weight vectors and the input. Convolutional layers compute weighted sums of local regions. Attention mechanisms compute large matrix products of query/key/value matrices. All these boil down to summing a lot of terms: $\sum_{k=1}^K a_k b_k$. When $K$ is large (it could be in the thousands or more for a single neuron in a fully connected layer of a large model), the potential for floating-point error in that sum increases.&lt;br /&gt;
&lt;br /&gt;
If using FP32, the error may still be negligible in many cases, but in lower precision like FP16, it can become significant. For example, consider a dot product of length 2048 in FP16. Each product is in FP16 (with ~3 decimal digits precision) and if accumulated in FP16, the worst-case error could be on the order of $2048 \times \varepsilon_{FP16} \approx 2048 \times 0.0009766 \approx 2$ (i.e., you could lose a couple units in the third decimal place relative accuracy in worst case). Even in random error accumulation, you might see error on the order of $\sqrt{2048} \times 0.0009766 \approx 0.044$ or 4.4% (though this is a rough estimate). That’s quite significant. Fortunately, as discussed, most hardware will accumulate such sums in a higher precision like FP32, dramatically reducing that error (~7 orders of magnitude smaller $\varepsilon$). But if it didn’t, the network’s outputs could get noisy. A fully-connected layer with many inputs might output slightly different results depending on summation order if done in low precision.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Large-scale reductions&#039;&#039;&#039; also occur in operations like global average pooling (summing all elements of a feature map) or computing the total loss by summing loss over batch samples. If one sums 100,000 values in FP32, the worst-case error could be on the order of 100,000 &#039;&#039; 1e-7 ≈ 1e-2 (1% uncertainty), though typical error would be much less. In FP16, summing 100k values is likely impossible without overflow: as noted, adding ~10^4 moderate numbers can overflow FP16&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. So large reductions &#039;&#039;must* be handled carefully (either in chunks or in higher precision). Frameworks generally do: for example, when performing a sum reduction on GPU, libraries break it into parallel blocks that accumulate in registers (usually 32-bit registers, even if inputs are 16-bit). &lt;br /&gt;
&lt;br /&gt;
Another aspect is that many dot products in neural nets involve values of varying magnitude. If the distribution of terms is such that some are much larger than others, the smaller terms might not contribute much if summed last. Ideally, one might sort by magnitude or use pairwise summation to improve that. Most BLAS libraries effectively do a pairwise strategy due to their parallel structure. So, while worst-case analyses are grim, practical implementations mitigate a lot of it.&lt;br /&gt;
&lt;br /&gt;
However, certain layers can suffer if not handled properly. One famous example is in RNNs or LSTMs: if one uses FP16 naive summation for the gating mechanism, errors could accumulate over many time steps and cause drift or instability. In modern practice, recurrent layers are often run in FP16 with FP32 accumulators to avoid this.&lt;br /&gt;
&lt;br /&gt;
In summary, &#039;&#039;&#039;dot products and large reductions need sufficient precision&#039;&#039;&#039; or robust algorithms. If you are implementing a custom kernel (say a custom ONNX op) and you have to sum a huge array, you should consider using a technique from Section 3 (pairwise summation, Kahan, or at least double accumulator if possible). Many deep learning computations, by virtue of hardware and library design, already do something like this under the hood. But it&#039;s always good to be aware, especially if pushing into lower precision territories.&lt;br /&gt;
&lt;br /&gt;
=== 6.2. Cancellation, Dynamic Range, and Conditioning ===&lt;br /&gt;
&lt;br /&gt;
We introduced &#039;&#039;cancellation&#039;&#039; earlier in the context of summation. In deep learning, cancellation can appear in a few places. One is in the computation of certain norms or variances. For instance, Batch Normalization computes mean and variance of activations. The variance formula $\mathrm{Var}(x) = E[x^2] - (E[x])^2$ is subject to catastrophic cancellation if $E[x]$ and $\sqrt{E[x^2]}$ are close in magnitude (which can happen if the variance is small compared to the mean). If implemented naively in single precision, you might get a negative variance due to numerical error (which would then be clamped to zero or sqrt would yield NaN). Robust implementations compute variance in a single pass or increase precision. Indeed, in many frameworks, batchnorm accumulation is done in FP32 even if the activations are FP16, precisely to avoid numerical issues in computing mean and variance. ONNX itself doesn’t specify this, but an ONNX backend is likely to do batchnorm stats in higher precision for safety. The &#039;&#039;conditioning&#039;&#039; of the variance calculation is poor when variance is tiny relative to mean, so it&#039;s a spot where higher precision is needed.&lt;br /&gt;
&lt;br /&gt;
Another example is in certain loss functions or regularizations. If you subtract two large numbers to get a small difference, you lose precision. Say you have two nearly identical predictions and you subtract them as part of some operation – if done in float16, you might get zero whereas float32 would have a slight difference. This could affect, say, a gradient if that difference was supposed to propagate information. Fortunately, neural nets often avoid directly subtracting large nearly equal numbers; but it can happen. In attention mechanisms, the &#039;&#039;score normalization&#039;&#039; (softmax) subtracts a max to avoid huge exponentials, which is good for range but if multiple scores are close to the max, you subtract numbers that are close. However, those subtractions are not catastrophic in the same way; they&#039;re intentional to reduce range and are typically fine in FP32 or even FP16.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Dynamic range&#039;&#039;&#039; issues are everywhere in deep learning. Activation functions like ReLU and exponential can produce very large or very small values. If subsequent computations don’t account for that, you get infinities or zeros. For example, consider a network output that produces a logit of 100 in FP16. $\exp(100)$ in FP16 overflows to infinity (since FP16 max ~ 6e4 and $e^{100} \sim 3.7e43$). If you then do softmax, that one infinity will dominate the sum and you might get a probability of 1 for that class (or NaNs if not handled). The standard softmax implementation subtracts the max logit, so in this case it would subtract 100 from all logits, making the largest exponent $\exp(0)=1$ and others tiny – this avoids overflow. But what about underflow? If you had a very negative logit, $\exp(-100)$ in FP16 would underflow to 0. In softmax, that just contributes ~0 probability, which is fine (it effectively is zero anyway). Underflow tends to be less dangerous than overflow in inference — underflow usually just means “this probability/neuron is effectively 0,” whereas overflow can poison the results with NaNs or Infs. &lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Conditioning&#039;&#039;&#039; refers to how sensitive a function is to input perturbations. Neural networks themselves as functions can be poorly conditioned (e.g., tiny changes in weights cause big changes in output for some architectures), but typically they are designed to be reasonably well-behaved. Certain internal computations like matrix inversion (if one does a pseudoinverse or something for a layer) can be extremely ill-conditioned if the matrix is near singular. That again would need double precision perhaps. In ML, one common ill-conditioned operation is the &#039;&#039;&#039;softmax denominator&#039;&#039;&#039;: $\sum_j e^{x_j}$ can be very large and also very sensitive to changes in the largest $x_j$. However, the softmax is more stable if computed carefully (max trick). Another is &#039;&#039;&#039;normalization&#039;&#039;&#039;: computing $\frac{x - \mu}{\sigma}$ for layer norm or batch norm – if $\sigma$ is very small, then the division blows up noise. In those cases, a small $\epsilon$ is usually added to $\sigma^2$ to prevent instability (this is a model-level fix, not a numeric one per se, but it acknowledges numeric limits).&lt;br /&gt;
&lt;br /&gt;
In training, &#039;&#039;&#039;optimizers&#039;&#039;&#039; can suffer from precision issues. For example, the Adam optimizer keeps a moving average of squared gradients; in half precision, these small updates might flush to zero or not change at some point. Some research found that certain adaptive optimizers break in FP16 because the small differences are lost (&amp;quot;Why FP16 training breaks NAdam&amp;quot; etc.). The solution is often to keep the optimizer states in FP32 while doing the bulk of compute in FP16. This again is a form of mixed precision.&lt;br /&gt;
&lt;br /&gt;
From an inference perspective, once the model is trained, the main concerns are avoiding overflow in activations and ensuring summations (like in softmax or reductions) are done with enough precision. The model’s weights are fixed, so we don&#039;t have to worry about cumulative error over iterations as in training, just the forward pass.&lt;br /&gt;
&lt;br /&gt;
=== 6.3. Normalization Layers and Softmax ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Normalization layers&#039;&#039;&#039; (BatchNorm, LayerNorm, etc.) and &#039;&#039;&#039;Softmax&#039;&#039;&#039; are specifically worth examining for numerical stability:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Batch Normalization:&#039;&#039;&#039; During inference, batchnorm applies an affine transform using precomputed mean and variance (from training). It’s basically: $y = \frac{x - \mu}{\sqrt{\sigma^2 + \epsilon}} \gamma + \beta$. This is a stable operation as long as $\epsilon$ is provided (to avoid division by zero). Since $\mu, \sigma^2$ are constants in inference, it reduces to linear ops which are fine. The main numerical work (computing mean/variance) happened during training or model calibration, and frameworks usually did that in FP32. So batchnorm at inference time is not a source of instability (it’s just a scale and shift of the data).&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Layer Normalization / Group Normalization:&#039;&#039;&#039; These compute mean/var on the fly for each sample (and possibly channel group). In inference, they still need to compute these statistics for each forward pass. If implemented in low precision, the variance calculation can suffer cancellation as mentioned. However, it&#039;s likely that inference engines compute the mean and variance in FP32 even if inputs are FP16 (especially since these are per-sample operations, not huge batch reductions – it&#039;s feasible to do in higher precision). If someone implemented layer norm purely in FP16, they might see issues when the variance is very small. Usually there’s an $\epsilon$ added which might dominate a tiny variance and thus mitigate catastrophic cancellation (the result might just output something mostly from $\epsilon$). In summary, normalization layers are usually fine as long as the implementation uses enough precision for the summation of squares.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Softmax:&#039;&#039;&#039; Softmax is notorious if done improperly. The safe algorithm: subtract the maximum input from all inputs (to bring the largest exponent to 1.0), exponentiate, sum them up, divide each exponent by the sum. The max subtraction handles overflow in $\exp()$. But what about the sum? As discussed, if there are $N$ elements in the softmax, the sum is at most $N$ (when all inputs equal the max). In classification, $N$ might be the number of classes. For ImageNet, $N=1000$, so worst sum ~1000 (which is fine in FP16, as 1000 &amp;lt;&amp;lt; 65504). For an extreme case like a language model, $N$ could be 50,000 (vocab size). Sum could be 50,000 in worst case (everyone equal probability). 50,000 is still below 65504, so actually an FP16 sum would &#039;&#039;not&#039;&#039; overflow in that scenario, and the rounding error on a sum of 50k identical terms would be maybe a few ULPs (which at 50k magnitude might be ~0.5 difference worst-case). So maybe softmax summation in FP16 is not catastrophic for output probabilities – you’d get essentially 1.0 after normalization instead of 0.99999 in some case. Usually fine.&lt;br /&gt;
&lt;br /&gt;
:The bigger issue is if one or a few terms dominate the softmax. If one term is much larger, the others underflow to 0 after the max subtraction. That’s fine: you get a one-hot distribution essentially. If the model expected a softer distribution, that could be a problem. But if the logits were really that far apart, even FP32 would give nearly one-hot. So not really a precision issue, more of a model property.&lt;br /&gt;
&lt;br /&gt;
:However, the &#039;&#039;&#039;stability during training&#039;&#039;&#039; is a big issue. The GitHub thread we saw (ColossalAI) mentioned training a 130B model where FP16 softmax in attention caused training to occasionally blow up&amp;lt;ref name=&amp;quot;ref14&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref14&amp;quot;/&amp;gt;. They observed attention scores in some heads ranging up to 1e4 or as low as 1e-3&amp;lt;ref name=&amp;quot;ref14&amp;quot;/&amp;gt;. 1e4 exponent in FP16 is definitely Inf (because $\exp(10^4)$ is astronomically huge, but even exp(100) is inf in half). The method CogView introduced (Precision Bottleneck Relaxation) involved subtracting the max &#039;&#039;per head&#039;&#039; to reduce range&amp;lt;ref name=&amp;quot;ref14&amp;quot;/&amp;gt;—which is basically the softmax max trick at a finer granularity or something similar. In the end, they found &#039;&#039;&#039;the simplest fix was to compute softmax in FP32&#039;&#039;&#039; for those layers&amp;lt;ref name=&amp;quot;ref14&amp;quot;/&amp;gt;. They reported that it had negligible speed impact but significantly improved stability&amp;lt;ref name=&amp;quot;ref14&amp;quot;/&amp;gt;. This highlights a general principle: certain operations like softmax (and sometimes matrix multiplications in attention) benefit from higher precision, and using FP32 in a few critical spots can save a mixed-precision training from crashing. In inference, using FP32 for softmax is also a safeguard if one is uncertain. It costs little because softmax is not a heavy compute compared to matrix multiplies.&lt;br /&gt;
&lt;br /&gt;
:ONNX does have a Softmax operator; if the input is float16, a backend might do exactly what they described: internally cast to float32 for the softmax computation, then cast output back to float16. Some inference engines (like DeepSpeed’s inference or others) keep attention softmax in FP32 even if rest is FP16, for robustness&amp;lt;ref name=&amp;quot;ref14&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Apart from softmax, other places where exponentials occur are in loss functions like cross-entropy (which uses log-softmax internally), or in activation functions like Sigmoid and Tanh (which are basically scaled logistic functions). Sigmoid($x)=1/(1+e^{-x})$. If $x$ is moderately large positive, $e^{-x}$ underflows to 0 in FP16 if $x&amp;gt;~11$. That gives a sigmoid output of 1.0 exactly. In FP32, $e^{-x}$ for $x=11$ is about $5e-5$, not underflow, so sigmoid ~0.99995. So FP16 will saturate some outputs to exactly 0 or 1 sooner than FP32 would. This can slightly change behaviors (e.g., a neuron might go to full saturation a bit early). Usually, this isn’t a huge deal, but it could make gradients zero earlier. In inference, we don’t care about gradients, just the output value. Getting exactly 1.0 vs 0.99995 probably has no practical difference for a classification output. But one should be aware that &#039;&#039;&#039;activation functions can saturate faster&#039;&#039;&#039; in lower precision due to limited range or precision. ReLU has no issue (it’s linear or 0), but exponential-based activations do. In practice, again, frameworks often simply compute those in the given precision and accept the slight difference. If it were a problem, one could do those in FP32 as well.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Summary:&#039;&#039;&#039; Numerical stability in inference is maintained by using robust computation methods: subtracting maxima in softmax/LogSumExp, adding epsilon in denominators for normalization, and using higher precision for summations or critical steps. Most of these are built into the algorithms. ONNX models assume these standard practices (they don&#039;t need to explicitly do them in the graph because the backend inherently will for ops like Softmax). It&#039;s wise when building or exporting models to ONNX to test them in the target precision and ensure there are no surprises (like an output becomes NaN or Inf due to precision). Usually if training was done in mixed precision, the model will have been vetted in that environment already.&lt;br /&gt;
&lt;br /&gt;
== Performance vs. Accuracy Trade-offs ==&lt;br /&gt;
&lt;br /&gt;
There is an inherent trade-off between numerical precision and computational performance (speed, memory usage) in AI systems. Choosing lower precision can dramatically increase throughput and reduce memory, but it may introduce arithmetic errors. Engineers must balance these considerations:&lt;br /&gt;
&lt;br /&gt;
=== 7.1. Throughput, Memory Bandwidth, and Precision ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Throughput (speed)&#039;&#039;&#039; is often higher for lower-precision operations. Modern hardware can execute more low-precision ops in parallel than high-precision ops. For example, an NVIDIA V100 GPU can perform 2× more half-precision FLOPs than single-precision per clock, and newer architectures with Tensor Cores can do 8× more (since a Tensor Core warp operation might perform 64 FP16 FMAs per cycle vs 16 FP32). Similarly, int8 operations can be even faster and more energy efficient. Lower precision numbers also use less memory bandwidth – fetching 16-bit data is twice as fast as 32-bit data from memory (assuming the same bus width), which can relieve memory bottlenecks. Many deep learning workloads are memory-bound, so halving the data size can nearly double throughput if compute is not the limiting factor. This is a huge incentive to use lower precision.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Memory and storage&#039;&#039;&#039;: Using FP16 or int8 weights reduces model size, which is important for deploying large models to memory-limited environments (like edge devices or GPUs with limited VRAM). It also means you can fit larger batch sizes in memory, or more models on a chip, etc. There is also a cascade effect: smaller data means better cache utilization, which further improves performance.&lt;br /&gt;
&lt;br /&gt;
However, &#039;&#039;&#039;accuracy&#039;&#039;&#039; suffers if precision is too low. We have discussed how rounding error and limited range can cause deviations in outputs. The question is: do those deviations matter for the application? Often for neural networks, a small amount of noise or error in intermediate calculations does not change the final prediction. Neural nets are somewhat error-tolerant due to their redundant distributed representations. This is why quantization and FP16 techniques can work at all. The final model accuracy (e.g. top-1 accuracy on ImageNet) might drop only 0.5% or less when using FP16 instead of FP32, or int8 instead of FP16, if done properly. For many use-cases, this loss is acceptable given the speed gains.&lt;br /&gt;
&lt;br /&gt;
If one goes too low (like naive FP8 or int4 without special care), accuracy might drop severely – perhaps making the model unusable. Thus the trade-off: as precision decreases, performance increases but at some point accuracy degrades beyond an acceptable threshold. Engineers aim to find the lowest precision that still preserves accuracy.&lt;br /&gt;
&lt;br /&gt;
There is also the consideration of &#039;&#039;&#039;accumulation strategy&#039;&#039;&#039; affecting performance: for instance, Kahan summation (compensated) is more accurate but ~4× slower than naive summation. If you need that accuracy, you pay a performance cost. In many AI inference scenarios, the slight extra accuracy from something like Kahan isn&#039;t needed, so it’s not used. But in a scenario where you sum millions of elements for a final result that is thresholded, maybe you do need it.&lt;br /&gt;
&lt;br /&gt;
Another dimension is &#039;&#039;&#039;determinism vs speed&#039;&#039;&#039;. Enforcing a deterministic reduction order (for reproducibility) might require disabling some parallelism, which could slow things down. If exact repeatability is critical (like certain scientific or financial applications of neural nets), one might sacrifice some performance to ensure the summation happens the same way every time. ONNX allows both modes depending on the runtime.&lt;br /&gt;
&lt;br /&gt;
From a memory perspective, if precision is higher than needed, you waste memory. For example, storing intermediate activations in FP32 when they could be FP16 uses 2× memory, which might mean lower batch size or more memory traffic. So there&#039;s strong motivation to keep things as low-precision as possible &#039;&#039;except&#039;&#039; where accuracy truly demands.&lt;br /&gt;
&lt;br /&gt;
In summary, the trade-off often manifests as &#039;&#039;diminishing returns&#039;&#039;: FP64 vs FP32 – huge slowdown for usually negligible accuracy gain in ML; FP32 vs FP16 – moderate speedup for usually minor accuracy hit; FP16 vs int8 – another big speedup but maybe more accuracy loss unless calibration is done; int8 vs int4 – speed gain but often big accuracy drop that might not be worth it for current models.&lt;br /&gt;
&lt;br /&gt;
Engineers use techniques like &#039;&#039;&#039;mixed precision&#039;&#039;&#039; to target high precision only where needed. For example, run most of the model in FP16, but keep a few sensitive layers in FP32. This yields most of the performance benefits while ensuring critical parts are accurate. We already saw softmax as one example. Another might be final output layer: sometimes using FP32 for the logits and softmax can be safer.&lt;br /&gt;
&lt;br /&gt;
Hardware is also trending to support mixed modes – like FP8 Tensor Cores accumulate in FP32, etc. So hardware tries to give accuracy cheaply.&lt;br /&gt;
&lt;br /&gt;
=== 7.2. Hardware Accelerators and Tensor Cores ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Hardware accelerators&#039;&#039;&#039; (GPUs, TPUs, FPGAs, ASICs like GraphCore IPU, etc.) have specific support for certain precisions. Leveraging these can provide massive speedups:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;NVIDIA Tensor Cores:&#039;&#039;&#039; These started in Volta (V100) with support for 4x4 matrix multiply-adds on FP16 inputs with FP32 accumulation&amp;lt;ref name=&amp;quot;ref6&amp;quot;/&amp;gt;. Using Tensor Cores can give up to 8x throughput compared to using regular FP32 cores. But to use them, the data needs to be FP16 (or now BF16 or FP8 on newer GPUs) and aligned to certain dimensions (multiples of 8 etc. in matrix sizes, which frameworks handle by padding if needed). If an ONNX model is all FP32, by default it would not use Tensor Cores unless the runtime automatically converts some ops to mixed precision (which some do via a flag or environment variable enabling TF32 or FP16). The introduction of &#039;&#039;&#039;TensorFloat-32 (TF32)&#039;&#039;&#039; in Ampere GPUs is essentially a compromise: treat FP32 inputs as two 16-bit significand halves (10-bit mantissa actually) so that it can run on Tensor Cores, giving ~8x speed, while still outputting FP32. TF32 has about the precision of FP16 but range of FP32, and it was chosen because it typically doesn’t hurt convnet accuracy. ONNX Runtime or others on Ampere will use TF32 by default for conv and matmul unless disabled, giving a free speed boost at minimal accuracy cost. If exact FP32 is needed, users have to turn it off (sacrificing speed).&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;TPU (Google):&#039;&#039;&#039; TPUs have native support for BF16 matrix ops with FP32 accumulators. They were designed specifically for the mixed precision approach in training. For inference, TPUs also support int8 and other quantized ops (e.g., via Edge TPU for lower-power). If exporting ONNX to run on a TPU (via some bridge), one might get automatic BF16 usage. BF16 has the advantage of no loss in range, so TPUs can more straightforwardly run models in BF16 without modifications.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;FPGAs and ASICs:&#039;&#039;&#039; Some custom accelerators might use fixed-point or block floating-point arithmetic for efficiency. They often treat the network in quantized terms. The ONNX quantization specification (QuantizeLinear/DequantizeLinear ops) exists for representing quantized models (INT8 etc.), which is another domain beyond floating-point, but it overlaps as a precision issue. In those systems, accumulators for INT8 × INT8 multiplies are usually 32-bit (to avoid overflow for dot products up to certain lengths). The trade-off there is between integer vs float representation, which can give big speedups on CPUs/DSAs via SIMD instructions (many CPUs have vector int8 multiply-add instructions now for inference).&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Memory bandwidth on hardware:&#039;&#039;&#039; Some accelerators include special high-bandwidth memory or cache specifically for lower precision. For example, an accelerator might fetch two FP16 values in one 32-bit memory transaction – effectively doubling memory bandwidth for half precision. On some hardware, using FP16 can even reduce memory &#039;&#039;latency&#039;&#039; if multiple values can be packed.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Parallelism and Vectorization:&#039;&#039;&#039; Many hardware units process data in fixed vector widths (e.g., 256-bit registers). That could be 8 floats at once or 16 half-floats at once in the same 256 bits. If your code is in FP16, you might use the full vector width (16 values) whereas FP32 might only use half (8 values). This means FP16 can make better use of vector units. Similarly, matrix units often have fixed tile sizes, and smaller data types allow larger tiles or more tiles concurrently.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Energy efficiency:&#039;&#039;&#039; Lower precision often means less energy per operation, which on large deployments (data centers or battery-powered devices) is a significant advantage. This isn&#039;t directly performance, but it&#039;s an important trade: you might choose a slightly lower precision to save power even if accuracy drops a tiny bit.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Software overhead:&#039;&#039;&#039; Using mixed precision on hardware involves some overhead of casting and moving data between precisions. For instance, if you have to frequently cast FP16 to FP32 and back, that adds instructions and memory copies. A well-designed accelerator (like GPUs with unified memory for FP16/32 or special hardware casting) minimize this. Still, the runtime might insert extra operations (as we saw, ONNX Runtime CPU inserted many Cast nodes converting FP16 to FP32 and back&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;, which actually &#039;&#039;slowed down&#039;&#039; that FP16 model by 4x vs FP32 on a CPU, because the CPU had no native FP16). So ironically, on some hardware, using lower precision in ONNX might degrade performance if not supported (the CPU case). Always ensure the target hardware actually supports the low precision you plan to use; otherwise, you&#039;ll pay overhead for emulation.&lt;br /&gt;
&lt;br /&gt;
To illustrate trade-off: Suppose you have a model that achieves 99% accuracy in FP32 and drops to 98.5% in FP16. If FP16 runs 2x faster, many would accept the 0.5% accuracy drop for double throughput. If int8 runs 4x faster but accuracy drops to 95%, that might be too much drop for some applications, but acceptable for others that value speed. It&#039;s application-dependent.&lt;br /&gt;
&lt;br /&gt;
In critical applications, one might maintain higher precision accumulators or run certain validation steps in high precision to ensure nothing drifted. In research, sometimes a model is trained in mixed precision but validated in double to ensure any small numeric differences don’t influence the reported result (rare, but for scientific computing integration maybe).&lt;br /&gt;
&lt;br /&gt;
In the ONNX world, performance-vs-accuracy is often managed via &#039;&#039;&#039;Execution Providers&#039;&#039;&#039; (EPs) that target specific hardware. For example, the TensorRT EP in ONNX Runtime can take an ONNX model and run it using NVIDIA&#039;s high-performance inference library, which will use FP16 or INT8 if allowed, to maximize speed. The user can configure the EP with precision flags. If the accuracy drop is too high, one might restrict it to FP32. If it&#039;s fine, allow FP16. Tools exist to measure the accuracy difference (like calibrating int8 quantization).&lt;br /&gt;
&lt;br /&gt;
To conclude, the trade-offs revolve around picking the lowest precision that yields acceptable accuracy, and utilizing hardware features fully. ONNX being a high-level spec abstracts this, but it&#039;s ultimately up to the deployment to decide and sometimes up to the exporter to incorporate quantization. ONNX’s role is to be able to &#039;&#039;represent&#039;&#039; these choices (they have added quantized types, FP8 types, etc., to allow the graph to include those when needed).&lt;br /&gt;
&lt;br /&gt;
== Best Practices and Design Guidelines ==&lt;br /&gt;
&lt;br /&gt;
Given the above insights, here are some best practices and guidelines for dealing with numerical precision and accumulators in AI inference (and training) design:&lt;br /&gt;
&lt;br /&gt;
=== 8.1. Choosing an Accumulator Precision ===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Match precision to data length and range:&#039;&#039;&#039; If an operation accumulates a large number of terms, consider using a higher precision accumulator than the input data. For example, for summing &amp;gt;1000 values of type FP16, an FP32 accumulator is recommended to avoid large rounding error or overflow. If summing &amp;gt; million values of FP32, consider double (FP64) accumulation if high accuracy is needed (though this is rare in DL). &#039;&#039;Rule of thumb:&#039;&#039; if $N \cdot \varepsilon$ is not tiny (say &amp;gt;1e-3), you may benefit from higher precision. For FP16 ($\varepsilon \approx 1e-3$), that threshold N is a few hundred. For FP32 ($\varepsilon \approx 1e-7$), N would be in the millions before 1e-3 error.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Use higher precision in critical layers:&#039;&#039;&#039; Identify if certain layers of the model are more sensitive to precision. For instance, the output layer or a layer that feeds into a very sharp decision boundary might require more precision. If a small change there could flip the prediction, you don&#039;t want excessive numerical error. Using FP32 for logits and softmax is a common practice to maintain classification confidence stability.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Follow hardware defaults, but override if needed:&#039;&#039;&#039; Many hardware accelerators have sensible defaults (like accumulate in FP32 for tensor ops). It&#039;s usually best to use those defaults as they were chosen to balance accuracy and speed. Only override if you have evidence of a problem. For example, if using ONNX Runtime on CPU, you discovered it upcasts to float for you – great. But if using some custom accelerator that does pure FP16, you might manually insert casts in the ONNX graph around certain ops to force higher precision on those ops.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Maintain precision for gradient accumulation (training):&#039;&#039;&#039; This is more training-oriented, but worth noting: when summing gradients over batches or updating weights, use higher precision (FP32 or even 64) for those accumulations. This ensures training doesn&#039;t diverge due to numeric error. Many frameworks keep a &amp;quot;master copy&amp;quot; of weights in FP32 when training in FP16&amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;. For inference, this translates to: any running statistics or counters in the model (like if you had a running average inside the model) should probably be in higher precision to avoid drift.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Be mindful of intermediate cast costs:&#039;&#039;&#039; If you do decide to cast to higher precision for an operation in ONNX, remember it incurs memory copies and compute overhead. Make sure it&#039;s worth it. For example, if you cast a huge tensor to double, that could slow things drastically. Usually FP16-&amp;gt;FP32 is fine (hardware handles it well), but FP32-&amp;gt;FP64 might be expensive on GPUs.&lt;br /&gt;
&lt;br /&gt;
=== 8.2. Safe Reduction Lengths and Heuristics ===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Chunk long reductions:&#039;&#039;&#039; If you must do a very long sum in low precision (due to memory constraints or hardware), chunk the sum into parts, sum each to a higher precision local accumulator, then sum the partial sums. This is essentially pairwise summation. Many libraries use a fixed chunk size heuristic (e.g., sum in blocks of 256 in registers, then add those up). This bounds the error growth. If implementing manually, you can choose a chunk size such that chunk_length &#039;&#039; ε is small (like 256 &#039;&#039; 1e-3 = 0.256 for FP16, hmm maybe choose 100).&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Sort by magnitude if possible:&#039;&#039;&#039; In some cases (not typical in DL, but possible in post-processing), if you have numbers varying by orders of magnitude, summing from smallest to largest improves accuracy. It prevents small ones from being swallowed by big ones. This is more relevant if you output a sum of mixed-scale values (like some algorithm mixing big and small contributions). It’s not always feasible in real-time inference to sort, but keep in mind.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Use compensation for critical sums:&#039;&#039;&#039; If you find a particular sum or difference in your inference computation is crucial (like computing a small residual from two close numbers), consider using Kahan summation or another compensated scheme just for that part. For example, if you had an ONNX custom function that needs to sum a lot of almost cancelling terms, you might implement it with compensation internally.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Heuristics for FP8/low-bit:&#039;&#039;&#039; When using extremely low precision like FP8, adopt the recommended practices: use scaling (don&#039;t feed raw data into FP8, normalize it first), and perhaps only use FP8 on portions of the network that are empirically robust. The industry is still establishing heuristics, but one might be: use FP8 for matrix multiplications, but accumulate in FP16 or FP32; keep normalization in FP16; use FP16 for first and last layer maybe. This is evolving, so keep an eye on literature.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Keep an eye on sum length in softmax and normalization:&#039;&#039;&#039; If you have extremely large class counts or group sizes (e.g., a softmax over 1e6 items, or layer norm over tens of thousands of features), realize this is a giant reduction. You may want to ensure that reduction is done in higher precision. ONNX Softmax by default will likely not handle a million-class case in FP16 well (the sum would overflow since 1e6 &amp;gt; 65504). So either do that on CPU in FP32 or break it down (could do hierarchical softmax, etc.). This is a corner case, but relevant for huge vocab language models. In practice, vocab ~50k is fine as we said.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Test numeric differences:&#039;&#039;&#039; A practical tip: after quantizing or reducing precision, run some test inputs through both the high-precision model and low-precision model. Compare outputs (and maybe some intermediate signals if possible). If differences are consistently small relative to the application tolerance, you&#039;re good. If you see occasional large differences or instabilities (like outputs become NaN or Inf in low precision), that&#039;s a red flag. Use that testing to guide where you might need to increase precision or change algorithms.&lt;br /&gt;
&lt;br /&gt;
=== 8.3. When Higher Precision Is Required ===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Ill-conditioned calculations:&#039;&#039;&#039; If your model or pipeline performs any operation known to be ill-conditioned (matrix inversion, subtraction of nearly equal quantities, computing a tiny difference of large numbers), you likely need higher precision for that part. For example, if your ONNX model includes a custom op to solve a linear system, use double precision in that op if possible (or at least float with iterative refinement). In ML, this might occur in something like a physics-informed neural network or a differentiable simulation within the model.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Strict accuracy requirements:&#039;&#039;&#039; Some applications (medical, aerospace, finance) might have strict tolerances. If an inference model’s output needs to be correct to 5 decimal places, you probably cannot use FP16, which has only 3-4 decimal digits precision. FP32 might be barely enough (~7 digits). FP64 might be warranted to guarantee that level of accuracy in worst case. Most deep learning applications (like image classification) don&#039;t demand that level of numeric precision in outputs – as long as the top class is correct, it doesn&#039;t matter if its probability was 0.901 vs 0.899. But if you&#039;re doing something like summing probabilities to make a risk estimate or doing a cumulative sum over many predictions, be careful to accumulate precisely.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Large sums or products in post-processing:&#039;&#039;&#039; Sometimes the neural net is fine, but the &#039;&#039;post-processing&#039;&#039; of results might need precision. E.g., summing up probabilities of many events, or computing confidence intervals, etc. Do those calculations in double if needed outside the model.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Preventing drift in long sequences:&#039;&#039;&#039; For sequential models (RNNs processing thousands of steps), rounding error can accumulate over steps. If each step has a small error, after 10000 steps it could add up. In such cases, using a higher precision for the state might reduce drift. Some practitioners use FP64 for the hidden state accumulation in very long sequence processing to maintain numerical stability (though this is not common due to speed loss). Alternatively, periodically renormalize or reset small errors.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Simulations or differential equations:&#039;&#039;&#039; If your ONNX model involves simulating something (like a differential equation solver inside a model, or an ODE/RNN hybrid), those often need double precision to be stable for long durations. Recognize those situations and allocate precision accordingly.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Verification and debugging:&#039;&#039;&#039; Use higher precision as a tool to verify correctness. For example, run your model in FP64 (if possible) and compare to FP32 to see if there are differences. If not, FP32 is fine. If yes, you might find a particular calculation losing too much info in FP32, implying maybe you should keep it in FP64 in production as well. This is rare in inference but can be part of rigorous validation.&lt;br /&gt;
&lt;br /&gt;
In ONNX, using higher precision may involve casting to &amp;lt;code&amp;gt;tensor(double)&amp;lt;/code&amp;gt; for certain ops. Not all runtimes support double on GPU though (some do via emulation or just slower path). ONNX CPU does support double for general ops. So be mindful of the target.&lt;br /&gt;
&lt;br /&gt;
Finally, &#039;&#039;&#039;document your precision choices&#039;&#039;&#039;. If you deliver an ONNX model that uses mixed types (float16 inputs, float32 accumulations, int8 weights, etc.), ensure that is clear to whoever maintains or uses it. Precision issues can be sneaky, so clarity helps.&lt;br /&gt;
&lt;br /&gt;
== Limitations and Open Problems ==&lt;br /&gt;
&lt;br /&gt;
Despite advances in handling numerical precision, there remain limitations and open challenges in this domain:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Lack of Precision Specification in ONNX:&#039;&#039;&#039; As noted, ONNX currently has no way to formally specify that an op &#039;&#039;should&#039;&#039; use a higher precision internally. This can lead to nondeterministic differences across platforms&amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;. While proposals exist to add attributes for this&amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;, it is not yet standard. Thus an ONNX model might behave slightly differently on two compliant backends, and there&#039;s no built-in mechanism to ensure identical results. This is a limitation for applications requiring cross-platform consistency. Solving it may involve extending ONNX (e.g., an attribute like &amp;lt;code&amp;gt;accumulatePrecision=&amp;quot;FLOAT32&amp;quot;&amp;lt;/code&amp;gt; for MatMul) or tightening the spec, but those come with backward compatibility and implementation burden concerns.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Reproducibility and Determinism:&#039;&#039;&#039; Even with the same precision, parallel computation can introduce nondeterminism (due to different summation orders). Ensuring bit-identical results across runs (important for some sensitive domains) is non-trivial. Frameworks often provide &amp;quot;deterministic mode&amp;quot; which might use fixed ordering at cost of speed. In ONNX, there&#039;s no global flag for this; it&#039;s up to the runtime. More research could be done on algorithms that produce bit-wise identical results independent of parallel execution order (perhaps using techniques like pairwise summation or consistent partitioning). Stochastic rounding is another area: sometimes using probabilistic rounding can reduce bias in accumulated error, but it introduces randomness – reconciling that with reproducibility is tricky.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Ultra-low Precision (FP8 and below):&#039;&#039;&#039; While FP8 is being explored, going to 4-bit floats or mixed schemes is an open challenge. The OpenAI community is investigating 4-bit (quantization-wise) for LLMs with some success, but a true 4-bit float (like E3M1) would be extremely limited. Perhaps hybrid schemes (like block floating point, log scales, etc.) will come. ONNX already has some support (they mention 4-bit in documentation placeholders&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;), but it&#039;s nascent. Research into training techniques, calibration, and new number formats (like &#039;&#039;&#039;posit numbers&#039;&#039;&#039; or tapering precision floats) is ongoing. If a new format proves useful, ONNX will need to adapt to represent it.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Quantization Noise and Model Robustness:&#039;&#039;&#039; Reducing precision can sometimes expose models to issues like adversarial vulnerability or fragility – e.g., a slight perturbation might flip a decision if the margin was small and precision loss nudged it. Ensuring model robustness under quantization is an open problem. It might require re-training the model with quantization in the loop (quantization-aware training) or designing models that have large &amp;quot;margin&amp;quot; between classes to tolerate quantization noise.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Dynamic range during training vs inference:&#039;&#039;&#039; In training, activations can have wider distributions (especially early in training). Models often learn to constrain their ranges (via activation functions saturating or normalization), which makes inference more stable in lower precision. But if one tries to train in FP8 from scratch, it&#039;s very hard. This gap between what&#039;s possible in a post-trained inference model vs training is an open area – how to train directly in low precision without significant accuracy loss. Solving that could simplify workflows (no need for high-precision training then quantize; just train low-prec end to end). Techniques like loss scaling, gradient clipping, etc., are partial solutions. The development of optimizers that are resilient to low precision is ongoing.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Hardware variability:&#039;&#039;&#039; As new hardware emerges (with new numeric capabilities or quirks), ensuring that ONNX models run consistently is an ongoing task. For instance, one device might implement a fused op that reduces rounding error, another does it separately. Even something like convolution might use a different algorithm (FFT-based vs direct) which has different numeric properties. Open problem: could ONNX allow specifying numeric tolerances per op or preferred algorithms? That may be overkill for most, but for some HPC ML use cases, maybe desired.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Testing and validation of numerics:&#039;&#039;&#039; There isn&#039;t a standardized set of &amp;quot;numeric compliance&amp;quot; tests for ONNX runtimes beyond simple correctness. Perhaps in the future, test suites might include challenging numeric scenarios (large sums, nearly singular matrices) to ensure a runtime handles them gracefully (maybe by switching precision). Right now, it&#039;s mostly on the user to test their model under expected conditions.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Combining quantization with floating point:&#039;&#039;&#039; Many production models use a mix (e.g., int8 for some layers, FP16 for others). ONNX can represent this (with QuantizeLinear ops or MixedPrecision through casting), but tooling to automatically find the optimal mix is still developing. It&#039;s somewhat trial-and-error or using heuristics. An open question is how to automate precision assignment: given a model and a target hardware, how to automatically choose which layers can be int8, which should be FP16, etc., to meet an accuracy target. This crosses into NAS (neural architecture search) territory or automated quantization. &lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Numerical debugging tools:&#039;&#039;&#039; When a model goes wrong due to precision, it&#039;s often hard to pinpoint where. Better tooling to analyze numerical error layer by layer could be useful. For instance, one could run a model in double and float and diff the intermediate results to see where the largest divergence happens. Doing this systematically for big nets is an open tooling challenge. Some frameworks have hooks for this, but not standardized.&lt;br /&gt;
&lt;br /&gt;
In conclusion, while we&#039;ve come far with using lower precision in AI without sacrificing much accuracy, the landscape is always evolving. The key open problem is how to push precision lower (for efficiency gains) without crossing the threshold where accuracy falls off a cliff. Each new generation of hardware and techniques (like FP8, novel quantization schemes) extends that boundary, but also brings new questions on how to manage the numerical errors. ONNX as an interchange will need to keep pace, possibly by adding more expressive power for these numeric considerations.&lt;br /&gt;
&lt;br /&gt;
== See Also ==&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;IEEE 754 Floating-Point&#039;&#039;&#039; – The standard defining binary16, binary32, binary64 formats and rounding behavior.&lt;br /&gt;
* &#039;&#039;&#039;Floating-Point Arithmetic&#039;&#039;&#039; – General topic on how floating-point math works, common issues (overflow, underflow, cancellation).&lt;br /&gt;
* &#039;&#039;&#039;Kahan Summation Algorithm&#039;&#039;&#039; – Algorithm for compensated summation to reduce numerical error&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;.&lt;br /&gt;
* &#039;&#039;&#039;BFloat16&#039;&#039;&#039; – A short-format float with 8-bit exponent, used in TPUs and some GPUs for training networks.&lt;br /&gt;
* &#039;&#039;&#039;Mixed Precision Training&#039;&#039;&#039; – Technique of using FP16 (or BF16) along with FP32 accumulators to accelerate training&amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;.&lt;br /&gt;
* &#039;&#039;&#039;Quantization in Deep Learning&#039;&#039;&#039; – Converting networks to lower bit integer arithmetic for inference (INT8 quantization and calibration techniques).&lt;br /&gt;
* &#039;&#039;&#039;ONNX Operator Specification&#039;&#039;&#039; – Official docs for ONNX ops, which describe their mathematical function but not the required numeric precision (a point of potential extension in future).&lt;br /&gt;
* &#039;&#039;&#039;CUDA Tensor Cores &amp;amp; TF32&#039;&#039;&#039; – NVIDIA documentation on how tensor cores perform mixed precision and the TF32 format for accelerating FP32.&lt;br /&gt;
* &#039;&#039;&#039;Pairwise Summation&#039;&#039;&#039; – Summation algorithm with logN error growth&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;, used in many numerical libraries to improve accuracy of reductions.&lt;br /&gt;
* &#039;&#039;&#039;Softmax Stability&#039;&#039;&#039; – Articles on numerically stable softmax and log-sum-exp implementations to avoid overflow.&lt;br /&gt;
* &#039;&#039;&#039;Performance Tuning for Low Precision&#039;&#039;&#039; – Guides (e.g., NVIDIA or Intel whitepapers) on using low precision and the trade-offs involved in various hardware.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;references&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref1&amp;quot;&amp;gt;Accelerate and simplify Scikit-learn model inference with ONNX Runtime - Microsoft Open Source Blog — opensource.microsoft.com — https://opensource.microsoft.com/blog/2020/12/17/accelerate-simplify-scikit-learn-model-inference-onnx-runtime&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref2&amp;quot;&amp;gt;Floating Point Precision: Understanding FP64, FP32, and FP16 in Large Language Models - DEV Community — dev.to — https://dev.to/lukehinds/floating-point-precision-understanding-fp64-fp32-and-fp16-in-large-language-models-3gk6&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref3&amp;quot;&amp;gt;Floating Point Precision and Its Limitations | by Umair Akbar | Medium — akbu.medium.com — https://akbu.medium.com/floating-point-precision-and-its-limitations-cfb7247d7789&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref4&amp;quot;&amp;gt;Kahan summation algorithm - Wikipedia — en.wikipedia.org — https://en.wikipedia.org/wiki/Kahan_summation_algorithm&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref5&amp;quot;&amp;gt;Pairwise summation - Wikipedia — en.wikipedia.org — https://en.wikipedia.org/wiki/Pairwise_summation&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref6&amp;quot;&amp;gt;Numerical behavior of NVIDIA tensor cores - PMC — pmc.ncbi.nlm.nih.gov — https://pmc.ncbi.nlm.nih.gov/articles/PMC7959640/&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref7&amp;quot;&amp;gt;Mixed precision and performance-accuracy tuning (neuron-cc) — AWS Neuron Documentation — awsdocs-neuron.readthedocs-hosted.com — https://awsdocs-neuron.readthedocs-hosted.com/en/latest/about-neuron/appnotes/neuron-cc/mixed-precision.html&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref8&amp;quot;&amp;gt;Create Float16 and Mixed Precision Models - ONNX Runtime — onnxruntime.ai — https://onnxruntime.ai/docs/performance/model-optimizations/float16.html&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref9&amp;quot;&amp;gt;[Performance] ONNX FP16 model is having performance bottle neck when compared to FP32 variant · Issue #25824 · microsoft/onnxruntime · GitHub — github.com — https://github.com/microsoft/onnxruntime/issues/25824&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref10&amp;quot;&amp;gt;Float stored in 8 bits - ONNX 1.21.0 documentation — onnx.ai — https://onnx.ai/onnx/technical/float8.html&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref11&amp;quot;&amp;gt;[2209.05433] FP8 Formats for Deep Learning — arxiv.org — https://arxiv.org/abs/2209.05433&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref12&amp;quot;&amp;gt;Should MatMul op allow specification of accumulation precision? · Issue #7072 · onnx/onnx · GitHub — github.com — https://github.com/onnx/onnx/issues/7072&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref13&amp;quot;&amp;gt;Quantize ONNX models | onnxruntime — onnxruntime.ai — https://onnxruntime.ai/docs/performance/model-optimizations/quantization.html&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref14&amp;quot;&amp;gt;Keep Attention Softmax FP32 during FP16/ZeRO Training · Issue #1485 · hpcaitech/ColossalAI · GitHub — github.com — https://github.com/hpcaitech/ColossalAI/issues/1485&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;/references&amp;gt;&lt;/div&gt;</summary>
		<author><name>Timo.stripf</name></author>
	</entry>
	<entry>
		<id>https://www.emmtrix.com/w139/index.php?title=Numerical_Precision_in_ONNX_and_AI_Inference&amp;diff=2894</id>
		<title>Numerical Precision in ONNX and AI Inference</title>
		<link rel="alternate" type="text/html" href="https://www.emmtrix.com/w139/index.php?title=Numerical_Precision_in_ONNX_and_AI_Inference&amp;diff=2894"/>
		<updated>2026-01-30T15:20:35Z</updated>

		<summary type="html">&lt;p&gt;Timo.stripf: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&#039;&#039;&#039;Open Neural Network Exchange (ONNX)&#039;&#039;&#039; is an open standard format for representing machine learning models and neural network computations across different frameworks and hardware&amp;lt;ref name=&amp;quot;ref1&amp;quot;/&amp;gt;. As models are exported and deployed via ONNX, the &#039;&#039;&#039;numerical precision&#039;&#039;&#039; of computations becomes critical. Deep learning inference involves a variety of floating-point operations, and small numerical discrepancies can accumulate and affect model accuracy or reproducibility. This article provides an in-depth overview of floating-point precision issues in numerical computing, with a focus on how they manifest in AI inference and the ONNX ecosystem. We cover floating-point formats and rounding behavior, accumulation strategies for summations, the use of low-precision formats (FP16, BFloat16, FP8) in modern AI, and how ONNX defines (or leaves unspecified) the precision of accumulators in operations. Strategies for maintaining numerical stability in deep neural networks are discussed, along with the trade-offs between computational performance and arithmetic accuracy. The goal is to give engineers and researchers a comprehensive, encyclopedic reference on numerical precision in ONNX-based AI inference systems.&lt;br /&gt;
&lt;br /&gt;
== Floating-Point Precision in Numerical Computing ==&lt;br /&gt;
&lt;br /&gt;
=== 2.1. IEEE 754 Floating-Point Formats ===&lt;br /&gt;
&lt;br /&gt;
Modern computers represent real numbers using the IEEE 754 floating-point standard, which defines formats like 32-bit &#039;&#039;single precision&#039;&#039; (FP32) and 64-bit &#039;&#039;double precision&#039;&#039; (FP64). In IEEE 754 binary formats, a floating-point number is composed of three fields: 1 sign bit, several exponent bits (with a bias), and several fraction (mantissa) bits. For example, FP32 consists of 1 sign bit, 8 exponent bits, and 23 fraction bits (24 bits of significand precision including the implicit leading 1)&amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;. This provides roughly 7 decimal digits of precision and an exponent range allowing values on the order of $10^{\pm38}$. FP64 allocates 1 sign bit, 11 exponent bits, and 52 fraction bits (53-bit precision), yielding about 15–16 decimal digits of precision and a vastly larger range (~$10^{\pm308}$)&amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;. These larger formats (especially FP64) are the &amp;quot;gold standard&amp;quot; for numerical accuracy in scientific computing, as they reduce round-off error in complex calculations&amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;. However, the increased precision comes at the cost of doubled storage and typically reduced computational throughput (e.g. many GPUs execute FP64 instructions at a much lower rate than FP32&amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
In addition to single and double precision, the IEEE 754-2008 standard introduced a 16-bit &#039;&#039;half precision&#039;&#039; format (FP16, or binary16). FP16 has 1 sign bit, 5 exponent bits, and 10 fraction bits (11-bit significand including the implicit 1)&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. This format covers a much smaller dynamic range (approximately $6.1\times10^{-5}$ to $6.5\times10^4$ for normalized values&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;) and offers only around 3 decimal digits of precision. Half precision was originally used in graphics, but it has gained popularity in machine learning for its speed and memory advantages (discussed later in Section 4). Another format relevant to AI is &#039;&#039;&#039;BFloat16&#039;&#039;&#039; (Brain Float 16), a 16-bit float with &#039;&#039;&#039;8 exponent bits and 7 fraction bits&#039;&#039;&#039;&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. BFloat16 sacrifices precision (only ~7 fraction bits ≈ 2–3 decimal digits) in order to have the &#039;&#039;&#039;same exponent range as FP32&#039;&#039;&#039;&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. This means BFloat16 can represent very large or very small numbers (∼$10^{\pm38}$, similar range as FP32) but with much less precision between representable values&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. BFloat16 was introduced for deep learning by Google (for TPUs) to allow reduced precision &#039;&#039;training&#039;&#039; without frequent overflow/underflow, relying on the wide range and accepting the coarse precision.&lt;br /&gt;
&lt;br /&gt;
Each floating-point format has a &#039;&#039;&#039;unit roundoff&#039;&#039;&#039; or machine precision, denoted $\epsilon$. This is effectively the spacing between 1.0 and the next representable number. For FP32, $\epsilon \approx 2^{-23} \approx 1.19\times10^{-7}$, corresponding to about 7–8 decimal digits of precision&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. In FP64, $\epsilon = 2^{-52} \approx 2.22\times10^{-16}$ (15–16 decimal digits). Half precision is much less precise: FP16 has $\epsilon = 2^{-10} \approx 9.76\times10^{-4}$&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;, and BFloat16 has $\epsilon = 2^{-7} = 7.8125\times10^{-3}$&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. These values quantify how finely the continuum of real numbers is quantized in each format.&lt;br /&gt;
&lt;br /&gt;
=== 2.2. Machine Epsilon and Rounding Error ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Machine epsilon ($\varepsilon$)&#039;&#039;&#039; is formally defined as the smallest positive number such that $1.0 + \varepsilon &amp;gt; 1.0$ in the floating-point format&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. It effectively measures the &#039;&#039;&#039;precision&#039;&#039;&#039; of the representation. In FP32, $\varepsilon = 2^{-23} \approx 1.19\times10^{-7}$, meaning any result is rounded to about 7 decimal digits. FP16’s $\varepsilon = 2^{-10} \approx 9.77\times10^{-4}$, over 1000× larger (so only ~3 decimal digits of precision), and BFloat16’s $\varepsilon = 2^{-7} \approx 7.8\times10^{-3}$, reflecting its mere ~8-bit precision&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. Rounding in IEEE 754 by default uses &#039;&#039;&#039;round-to-nearest (ties to even)&#039;&#039;&#039;, which ensures the rounding error for any single operation is at most half a unit in the last place (0.5 ULP). We can model a floating-point addition as: &lt;br /&gt;
&lt;br /&gt;
$$\text{fl}(a+b) = (a+b)\,(1 + \delta),$$ &lt;br /&gt;
&lt;br /&gt;
where $|\delta| \le \varepsilon$ for that format&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. Here $\varepsilon/2$ is the maximum &#039;&#039;&#039;relative error&#039;&#039;&#039; in one rounding (also called unit roundoff). For example, when adding two FP32 numbers, the result is accurate to within about $10^{-7}$ of the true sum (relative), whereas in BF16, the result can deviate by up to $\sim7.8\times10^{-3}$ (nearly 0.78% relative error per operation)&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. Thus, &#039;&#039;&#039;each arithmetic operation incurs a small rounding error&#039;&#039;&#039; (on the order of machine epsilon) that can propagate or accumulate through a sequence of calculations.&lt;br /&gt;
&lt;br /&gt;
Because floating-point representations have finite exponent range, we must also consider &#039;&#039;&#039;overflow&#039;&#039;&#039; and &#039;&#039;&#039;underflow&#039;&#039;&#039;. If a result’s magnitude exceeds the maximum representable value, it overflows to infinity; if it falls below the minimum normal (or minimum subnormal) value, it underflows to zero (or a denormalized tiny value). FP32 and BF16 share an exponent size (8 bits), so their range for normal numbers is roughly $[10^{-38}, 10^{38}]$ (approx $~\!3.4\times10^{38}$ max)&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. FP16’s 5-bit exponent allows max $\sim 6.55\times10^4$ and min normal $\sim 6.10\times10^{-5}$&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. In practice, &#039;&#039;&#039;underflow/overflow&#039;&#039;&#039; can occur in low-precision arithmetic if values are not appropriately scaled. For instance, summing a large number of moderately sized terms in FP16 could overflow where FP32 would not. As an example, summing on the order of $10^4$ values of magnitude ~$10^1$ (say, adding 10,000 terms around 10 each) would produce a total $\sim10^5$, which exceeds FP16’s max finite value ~$6.5\times10^4$ and would overflow to $+\infty$ in half precision&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. Underflow is usually a concern when subtracting nearly equal numbers (catastrophic cancellation yielding a tiny difference that might flush to zero in low precision).&lt;br /&gt;
&lt;br /&gt;
=== 2.3. Accumulation Error in Reductions ===&lt;br /&gt;
&lt;br /&gt;
Floating-point addition is &#039;&#039;&#039;not associative&#039;&#039;&#039;: $(a+b)+c$ can differ from $a+(b+c)$ due to rounding, and thus the order in which we sum a list of numbers can change the result slightly. When summing a large sequence of $N$ numbers $x_1, x_2, ..., x_N$, the &#039;&#039;&#039;accumulated rounding error&#039;&#039;&#039; can be significant. In the &#039;&#039;worst case&#039;&#039; (e.g., adding many positive and negative terms that cause maximal cancellation or always rounding one way), the error can grow on the order of $O(N \varepsilon)$ relative to the exact sum&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;. Simply summing $N$ numbers in sequence (the &#039;&#039;&#039;naive linear summation&#039;&#039;&#039;) has a worst-case error bound proportional to $N\,\varepsilon$&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. This worst-case occurs for adversarial arrangements of values; in more typical scenarios with rounding errors of random signs, one can model the error as a random walk, yielding an expected root-mean-square (RMS) error growth on the order of $O(\sqrt{N}\,\varepsilon)$&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;. Still, with very large $N$, even $\sqrt{N}$ times $\varepsilon$ can become large enough to matter.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Cancellation&#039;&#039;&#039; is a particular problem: if we add two nearly equal numbers of opposite sign, most leading digits cancel and the result loses significant bits of precision. For example, in single precision, subtracting two numbers that agree in the first 6 decimal digits will yield a result with only ~1 digit of accuracy left. Catastrophic cancellation can dramatically increase relative error. Summation is especially sensitive to cancellation if large positive and negative terms are summed naively. The &#039;&#039;&#039;condition number&#039;&#039;&#039; of a summation (the ratio of the sum of absolute values $\sum|x_i|$ to the absolute value of the true sum $|\sum x_i|$) indicates how sensitive the result is to perturbations&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;. If the numbers have mixed signs and nearly cancel out (making the condition number large), even an optimal summation order will have a large relative error. For instance, summing [1.0, $10^{100}$, 1.0, $-10^{100}$] in double precision has an exact sum of 2.0, but naive summation may yield 0.0 due to cancellation; an improved summation algorithm yields the correct 2.0&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
In practical terms, when summing a long list of floating-point numbers (such as accumulating a dot product or reducing a tensor), the &#039;&#039;&#039;accumulation error&#039;&#039;&#039; can become noticeable if no precautions are taken. Techniques to mitigate this error include carefully choosing the summation order or using higher precision for intermediate sums. We will explore these strategies in the next section.&lt;br /&gt;
&lt;br /&gt;
== Accumulators and Reduction Strategies ==&lt;br /&gt;
&lt;br /&gt;
When performing reductions (sums, dot products, etc.), the method of accumulation can greatly affect the accuracy of the result. Below, we discuss various strategies:&lt;br /&gt;
&lt;br /&gt;
=== 3.1. Naive Linear Accumulation ===&lt;br /&gt;
&lt;br /&gt;
The simplest accumulation strategy is to use a single accumulator and add each element sequentially. In C-like pseudocode:&lt;br /&gt;
&lt;br /&gt;
This &#039;&#039;&#039;naive linear summation&#039;&#039;&#039; adds values one by one to the running total. While straightforward and fast, it is prone to the rounding error accumulation described earlier. The partial sum &amp;lt;code&amp;gt;sum&amp;lt;/code&amp;gt; continually grows (in absolute value) as terms are added, so new small terms may fall outside its precision. For example, if &amp;lt;code&amp;gt;sum&amp;lt;/code&amp;gt; is large and positive, adding a comparatively tiny positive number may produce no change because the small term&#039;s significant digits vanish in the presence of the large sum (the increment is below the rounding threshold of &amp;lt;code&amp;gt;sum&amp;lt;/code&amp;gt;). Conversely, adding a small number of opposite sign can suffer from cancellation. The order of addition matters: summing from smallest magnitude to largest (ascending order) tends to yield a more accurate result than summing in descending order, because adding small terms first preserves their contributions before the sum becomes very large. The naive loop above implicitly sums in the given index order; if the data is unsorted, this order is essentially arbitrary.&lt;br /&gt;
&lt;br /&gt;
The performance of linear accumulation is excellent (minimal overhead), and for many applications the resulting error is within acceptable limits. However, for &#039;&#039;&#039;very large N&#039;&#039;&#039; or ill-conditioned sums, the inaccuracy can become a problem. In a reduction involving millions of elements, a single-precision sum could lose several digits of accuracy. In deep learning, this could translate to small discrepancies in layer outputs or aggregated gradients if done naively in low precision.&lt;br /&gt;
&lt;br /&gt;
=== 3.2. Pairwise and Tree-Based Reductions ===&lt;br /&gt;
&lt;br /&gt;
A better approach to summation is &#039;&#039;&#039;pairwise summation&#039;&#039;&#039;, which sums numbers in a binary tree structure rather than linearly. The idea is to recursively split the array into halves, sum each half, then add the two partial sums. This can be implemented as:&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
By always adding numbers of similar magnitudes first (small subsets are reduced to subtotals, then those are added, etc.), pairwise summation avoids the situation of a very large running total absorbing small additions until the final steps. The error of pairwise summation grows logarithmically with $N$ in the worst case (roughly $O(\varepsilon \log_2 N)$)&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;, instead of linearly. Intuitively, at each level of the tree, rounding error is constrained, and there are about $\log_2 N$ levels. In fact, the worst-case relative error bound for pairwise summation (base case of 1 element) is:&lt;br /&gt;
&lt;br /&gt;
\[ |E_N| \le \frac{\varepsilon \log_2 N}{1 - \varepsilon \log_2 N} \sum_{i=1}^N |x_i|, \]&lt;br /&gt;
&lt;br /&gt;
which for practical $N$ simplifies to on the order of $\varepsilon \log_2 N$ times the condition number of the sum&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;. In typical random-error scenarios, the error behaves even better (on the order of $\sqrt{\log N}$)&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;. The key is that &#039;&#039;&#039;pairwise (tree) reduction balances the accumulations&#039;&#039;&#039;, preventing one sum from overwhelming the others too early.&lt;br /&gt;
&lt;br /&gt;
From a performance standpoint, pairwise summation has the same $O(N)$ total additions as linear summation, just a different order. It is also highly amenable to parallelization: independent partial sums can be computed in parallel at each level of the tree. Many numerical libraries and BLAS implementations use a form of blocked or pairwise summation in dot products for improved accuracy at negligible performance cost. For example, NumPy and Julia perform partial pairwise summation by default for better numerical behavior&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=== 3.3. Compensated Summation (Kahan, Neumaier) ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Compensated summation&#039;&#039;&#039; is an alternative technique to improve summation accuracy by tracking the small “lost” quantities. The most famous algorithm is the &#039;&#039;&#039;Kahan summation algorithm&#039;&#039;&#039; (also called Kahan–Babuška)&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. Kahan summation uses an extra variable to accumulate the round-off error. In pseudocode:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;c&amp;quot;&amp;gt;&lt;br /&gt;
double sum = 0.0;&lt;br /&gt;
double c = 0.0;    // compensation for lost low-order bits&lt;br /&gt;
for (i = 1; i &amp;lt;= N; ++i) {&lt;br /&gt;
    double y = x[i] - c;    // recover low-order bits by subtracting compensation&lt;br /&gt;
    double t = sum + y;     // perform the addition&lt;br /&gt;
    c = (t - sum) - y;      // compute new compensation (the error in t)&lt;br /&gt;
    sum = t;&lt;br /&gt;
}&lt;br /&gt;
return sum;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Here &amp;lt;code&amp;gt;c&amp;lt;/code&amp;gt; accumulates the tiny leftovers that would otherwise be dropped&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. Each iteration essentially subtracts the last error from the new addend (&amp;lt;code&amp;gt;y = x[i] - c&amp;lt;/code&amp;gt;) so that &amp;lt;code&amp;gt;t = sum + y&amp;lt;/code&amp;gt; includes the small bits from the previous operation. The new error is then stored in &amp;lt;code&amp;gt;c&amp;lt;/code&amp;gt; for the next iteration. The result is that the final &amp;lt;code&amp;gt;sum&amp;lt;/code&amp;gt; is as if computed with higher precision than the machine’s: Kahan’s algorithm can &#039;&#039;&#039;greatly reduce the error&#039;&#039;&#039; in summing a sequence of floats. In fact, with exact arithmetic for the compensation, Kahan summation yields a result accurate to within 1 or 2 ULP of the true sum, regardless of N (assuming the sequence of partial sums doesn’t overflow)&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. More precisely, Kahan summation achieves a &#039;&#039;&#039;worst-case error independent of N&#039;&#039;&#039; (error bound depends only on machine precision, not the number of terms)&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. In practice, using a double accumulator for summing single-precision numbers via Kahan can give near double-precision accuracy.&lt;br /&gt;
&lt;br /&gt;
An important subtlety is that the compensation variable &amp;lt;code&amp;gt;c&amp;lt;/code&amp;gt; should be kept in &#039;&#039;full precision&#039;&#039;. In the implementation above, we used &amp;lt;code&amp;gt;double&amp;lt;/code&amp;gt; for both sum and c. If summing FP32 values, one might use &amp;lt;code&amp;gt;float sum&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;float c&amp;lt;/code&amp;gt; (both 32-bit), which still improves accuracy, but using a higher precision for the accumulator and compensation (i.e. double) yields even better results. Many implementations of Kahan summation use the same type for sum and c, but it&#039;s assumed to be higher precision than the inputs when possible.&lt;br /&gt;
&lt;br /&gt;
Kahan’s algorithm carries a performance cost: each iteration does five arithmetic operations (two adds, one subtract, etc.) and has a data dependency, which can hinder parallelism or pipelining&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. Thus, it runs slower than naive summation (roughly 2–3× slower or more, depending on CPU pipeline). In loops where summation is a bottleneck, this may be significant. Nonetheless, for modest N or where maximum accuracy is required, compensated summation is extremely useful. In fact, Python’s &amp;lt;code&amp;gt;math.fsum&amp;lt;/code&amp;gt; function uses a variant of Kahan’s algorithm (actually a more advanced multi-part compensation) to sum iterables with high precision.&lt;br /&gt;
&lt;br /&gt;
There are variations and improvements on basic Kahan summation. &#039;&#039;&#039;Neumaier’s algorithm&#039;&#039;&#039; (sometimes called &#039;&#039;Kahan-Babuška-Neumaier&#039;&#039;) improves Kahan’s method by also handling cases where the next input is larger in magnitude than the running sum&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. In such cases, the original Kahan formula could suffer error; Neumaier’s version effectively swaps roles when a huge value comes in. Its pseudocode is:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight&amp;gt;&lt;br /&gt;
function NeumaierSum(array):&lt;br /&gt;
    sum = 0.0&lt;br /&gt;
    compensation = 0.0&lt;br /&gt;
    for each x in array:&lt;br /&gt;
        t = sum + x&lt;br /&gt;
        if (fabs(sum) &amp;gt;= fabs(x))&lt;br /&gt;
            compensation += (sum - t) + x;&lt;br /&gt;
        else&lt;br /&gt;
            compensation += (x - t) + sum;&lt;br /&gt;
        sum = t;&lt;br /&gt;
    return sum + compensation;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This algorithm ensures the compensation covers both scenarios: when the sum is the bigger addend or when the new term is bigger&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. In the end, it adds the final &amp;lt;code&amp;gt;compensation&amp;lt;/code&amp;gt; once to the result. Neumaier’s method can produce the correct result in cases where basic Kahan fails. For instance, summing [1.0, $+10^{100}$, 1.0, $-10^{100}$] in double precision: regular Kahan summation might give 0.0 (losing the small terms), whereas Neumaier’s algorithm yields 2.0 exactly&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. There are also higher-order compensation methods (carrying multiple compensation terms, e.g., the &#039;&#039;&#039;Klein summation&#039;&#039;&#039; variant&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt; or using arbitrary precision for intermediate sums) that further reduce error at increased computational cost&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
In summary, compensated summation algorithms trade extra computation for greatly improved numerical accuracy. They are especially valuable when summing large arrays of numbers with varying magnitudes, where naive summation could lose most of the low-order bits. Many scientific computing environments provide compensated sum routines for this reason. In deep learning, however, a different approach to improved precision is often taken: &#039;&#039;&#039;using a wider accumulator type&#039;&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
=== 3.4. Mixed-Precision Accumulation ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Mixed-precision accumulation&#039;&#039;&#039; refers to performing arithmetic in a higher precision than the input data. A common scenario is accumulating sums in 32-bit floats when the operands are 16-bit floats. Rather than implementing a compensation algorithm, one can simply use a wider accumulator so that intermediate rounding error is smaller. This is effectively what modern GPUs do for tensor operations: for example, NVIDIA &#039;&#039;&#039;Tensor Cores operate on FP16 input data with an FP32 accumulator&#039;&#039;&#039; (the 16-bit products are accumulated in 32-bit)&amp;lt;ref name=&amp;quot;ref6&amp;quot;/&amp;gt;. In this way, summing 2048 products in a dot product has far less precision loss than it would with a 16-bit accumulator. Hardware that supports mixed precision can give the best of both worlds: fast low-precision multiplies and high-precision accumulation.&lt;br /&gt;
&lt;br /&gt;
Software can also leverage mixed precision. For instance, one can sum an array of &amp;lt;code&amp;gt;float&amp;lt;/code&amp;gt; values into a &amp;lt;code&amp;gt;double&amp;lt;/code&amp;gt; variable, accumulating the result in double precision and then converting back to float at the end:&lt;br /&gt;
&lt;br /&gt;
This approach often yields a more accurate &amp;lt;code&amp;gt;result&amp;lt;/code&amp;gt; than summing in single precision directly, because the intermediate sum carries 52-bit precision. The final rounding to float happens only once at the end (minimizing overall rounding error). In fact, the above is one way to implement &amp;lt;code&amp;gt;math.fsum&amp;lt;/code&amp;gt; in Python (it internally does something similar with multiple partial sums in double precision).&lt;br /&gt;
&lt;br /&gt;
In machine learning training and inference, &#039;&#039;&#039;mixed precision&#039;&#039;&#039; is widely used. When using FP16 for speed, it&#039;s common to accumulate with FP32. For example, &#039;&#039;&#039;Google&#039;s TPUs use BFloat16 for multiplication but accumulate in FP32&#039;&#039;&#039; for matrix ops&amp;lt;ref name=&amp;quot;ref6&amp;quot;/&amp;gt;. AWS’s Inferentia (Neuron) hardware similarly supports FP16/BF16 matrix multiplication with FP32 accumulation&amp;lt;ref name=&amp;quot;ref7&amp;quot;/&amp;gt;. The motivation is that summing hundreds or thousands of half-precision values would otherwise introduce intolerable error or risk overflow. With an FP32 accumulator, the reduction error is drastically reduced (by about $2^{13}$ times, since FP32 has 13 more precision bits than FP16). Mixed-precision accumulation thus provides a simple and effective form of compensation: &#039;&#039;&#039;use a larger bucket to collect many small contributions&#039;&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
There is a spectrum of options: summing int8 or int16 integers might use 32-bit integers as accumulators for the same reason (to avoid overflow). Some BLAS libraries even offer &#039;&#039;extended precision accumulators&#039;&#039; for single precision dot products (accumulating in double). The downside of mixed precision is the extra memory and maybe throughput cost of the wider type. But on many platforms, the add latency is dominated by hardware design rather than bit-width, so using a 32-bit adder vs 16-bit has negligible speed impact if supported natively. The key is whether data movement or register pressure increases.&lt;br /&gt;
&lt;br /&gt;
In summary, &#039;&#039;&#039;using a higher precision accumulator is often the easiest way to improve numerical accuracy&#039;&#039;&#039; for reductions. It is a primary strategy in deep learning hardware to maintain stability while exploiting low precision for storage and initial computations.&lt;br /&gt;
&lt;br /&gt;
== Precision Formats in AI and Machine Learning ==&lt;br /&gt;
&lt;br /&gt;
Modern AI systems use a variety of numeric formats to balance speed, memory, and accuracy. We discuss common precisions and their roles:&lt;br /&gt;
&lt;br /&gt;
=== 4.1. FP32 and FP64 in Training and Inference ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;FP32 (single precision)&#039;&#039;&#039; has been the workhorse of deep learning for years. Most training scripts in frameworks like TensorFlow and PyTorch used FP32 by default, as it offers sufficient precision to propagate gradients and accumulate weight updates over millions of iterations without excessive error. With ~7 decimal digits of precision and a wide exponent range, FP32 can handle the dynamic range of activations and gradients in typical neural networks. In inference, FP32 is often more precision than strictly necessary for final predictions, but it has remained common because of its ease and the fact that many GPU operations were optimized for FP32.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;FP64 (double precision)&#039;&#039;&#039; is rarely used in deep learning model training or inference, except in specific scenarios requiring extreme precision. FP64 is common in scientific computing and certain types of numerical simulations. In neural networks, however, the extra precision is usually not needed for model accuracy—using doubles tends to yield negligible improvements in model metrics but at a large performance cost (since on GPUs FP64 throughput can be 1/2, 1/8, or even 1/32 of FP32 throughput depending on hardware). One exception might be when integrating neural nets into larger physics simulations or when solving ill-conditioned problems as part of the model. Some researchers use FP64 to diagnose numerical issues or ensure reproducibility. Generally though, deep learning has favored lower precision for efficiency, and FP64 is considered overkill for most networks&amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
That said, certain classical machine learning algorithms benefit from double precision. For example, computing the inverse of a matrix (as in a Gaussian Process regression or Kalman filter embedded in a model) can incur large rounding errors in single precision, so using FP64 can significantly improve the result&amp;lt;ref name=&amp;quot;ref1&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref1&amp;quot;/&amp;gt;. The ONNX standard supports a float64 tensor type for such needs. In ONNX Runtime, however, emphasis has been on FP32 performance, and some pipelines default to float even if the original model was double&amp;lt;ref name=&amp;quot;ref1&amp;quot;/&amp;gt;. If a model’s prediction computation involves operations that are numerically sensitive (like matrix inversion, Cholesky decomposition, or summing extremely large counts), then FP64 may be required to avoid noticeable discrepancies&amp;lt;ref name=&amp;quot;ref1&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref1&amp;quot;/&amp;gt;. A good practice is to test a model in both float and double if you suspect precision issues; significant differences indicate an ill-conditioned calculation that might need higher precision.&lt;br /&gt;
&lt;br /&gt;
In summary, FP32 remains the &#039;&#039;&#039;default&#039;&#039;&#039; format for most training and inference due to its balance of accuracy and efficiency&amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;, while FP64 is reserved for niche cases or precision-critical subroutines. FP32 typically achieves the same accuracy as FP64 for end-to-end deep learning tasks, because network training can often compensate for small noise, and other sources of error (like generalization error) dominate. Therefore, the community trend has been moving to &#039;&#039;&#039;lower&#039;&#039;&#039; precisions, not higher, to gain speed — provided the models still converge and perform well.&lt;br /&gt;
&lt;br /&gt;
=== 4.2. FP16 and BFloat16 in Deep Learning ===&lt;br /&gt;
&lt;br /&gt;
To further speed up neural network training and inference, &#039;&#039;&#039;half-precision (FP16)&#039;&#039;&#039; was adopted, especially with the advent of GPUs supporting FP16 math at twice the rate of FP32. Using FP16 reduces memory storage and bandwidth by 50% compared to FP32, which is significant for large models. However, FP16’s small exponent range and precision posed challenges: training a large network purely in FP16 can fail due to gradient underflow/overflow or weight updates not registering (because changes are below the 0.001 quantization step). To address this, frameworks introduced &#039;&#039;&#039;mixed-precision training&#039;&#039;&#039;, where compute-intensive parts (matrix multiplies, convolutions) run in FP16, but certain accumulations and model parameters remain in FP32. For instance, NVIDIA’s &#039;&#039;&#039;Tensor Core&#039;&#039;&#039; units require FP16 (or BF16) inputs but accumulate results in FP32, effectively ensuring that reductions are done in higher precision&amp;lt;ref name=&amp;quot;ref6&amp;quot;/&amp;gt;. Research showed that with proper loss scaling and mixed precision, models can train to FP32-level accuracy using mostly FP16 arithmetic, achieving significant speedups.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;BFloat16 (BF16)&#039;&#039;&#039; emerged as an alternative 16-bit format primarily from Google’s TPU training systems. BF16 has a larger exponent (8 bits, same as FP32) but only 7 fraction bits&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. This means BF16 can handle the &#039;&#039;&#039;same dynamic range&#039;&#039;&#039; as FP32 (no overflows where FP32 wouldn’t) but at the cost of even lower precision than FP16 (BF16 has about 2–3 decimal digits of precision vs FP16’s ~3–4). Despite its coarse precision, BF16 turns out to be effective for training deep networks because the algorithms (stochastic gradient descent, etc.) are somewhat robust to that noise, and having the wide range prevents catastrophic loss of signal (no gradient will overflow to Inf unless it was going to in FP32 anyway). Many training frameworks (PyTorch, TensorFlow) support BF16 training on hardware like &#039;&#039;&#039;TPU, NVIDIA A100+, Intel CPUs with AVX-512 BF16, etc&#039;&#039;&#039;. Typically, when using BF16, &#039;&#039;&#039;accumulations are still done in FP32&#039;&#039;&#039; (for example, a TPU v2/v3 multiplies BF16 matrices and accumulates into an FP32 result for the dot product&amp;lt;ref name=&amp;quot;ref6&amp;quot;/&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
In inference, both FP16 and BF16 are used to deploy models for faster speed or lower memory. FP16 is popular on NVIDIA GPUs (which have specialized FP16 units). BF16 is advantageous on TPUs and some CPUs that support it, and increasingly on GPUs (Ampere and newer GPUs also support BF16 arithmetic). The choice between FP16 and BF16 can depend on hardware: BF16’s advantage is easier software conversion (you can often take an FP32-trained model and cast weights to BF16 with minimal loss, since the dynamic range is intact), whereas FP16 might require adjusting the model or ensuring values fit in range (sometimes needing techniques like maintaining a master copy of weights in FP32 during training, or handling out-of-range values).&lt;br /&gt;
&lt;br /&gt;
For ONNX, both Float16 and BFloat16 types are part of the standard. ONNX models can be converted to FP16 (truncating weights and inserting cast nodes for computations) using tools like &amp;lt;code&amp;gt;float16_converter&amp;lt;/code&amp;gt; in ONNX Runtime&amp;lt;ref name=&amp;quot;ref8&amp;quot;/&amp;gt;. ONNX Runtime on GPUs will typically execute those FP16 ops with mixed precision on tensor cores (FP16 input, FP32 accumulate), whereas on CPU it may emulate FP16 or upcast to FP32 (as many CPU implementations do not natively support half-precision math)&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;. In fact, as an example, running an ONNX model in FP16 on a CPU might insert hidden Cast operations to FP32 behind the scenes, because the CPU kernel for that op only exists in FP32&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;. This ensures correctness (the computation is done in higher precision) but can reduce performance. We will revisit such backend-dependent behavior in Section 5.3.&lt;br /&gt;
&lt;br /&gt;
In summary, &#039;&#039;&#039;FP16 and BF16&#039;&#039;&#039; are now established as important low-precision formats in deep learning. FP16 offers more precision bits, which can matter for e.g. small differences, but has a limited range (care needed to avoid overflow). BF16 offers safety in range but less precision (which can slightly affect convergence or final accuracy in some cases). Both typically rely on FP32 for accumulation to achieve acceptable accuracy. When deploying models, one should choose a format supported efficiently by the target hardware and verify that the accuracy is not significantly degraded. Often, CNNs and transformers can run in FP16/BF16 with virtually no drop in score, but certain operations (like softmax, see Section 6.3) might need special handling.&lt;br /&gt;
&lt;br /&gt;
=== 4.3. FP8 Formats (E4M3, E5M2) and Scaling ===&lt;br /&gt;
&lt;br /&gt;
The push for ever lower precision has led to experimental &#039;&#039;&#039;8-bit floating point (FP8)&#039;&#039;&#039; formats for deep learning. In 2022, researchers from NVIDIA, Intel, and Arm proposed an FP8 standard with two variants: &#039;&#039;&#039;E4M3&#039;&#039;&#039; (4 exponent bits, 3 fraction bits) and &#039;&#039;&#039;E5M2&#039;&#039;&#039; (5 exponent bits, 2 fraction bits)&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;. Both have 1 sign bit, for a total of 8 bits. E5M2 has a wider exponent range (5 bits gives bias 15) but one fewer mantissa bit than E4M3. The idea is to use E4M3 for numbers that need more precision (like weights or activations around 1.0) and E5M2 for numbers that need more dynamic range (like gradient updates which can be very small or large). In fact, the study found that using E4M3 for forward activations/weights and E5M2 for backward gradients can allow training of large networks with FP8 to accuracy on par with FP16 training&amp;lt;ref name=&amp;quot;ref11&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
FP8 formats are &#039;&#039;not&#039;&#039; IEEE 754 standard (though IEEE is considering lower precision in its 2019 revision). Notably, the E4M3 format as used by NVIDIA does not allocate any bit-pattern for $+\infty$ or $-\infty$ – it has only NaN for invalid results, using that freed bit pattern to extend the representable range by one exponent value&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;. Essentially, E4M3 can represent numbers up to ~$2^{15}$ (around 3e4) instead of capping at infinity at $2^{16}$, by foregoing infinities&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;. Variations also exist regarding negative zero and how NaNs are handled&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;. For instance, GraphCore’s IPU uses FP8 formats that disallow negative zero and infinities, to maximize useable range&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Using 8-bit floats for neural networks requires additional techniques, chiefly &#039;&#039;&#039;scaling&#039;&#039;&#039;. Because 8-bit has very limited precision and range, one typically applies a power-of-two scale factor to groups of values to normalize them into an FP8-friendly range. This is akin to block floating-point or “microscaling” per layer or per tensor. For example, one might keep a FP32 scale for each channel or tensor, and represent values as $(FP8\_value) \times 2^{scale}$. The ONNX standard has introduced support for FP8 in version 1.15, including types for E4M3 and E5M2 (with variations FNUZ: finite-only and no negative zero)&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;, and even a type called &#039;&#039;&#039;E8M0&#039;&#039;&#039; which is an 8-bit exponent-only number used purely as a scale factor&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;. The E8M0 type has 8 exponent bits, no mantissa, effectively representing powers of two from $2^{-127}$ to $2^{128}$ for scaling purposes&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;. This indicates an approach where an FP8 tensor would be accompanied by an E8M0 scale (or scales) to interpret its value correctly.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Rounding&#039;&#039;&#039; becomes critical when casting values to FP8. The quantization error is much larger relative to FP16 or FP32. Studies have shown that choosing the right rounding mode (e.g., round-to-nearest, stochastic rounding, etc.) and clamping behavior (saturating to max instead of inf) can affect model accuracy&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;. ONNX added a &amp;lt;code&amp;gt;round_mode&amp;lt;/code&amp;gt; attribute to its Cast operator to allow controlling this for FP8&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;. In particular, one paper found that &#039;&#039;&#039;rounding up with saturation&#039;&#039;&#039; (round-to-nearest ties away from 0, and clamp overflows to max finite rather than inf) gave better accuracy in large language model training than other rounding modes&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
At present, FP8 is on the cutting edge. NVIDIA’s H100 GPU supports FP8 tensor cores (for both E4M3 and E5M2), and early results show minimal accuracy loss on some vision and NLP tasks when properly tuned. However, using FP8 requires fine-tuning hyperparameters like scaling factors for each layer and perhaps tweaking training procedures. ONNX’s inclusion of FP8 types means you can represent and transport models using FP8 weights or activations, but actual hardware support is limited and typically paired with calibration or dynamic scaling logic outside of ONNX graph (or using custom ONNX functions for scaling). It remains an &#039;&#039;&#039;open research area&#039;&#039;&#039; how to fully automate FP8 quantization for arbitrary models without accuracy loss.&lt;br /&gt;
&lt;br /&gt;
In summary, FP8 formats promise further compression of neural network computation and data. They are an order of magnitude more error-prone than FP16, but when combined with careful scaling, mixed usage of E4M3/E5M2, and maybe modified training techniques, they have shown surprising success. ONNX is actively evolving to support these ultra-low precision types, signaling their potential importance in future AI inference deployments.&lt;br /&gt;
&lt;br /&gt;
== Accumulation Semantics in ONNX ==&lt;br /&gt;
&lt;br /&gt;
One of the roles of the ONNX specification is to define the mathematical behavior of operators (Conv, MatMul, Sum, etc.) in a framework-agnostic way. However, ONNX mostly defines &#039;&#039;what&#039;&#039; an operator computes in exact arithmetic, not &#039;&#039;how&#039;&#039; the computation is carried out in finite precision. This leaves certain details – like accumulator precision or summation order – up to the implementation (runtime or hardware). We&#039;ll examine how ONNX treats accumulation and what guarantees (or lack thereof) are provided.&lt;br /&gt;
&lt;br /&gt;
=== 5.1. Operator Definitions and Numerical Guarantees ===&lt;br /&gt;
&lt;br /&gt;
Each ONNX operator is described as performing an ideal mathematical function on its inputs. For example, &amp;lt;code&amp;gt;MatMul&amp;lt;/code&amp;gt; is defined as the standard matrix product of two tensors, and &amp;lt;code&amp;gt;ReduceSum&amp;lt;/code&amp;gt; computes a summation along given axes. The ONNX spec, as of current versions, &#039;&#039;&#039;does not mandate the precision of intermediate calculations&#039;&#039;&#039; beyond the types of the inputs and outputs. If an operator’s inputs and outputs are float16, the spec implies the computation is done in float16 in a sense, but it does not forbid an implementation from using higher precision internally – it simply doesn&#039;t mention it. There is &#039;&#039;&#039;no explicit guarantee of identical bit-for-bit results&#039;&#039;&#039; across different ONNX backends for floating-point ops. Instead, results are expected to be numerically close, within tolerances one would normally expect from floating-point differences (due to different hardware or algorithms). The ONNX Backend Test suite typically uses a relative or absolute error tolerance when validating outputs for this reason.&lt;br /&gt;
&lt;br /&gt;
Because of this, important details like the use of extended precision or accumulation in higher precision are left ambiguous. This has led to discussions in the ONNX community about making such behavior more explicit. For example, a proposal was raised to allow specification of accumulation precision for certain ops like MatMul&amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;. The reasoning is that as lower-precision types (float16, bfloat16, float8) become more common, knowing or controlling whether an op internally accumulates in a higher precision is important for both accuracy and reproducibility&amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;. In June 2025, an issue on the ONNX GitHub suggested adding an attribute to MatMul (and similarly Gemm, Softmax, etc.) to specify the accumulator type&amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;. By default it would use input precision (status quo), but an attribute could request (for instance) 32-bit accumulation even if inputs are 16-bit&amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;. As of this writing, however, no such attribute exists in the official spec – it&#039;s an open issue. Thus, the numerical semantics are effectively: &#039;&#039;“compute the result as if in the same type as inputs, but actual implementations may use more precision internally as an optimization.”&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
From a guarantee standpoint, ONNX operators promise the mathematically correct result assuming real-number arithmetic, but they &#039;&#039;&#039;do not guarantee bit-exact results&#039;&#039;&#039; due to floating-point effects. There is an understanding that differences can arise from different summation orders, fused operations (e.g., an implementation might use an FMA (fused multiply-add) which has one rounding instead of two), or use of extended precision registers. ONNX does not formally define rounding modes either – it relies on IEEE 754 defaults, but if a backend used something like deterministic summation or alternative rounding, ONNX has no mechanism to express that.&lt;br /&gt;
&lt;br /&gt;
In summary, ONNX&#039;s current stance is largely &#039;&#039;&#039;implementation-dependent&#039;&#039;&#039; for numerical precision. It provides minimal explicit numerical guarantees beyond type constraints. This is why an ONNX model run on two different hardware or runtime libraries can have slightly different results, especially in reduced precision. The burden is on backends to produce results &amp;quot;close enough&amp;quot; to each other that they don&#039;t affect the model&#039;s decisions in any significant way.&lt;br /&gt;
&lt;br /&gt;
=== 5.2. MatMul, Conv, and Reduction Operators ===&lt;br /&gt;
&lt;br /&gt;
Consider ONNX &amp;lt;code&amp;gt;MatMul&amp;lt;/code&amp;gt;: it takes two tensors (often two matrices) and produces their matrix product. In pure math, $C_{ij} = \sum_k A_{ik} &#039;&#039; B_{kj}$. If A and B are float16, should that sum be computed in float16 or float32? As discussed, ONNX doesn’t specify – it simply says the output is a float16 matrix containing the product. In practice, many backends will &#039;&#039;promote&#039;&#039; the computation: for example, NVIDIA GPUs will do the multiplication and addition in a mixed FP16/FP32 mode by default (because of tensor cores), then convert the final result back to float16. ONNX Runtime on CPU might not support float16 multiplication natively, so it will insert cast ops to float32, perform the MatMul in float32, then cast back to float16&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;. From ONNX’s perspective, as long as the final result is within acceptable error of the “ideal” float16 computation, it’s fine. But note, an “ideal” float16 computation (where every multiply and add is rounded to FP16) might actually be &#039;&#039;less accurate* than what the backend did with FP32. This means that a backend using higher precision can produce a result that is not bit-equal to a pure FP16 implementation – it&#039;s actually closer to the true real number result. This is usually considered a good thing (improved accuracy), but it does highlight a lack of consistency: another backend might do everything in pure FP16 and get a slightly different answer.&lt;br /&gt;
&lt;br /&gt;
The same situation occurs for &#039;&#039;&#039;convolution&#039;&#039;&#039; (&amp;lt;code&amp;gt;Conv&amp;lt;/code&amp;gt; op) which is essentially a batched sum of products (like MatMul in sliding window), and for explicit reduction ops like &amp;lt;code&amp;gt;ReduceSum&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;ReduceMean&amp;lt;/code&amp;gt;, etc. If you have an ONNX &amp;lt;code&amp;gt;ReduceSum&amp;lt;/code&amp;gt; that sums 1000 float16 values, the spec doesn’t say how to sum them. A GPU kernel might sum in a tree using FP32 accumulators; a naive implementation might sum in order with FP16 and possibly overflow or underflow intermediate steps. Both are allowed as long as they output a float16 result that is “reasonable.” In practice, ONNX Runtime and others take care to avoid obviously bad behavior (like overflow) by using higher precision or by splitting the sum. But again, it’s not mandated by the ONNX standard explicitly.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Softmax&#039;&#039;&#039; is an interesting case: Softmax involves an internal sum of exponentials. ONNX defines the Softmax operator as $$\mathrm{Softmax}(x)_i = \frac{\exp(x_i)}{\sum_j \exp(x_j)},$$ typically applied along a specified axis. Directly computing this in float16 can be problematic for large vectors, as discussed in Section 6.3. Indeed, the issue of accumulation precision arises: should the sum $\sum_j \exp(x_j)$ be done in FP16 or can it be in FP32? Many implementations will do it in FP32 even if $x$ is FP16, because the dynamic range of $\exp(x)$ is large. The ONNX Softmax spec doesn’t detail this, but it expects the result to be a proper probability distribution (summing to 1.0 within normal floating error). A pure FP16 softmax on, say, 1000 elements could sum to something like 0.996 or 1.004 due to rounding, whereas FP32 accumulation would be much closer to 1.000. ONNX likely allows both; the small difference is usually inconsequential, but if it were large (say the FP16 sum overflowed to inf), that would be a correctness issue. A robust backend will prevent that (by using the common max-subtraction trick and higher precision accumulation). The ONNX issue 7072 indeed calls out Softmax as another op where explicit control of accumulation precision would be useful&amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
In summary, &#039;&#039;&#039;MatMul, Conv, and reduction ops&#039;&#039;&#039; in ONNX are defined mathematically but with &#039;&#039;implicit&#039;&#039; numerical behavior. Implementations commonly use higher precision for intermediate sums especially when input is low precision, but this is not standardized. Consequently, results can differ slightly between implementations. For critical applications, one must be aware of these differences. A model exported to ONNX with float16 weights might perform slightly differently on one runtime vs another if one does all float16 math and the other uses float32 accumulations. Both conform to ONNX. As a user, if you require a certain precision for accumulation, currently you have to ensure it by the way you export or run the model (for example, insert manual cast nodes in the ONNX graph to force certain ops to FP32). We may see future ONNX versions address this with attributes or metadata as mentioned, but for now these ops are effectively “compute with best effort accuracy”.&lt;br /&gt;
&lt;br /&gt;
=== 5.3. Backend and Hardware-Dependent Behavior ===&lt;br /&gt;
&lt;br /&gt;
Because ONNX is implemented by many backends (ONNX Runtime, TensorRT, CoreML, PyTorch JIT, etc.), and these in turn run on varied hardware (CPU, GPU, accelerators), the numeric behavior can be backend-dependent. Some examples:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;CPU backends:&#039;&#039;&#039; Many CPUs do not have native float16 vector arithmetic (with a few exceptions in recent CPUs). ONNX Runtime’s default CPU execution provider will often upcast float16 to float32, compute, then downcast&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;. This ensures accuracy but means that if you thought you were getting a full float16 pipeline, you’re not – the CPU is essentially ignoring the low precision in compute. On the other hand, x86 CPUs have extended 80-bit precision in x87 registers (though less used now) and 64-bit SIMD registers for doubles; some compiler optimizations might carry intermediate results in higher precision. This could make CPU results slightly different from a GPU’s if, say, a sum is done in 80-bit on CPU versus 32-bit on GPU. Generally, modern compilers avoid such extended precision unless explicitly asked, to improve reproducibility.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;GPU backends:&#039;&#039;&#039; GPUs often perform summations in parallel using tree reductions. The order of operations can change depending on how threads are scheduled. This means even on the same GPU, summing an array in one launch versus another might not give bit-identical results if the internal parallel reduction algorithm differs (though it should be deterministically the same given the same library version and input, but if you change GPU model or BLAS library, it might differ). Also, GPUs have fused multiply-add (FMA) instructions that do $(a*b + c)$ in one step with one rounding, which can produce slightly different results than doing the multiply and add separately (which would round twice). If an ONNX backend uses a fused kernel (like a GEMM library) it might yield different low-order bits than a simple loop.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Vendor libraries:&#039;&#039;&#039; ONNX Runtime can use vendor BLAS or DNN libraries (MKL, cuDNN, TensorRT, etc.). These libraries often implement their own strategies for numerical stability. For example, NVIDIA’s cuDNN might use FP32 accumulation for FP16 inputs by default. TensorRT (NVIDIA’s inference engine) will sometimes automatically promote precisions or insert scaling to maintain stability&amp;lt;ref name=&amp;quot;ref13&amp;quot;/&amp;gt;. On the other hand, if you explicitly request FP16 in TensorRT with no FP32 fallback, it might do the entire op in FP16 and you’d see more error. Some libraries offer knobs: TensorRT allows choosing FP32, FP16, or INT8 precision for each layer, and will use FP32 internally for safety in some cases unless forced otherwise.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Hardware-specific quirks:&#039;&#039;&#039; As another example, NVIDIA’s &#039;&#039;&#039;TensorFloat-32 (TF32)&#039;&#039;&#039; on Ampere GPUs is a precision mode where matrix multiplications use 10-bit mantissa (so about FP16 precision for the multiply) but keep 8-bit exponent (range like FP32). By default, CUDA libraries use TF32 for FP32 matrix multiplies on Ampere to speed them up, unless you disable it. This means if you run an ONNX model with FP32 on an A100 GPU, your matmuls might actually be lower precision than FP32 (though higher than FP16)&amp;lt;ref name=&amp;quot;ref6&amp;quot;/&amp;gt;. It’s an example of hardware choosing an intermediate precision for performance. From ONNX’s perspective, the output is still FP32 and (hopefully) within error tolerances, but a user might be surprised that it’s not a “true” FP32 computation internally. If exactness is needed, one has to turn off TF32.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Non-determinism:&#039;&#039;&#039; Some backends (especially on GPUs) allow non-deterministic execution for performance. For instance, atomic adds in parallel reductions can produce results that vary run-to-run because the order of accumulation is race-condition-dependent. ONNX’s spec doesn’t cover this explicitly, but it’s something to consider. In ONNX Runtime, there are settings for deterministic computing, but by default it may use all available performance optimizations, even if results vary at the 1e-7 level.&lt;br /&gt;
&lt;br /&gt;
In short, the numerical behavior under ONNX can depend on the combination of backend software and hardware capabilities. The lack of a strict spec for accumulation means implementations have freedom to use clever strategies (higher precision, reordering) to improve accuracy or speed. Usually this is positive (e.g. getting better accuracy than a naive approach). But it could also lead to slight inconsistencies. For example, you might find that exporting a PyTorch model to ONNX and running it with one engine yields outputs slightly different from PyTorch’s – not because ONNX is wrong, but because the summation or convolution was done differently (maybe PyTorch summed in FP32 as well, but maybe not in exactly the same way).&lt;br /&gt;
&lt;br /&gt;
The key takeaway is that ONNX &#039;&#039;by design&#039;&#039; abstracts away these details, which is good for portability but means you need to &#039;&#039;&#039;trust the backend&#039;&#039;&#039; or verify its precision behavior for sensitive models. If necessary, you can enforce certain behavior by inserting ops (for example, if you want to ensure FP32 accumulation, you could insert a cast to FP32 for inputs of a MatMul and cast the result back to FP16 – this way the ONNX graph itself mandates FP32 compute in between). This of course sacrifices some performance or device-specific magic.&lt;br /&gt;
&lt;br /&gt;
Looking forward, as mentioned, there are proposals to enrich ONNX’s specification regarding precision. This might include explicit flags for “exact summation” or “accumulate in int32” etc., which would make models more self-descriptive but also potentially less portable (if a backend doesn’t support that, what then?). For now, users should be aware of these backend differences. Most of the time, well-engineered ONNX runtimes yield results close enough that they don&#039;t affect the application (e.g., classification top-1 remains the same even if low-level sums differ by 1e-5). But for high-stakes numerical computations, testing and maybe adjusting the model or backend settings is prudent.&lt;br /&gt;
&lt;br /&gt;
== Numerical Stability in Deep Learning Models ==&lt;br /&gt;
&lt;br /&gt;
Deep learning computations involve many operations that, while theoretically well-defined, can be sensitive to numerical issues when implemented in finite precision. We&#039;ll discuss a few common scenarios in neural networks where numerical stability is a concern, and how precision and accumulation play a role.&lt;br /&gt;
&lt;br /&gt;
=== 6.1. Dot Products and Large-Scale Reductions ===&lt;br /&gt;
&lt;br /&gt;
Neural networks are full of dot products: every fully-connected layer computes $\mathbf{y} = W\mathbf{x}$, which is a series of inner products between weight vectors and the input. Convolutional layers compute weighted sums of local regions. Attention mechanisms compute large matrix products of query/key/value matrices. All these boil down to summing a lot of terms: $\sum_{k=1}^K a_k b_k$. When $K$ is large (it could be in the thousands or more for a single neuron in a fully connected layer of a large model), the potential for floating-point error in that sum increases.&lt;br /&gt;
&lt;br /&gt;
If using FP32, the error may still be negligible in many cases, but in lower precision like FP16, it can become significant. For example, consider a dot product of length 2048 in FP16. Each product is in FP16 (with ~3 decimal digits precision) and if accumulated in FP16, the worst-case error could be on the order of $2048 \times \varepsilon_{FP16} \approx 2048 \times 0.0009766 \approx 2$ (i.e., you could lose a couple units in the third decimal place relative accuracy in worst case). Even in random error accumulation, you might see error on the order of $\sqrt{2048} \times 0.0009766 \approx 0.044$ or 4.4% (though this is a rough estimate). That’s quite significant. Fortunately, as discussed, most hardware will accumulate such sums in a higher precision like FP32, dramatically reducing that error (~7 orders of magnitude smaller $\varepsilon$). But if it didn’t, the network’s outputs could get noisy. A fully-connected layer with many inputs might output slightly different results depending on summation order if done in low precision.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Large-scale reductions&#039;&#039;&#039; also occur in operations like global average pooling (summing all elements of a feature map) or computing the total loss by summing loss over batch samples. If one sums 100,000 values in FP32, the worst-case error could be on the order of 100,000 &#039;&#039; 1e-7 ≈ 1e-2 (1% uncertainty), though typical error would be much less. In FP16, summing 100k values is likely impossible without overflow: as noted, adding ~10^4 moderate numbers can overflow FP16&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. So large reductions &#039;&#039;must* be handled carefully (either in chunks or in higher precision). Frameworks generally do: for example, when performing a sum reduction on GPU, libraries break it into parallel blocks that accumulate in registers (usually 32-bit registers, even if inputs are 16-bit). &lt;br /&gt;
&lt;br /&gt;
Another aspect is that many dot products in neural nets involve values of varying magnitude. If the distribution of terms is such that some are much larger than others, the smaller terms might not contribute much if summed last. Ideally, one might sort by magnitude or use pairwise summation to improve that. Most BLAS libraries effectively do a pairwise strategy due to their parallel structure. So, while worst-case analyses are grim, practical implementations mitigate a lot of it.&lt;br /&gt;
&lt;br /&gt;
However, certain layers can suffer if not handled properly. One famous example is in RNNs or LSTMs: if one uses FP16 naive summation for the gating mechanism, errors could accumulate over many time steps and cause drift or instability. In modern practice, recurrent layers are often run in FP16 with FP32 accumulators to avoid this.&lt;br /&gt;
&lt;br /&gt;
In summary, &#039;&#039;&#039;dot products and large reductions need sufficient precision&#039;&#039;&#039; or robust algorithms. If you are implementing a custom kernel (say a custom ONNX op) and you have to sum a huge array, you should consider using a technique from Section 3 (pairwise summation, Kahan, or at least double accumulator if possible). Many deep learning computations, by virtue of hardware and library design, already do something like this under the hood. But it&#039;s always good to be aware, especially if pushing into lower precision territories.&lt;br /&gt;
&lt;br /&gt;
=== 6.2. Cancellation, Dynamic Range, and Conditioning ===&lt;br /&gt;
&lt;br /&gt;
We introduced &#039;&#039;cancellation&#039;&#039; earlier in the context of summation. In deep learning, cancellation can appear in a few places. One is in the computation of certain norms or variances. For instance, Batch Normalization computes mean and variance of activations. The variance formula $\mathrm{Var}(x) = E[x^2] - (E[x])^2$ is subject to catastrophic cancellation if $E[x]$ and $\sqrt{E[x^2]}$ are close in magnitude (which can happen if the variance is small compared to the mean). If implemented naively in single precision, you might get a negative variance due to numerical error (which would then be clamped to zero or sqrt would yield NaN). Robust implementations compute variance in a single pass or increase precision. Indeed, in many frameworks, batchnorm accumulation is done in FP32 even if the activations are FP16, precisely to avoid numerical issues in computing mean and variance. ONNX itself doesn’t specify this, but an ONNX backend is likely to do batchnorm stats in higher precision for safety. The &#039;&#039;conditioning&#039;&#039; of the variance calculation is poor when variance is tiny relative to mean, so it&#039;s a spot where higher precision is needed.&lt;br /&gt;
&lt;br /&gt;
Another example is in certain loss functions or regularizations. If you subtract two large numbers to get a small difference, you lose precision. Say you have two nearly identical predictions and you subtract them as part of some operation – if done in float16, you might get zero whereas float32 would have a slight difference. This could affect, say, a gradient if that difference was supposed to propagate information. Fortunately, neural nets often avoid directly subtracting large nearly equal numbers; but it can happen. In attention mechanisms, the &#039;&#039;score normalization&#039;&#039; (softmax) subtracts a max to avoid huge exponentials, which is good for range but if multiple scores are close to the max, you subtract numbers that are close. However, those subtractions are not catastrophic in the same way; they&#039;re intentional to reduce range and are typically fine in FP32 or even FP16.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Dynamic range&#039;&#039;&#039; issues are everywhere in deep learning. Activation functions like ReLU and exponential can produce very large or very small values. If subsequent computations don’t account for that, you get infinities or zeros. For example, consider a network output that produces a logit of 100 in FP16. $\exp(100)$ in FP16 overflows to infinity (since FP16 max ~ 6e4 and $e^{100} \sim 3.7e43$). If you then do softmax, that one infinity will dominate the sum and you might get a probability of 1 for that class (or NaNs if not handled). The standard softmax implementation subtracts the max logit, so in this case it would subtract 100 from all logits, making the largest exponent $\exp(0)=1$ and others tiny – this avoids overflow. But what about underflow? If you had a very negative logit, $\exp(-100)$ in FP16 would underflow to 0. In softmax, that just contributes ~0 probability, which is fine (it effectively is zero anyway). Underflow tends to be less dangerous than overflow in inference — underflow usually just means “this probability/neuron is effectively 0,” whereas overflow can poison the results with NaNs or Infs. &lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Conditioning&#039;&#039;&#039; refers to how sensitive a function is to input perturbations. Neural networks themselves as functions can be poorly conditioned (e.g., tiny changes in weights cause big changes in output for some architectures), but typically they are designed to be reasonably well-behaved. Certain internal computations like matrix inversion (if one does a pseudoinverse or something for a layer) can be extremely ill-conditioned if the matrix is near singular. That again would need double precision perhaps. In ML, one common ill-conditioned operation is the &#039;&#039;&#039;softmax denominator&#039;&#039;&#039;: $\sum_j e^{x_j}$ can be very large and also very sensitive to changes in the largest $x_j$. However, the softmax is more stable if computed carefully (max trick). Another is &#039;&#039;&#039;normalization&#039;&#039;&#039;: computing $\frac{x - \mu}{\sigma}$ for layer norm or batch norm – if $\sigma$ is very small, then the division blows up noise. In those cases, a small $\epsilon$ is usually added to $\sigma^2$ to prevent instability (this is a model-level fix, not a numeric one per se, but it acknowledges numeric limits).&lt;br /&gt;
&lt;br /&gt;
In training, &#039;&#039;&#039;optimizers&#039;&#039;&#039; can suffer from precision issues. For example, the Adam optimizer keeps a moving average of squared gradients; in half precision, these small updates might flush to zero or not change at some point. Some research found that certain adaptive optimizers break in FP16 because the small differences are lost (&amp;quot;Why FP16 training breaks NAdam&amp;quot; etc.). The solution is often to keep the optimizer states in FP32 while doing the bulk of compute in FP16. This again is a form of mixed precision.&lt;br /&gt;
&lt;br /&gt;
From an inference perspective, once the model is trained, the main concerns are avoiding overflow in activations and ensuring summations (like in softmax or reductions) are done with enough precision. The model’s weights are fixed, so we don&#039;t have to worry about cumulative error over iterations as in training, just the forward pass.&lt;br /&gt;
&lt;br /&gt;
=== 6.3. Normalization Layers and Softmax ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Normalization layers&#039;&#039;&#039; (BatchNorm, LayerNorm, etc.) and &#039;&#039;&#039;Softmax&#039;&#039;&#039; are specifically worth examining for numerical stability:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Batch Normalization:&#039;&#039;&#039; During inference, batchnorm applies an affine transform using precomputed mean and variance (from training). It’s basically: $y = \frac{x - \mu}{\sqrt{\sigma^2 + \epsilon}} \gamma + \beta$. This is a stable operation as long as $\epsilon$ is provided (to avoid division by zero). Since $\mu, \sigma^2$ are constants in inference, it reduces to linear ops which are fine. The main numerical work (computing mean/variance) happened during training or model calibration, and frameworks usually did that in FP32. So batchnorm at inference time is not a source of instability (it’s just a scale and shift of the data).&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Layer Normalization / Group Normalization:&#039;&#039;&#039; These compute mean/var on the fly for each sample (and possibly channel group). In inference, they still need to compute these statistics for each forward pass. If implemented in low precision, the variance calculation can suffer cancellation as mentioned. However, it&#039;s likely that inference engines compute the mean and variance in FP32 even if inputs are FP16 (especially since these are per-sample operations, not huge batch reductions – it&#039;s feasible to do in higher precision). If someone implemented layer norm purely in FP16, they might see issues when the variance is very small. Usually there’s an $\epsilon$ added which might dominate a tiny variance and thus mitigate catastrophic cancellation (the result might just output something mostly from $\epsilon$). In summary, normalization layers are usually fine as long as the implementation uses enough precision for the summation of squares.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Softmax:&#039;&#039;&#039; Softmax is notorious if done improperly. The safe algorithm: subtract the maximum input from all inputs (to bring the largest exponent to 1.0), exponentiate, sum them up, divide each exponent by the sum. The max subtraction handles overflow in $\exp()$. But what about the sum? As discussed, if there are $N$ elements in the softmax, the sum is at most $N$ (when all inputs equal the max). In classification, $N$ might be the number of classes. For ImageNet, $N=1000$, so worst sum ~1000 (which is fine in FP16, as 1000 &amp;lt;&amp;lt; 65504). For an extreme case like a language model, $N$ could be 50,000 (vocab size). Sum could be 50,000 in worst case (everyone equal probability). 50,000 is still below 65504, so actually an FP16 sum would &#039;&#039;not&#039;&#039; overflow in that scenario, and the rounding error on a sum of 50k identical terms would be maybe a few ULPs (which at 50k magnitude might be ~0.5 difference worst-case). So maybe softmax summation in FP16 is not catastrophic for output probabilities – you’d get essentially 1.0 after normalization instead of 0.99999 in some case. Usually fine.&lt;br /&gt;
&lt;br /&gt;
:The bigger issue is if one or a few terms dominate the softmax. If one term is much larger, the others underflow to 0 after the max subtraction. That’s fine: you get a one-hot distribution essentially. If the model expected a softer distribution, that could be a problem. But if the logits were really that far apart, even FP32 would give nearly one-hot. So not really a precision issue, more of a model property.&lt;br /&gt;
&lt;br /&gt;
:However, the &#039;&#039;&#039;stability during training&#039;&#039;&#039; is a big issue. The GitHub thread we saw (ColossalAI) mentioned training a 130B model where FP16 softmax in attention caused training to occasionally blow up&amp;lt;ref name=&amp;quot;ref14&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref14&amp;quot;/&amp;gt;. They observed attention scores in some heads ranging up to 1e4 or as low as 1e-3&amp;lt;ref name=&amp;quot;ref14&amp;quot;/&amp;gt;. 1e4 exponent in FP16 is definitely Inf (because $\exp(10^4)$ is astronomically huge, but even exp(100) is inf in half). The method CogView introduced (Precision Bottleneck Relaxation) involved subtracting the max &#039;&#039;per head&#039;&#039; to reduce range&amp;lt;ref name=&amp;quot;ref14&amp;quot;/&amp;gt;—which is basically the softmax max trick at a finer granularity or something similar. In the end, they found &#039;&#039;&#039;the simplest fix was to compute softmax in FP32&#039;&#039;&#039; for those layers&amp;lt;ref name=&amp;quot;ref14&amp;quot;/&amp;gt;. They reported that it had negligible speed impact but significantly improved stability&amp;lt;ref name=&amp;quot;ref14&amp;quot;/&amp;gt;. This highlights a general principle: certain operations like softmax (and sometimes matrix multiplications in attention) benefit from higher precision, and using FP32 in a few critical spots can save a mixed-precision training from crashing. In inference, using FP32 for softmax is also a safeguard if one is uncertain. It costs little because softmax is not a heavy compute compared to matrix multiplies.&lt;br /&gt;
&lt;br /&gt;
:ONNX does have a Softmax operator; if the input is float16, a backend might do exactly what they described: internally cast to float32 for the softmax computation, then cast output back to float16. Some inference engines (like DeepSpeed’s inference or others) keep attention softmax in FP32 even if rest is FP16, for robustness&amp;lt;ref name=&amp;quot;ref14&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Apart from softmax, other places where exponentials occur are in loss functions like cross-entropy (which uses log-softmax internally), or in activation functions like Sigmoid and Tanh (which are basically scaled logistic functions). Sigmoid($x)=1/(1+e^{-x})$. If $x$ is moderately large positive, $e^{-x}$ underflows to 0 in FP16 if $x&amp;gt;~11$. That gives a sigmoid output of 1.0 exactly. In FP32, $e^{-x}$ for $x=11$ is about $5e-5$, not underflow, so sigmoid ~0.99995. So FP16 will saturate some outputs to exactly 0 or 1 sooner than FP32 would. This can slightly change behaviors (e.g., a neuron might go to full saturation a bit early). Usually, this isn’t a huge deal, but it could make gradients zero earlier. In inference, we don’t care about gradients, just the output value. Getting exactly 1.0 vs 0.99995 probably has no practical difference for a classification output. But one should be aware that &#039;&#039;&#039;activation functions can saturate faster&#039;&#039;&#039; in lower precision due to limited range or precision. ReLU has no issue (it’s linear or 0), but exponential-based activations do. In practice, again, frameworks often simply compute those in the given precision and accept the slight difference. If it were a problem, one could do those in FP32 as well.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Summary:&#039;&#039;&#039; Numerical stability in inference is maintained by using robust computation methods: subtracting maxima in softmax/LogSumExp, adding epsilon in denominators for normalization, and using higher precision for summations or critical steps. Most of these are built into the algorithms. ONNX models assume these standard practices (they don&#039;t need to explicitly do them in the graph because the backend inherently will for ops like Softmax). It&#039;s wise when building or exporting models to ONNX to test them in the target precision and ensure there are no surprises (like an output becomes NaN or Inf due to precision). Usually if training was done in mixed precision, the model will have been vetted in that environment already.&lt;br /&gt;
&lt;br /&gt;
== Performance vs. Accuracy Trade-offs ==&lt;br /&gt;
&lt;br /&gt;
There is an inherent trade-off between numerical precision and computational performance (speed, memory usage) in AI systems. Choosing lower precision can dramatically increase throughput and reduce memory, but it may introduce arithmetic errors. Engineers must balance these considerations:&lt;br /&gt;
&lt;br /&gt;
=== 7.1. Throughput, Memory Bandwidth, and Precision ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Throughput (speed)&#039;&#039;&#039; is often higher for lower-precision operations. Modern hardware can execute more low-precision ops in parallel than high-precision ops. For example, an NVIDIA V100 GPU can perform 2× more half-precision FLOPs than single-precision per clock, and newer architectures with Tensor Cores can do 8× more (since a Tensor Core warp operation might perform 64 FP16 FMAs per cycle vs 16 FP32). Similarly, int8 operations can be even faster and more energy efficient. Lower precision numbers also use less memory bandwidth – fetching 16-bit data is twice as fast as 32-bit data from memory (assuming the same bus width), which can relieve memory bottlenecks. Many deep learning workloads are memory-bound, so halving the data size can nearly double throughput if compute is not the limiting factor. This is a huge incentive to use lower precision.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Memory and storage&#039;&#039;&#039;: Using FP16 or int8 weights reduces model size, which is important for deploying large models to memory-limited environments (like edge devices or GPUs with limited VRAM). It also means you can fit larger batch sizes in memory, or more models on a chip, etc. There is also a cascade effect: smaller data means better cache utilization, which further improves performance.&lt;br /&gt;
&lt;br /&gt;
However, &#039;&#039;&#039;accuracy&#039;&#039;&#039; suffers if precision is too low. We have discussed how rounding error and limited range can cause deviations in outputs. The question is: do those deviations matter for the application? Often for neural networks, a small amount of noise or error in intermediate calculations does not change the final prediction. Neural nets are somewhat error-tolerant due to their redundant distributed representations. This is why quantization and FP16 techniques can work at all. The final model accuracy (e.g. top-1 accuracy on ImageNet) might drop only 0.5% or less when using FP16 instead of FP32, or int8 instead of FP16, if done properly. For many use-cases, this loss is acceptable given the speed gains.&lt;br /&gt;
&lt;br /&gt;
If one goes too low (like naive FP8 or int4 without special care), accuracy might drop severely – perhaps making the model unusable. Thus the trade-off: as precision decreases, performance increases but at some point accuracy degrades beyond an acceptable threshold. Engineers aim to find the lowest precision that still preserves accuracy.&lt;br /&gt;
&lt;br /&gt;
There is also the consideration of &#039;&#039;&#039;accumulation strategy&#039;&#039;&#039; affecting performance: for instance, Kahan summation (compensated) is more accurate but ~4× slower than naive summation. If you need that accuracy, you pay a performance cost. In many AI inference scenarios, the slight extra accuracy from something like Kahan isn&#039;t needed, so it’s not used. But in a scenario where you sum millions of elements for a final result that is thresholded, maybe you do need it.&lt;br /&gt;
&lt;br /&gt;
Another dimension is &#039;&#039;&#039;determinism vs speed&#039;&#039;&#039;. Enforcing a deterministic reduction order (for reproducibility) might require disabling some parallelism, which could slow things down. If exact repeatability is critical (like certain scientific or financial applications of neural nets), one might sacrifice some performance to ensure the summation happens the same way every time. ONNX allows both modes depending on the runtime.&lt;br /&gt;
&lt;br /&gt;
From a memory perspective, if precision is higher than needed, you waste memory. For example, storing intermediate activations in FP32 when they could be FP16 uses 2× memory, which might mean lower batch size or more memory traffic. So there&#039;s strong motivation to keep things as low-precision as possible &#039;&#039;except&#039;&#039; where accuracy truly demands.&lt;br /&gt;
&lt;br /&gt;
In summary, the trade-off often manifests as &#039;&#039;diminishing returns&#039;&#039;: FP64 vs FP32 – huge slowdown for usually negligible accuracy gain in ML; FP32 vs FP16 – moderate speedup for usually minor accuracy hit; FP16 vs int8 – another big speedup but maybe more accuracy loss unless calibration is done; int8 vs int4 – speed gain but often big accuracy drop that might not be worth it for current models.&lt;br /&gt;
&lt;br /&gt;
Engineers use techniques like &#039;&#039;&#039;mixed precision&#039;&#039;&#039; to target high precision only where needed. For example, run most of the model in FP16, but keep a few sensitive layers in FP32. This yields most of the performance benefits while ensuring critical parts are accurate. We already saw softmax as one example. Another might be final output layer: sometimes using FP32 for the logits and softmax can be safer.&lt;br /&gt;
&lt;br /&gt;
Hardware is also trending to support mixed modes – like FP8 Tensor Cores accumulate in FP32, etc. So hardware tries to give accuracy cheaply.&lt;br /&gt;
&lt;br /&gt;
=== 7.2. Hardware Accelerators and Tensor Cores ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Hardware accelerators&#039;&#039;&#039; (GPUs, TPUs, FPGAs, ASICs like GraphCore IPU, etc.) have specific support for certain precisions. Leveraging these can provide massive speedups:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;NVIDIA Tensor Cores:&#039;&#039;&#039; These started in Volta (V100) with support for 4x4 matrix multiply-adds on FP16 inputs with FP32 accumulation&amp;lt;ref name=&amp;quot;ref6&amp;quot;/&amp;gt;. Using Tensor Cores can give up to 8x throughput compared to using regular FP32 cores. But to use them, the data needs to be FP16 (or now BF16 or FP8 on newer GPUs) and aligned to certain dimensions (multiples of 8 etc. in matrix sizes, which frameworks handle by padding if needed). If an ONNX model is all FP32, by default it would not use Tensor Cores unless the runtime automatically converts some ops to mixed precision (which some do via a flag or environment variable enabling TF32 or FP16). The introduction of &#039;&#039;&#039;TensorFloat-32 (TF32)&#039;&#039;&#039; in Ampere GPUs is essentially a compromise: treat FP32 inputs as two 16-bit significand halves (10-bit mantissa actually) so that it can run on Tensor Cores, giving ~8x speed, while still outputting FP32. TF32 has about the precision of FP16 but range of FP32, and it was chosen because it typically doesn’t hurt convnet accuracy. ONNX Runtime or others on Ampere will use TF32 by default for conv and matmul unless disabled, giving a free speed boost at minimal accuracy cost. If exact FP32 is needed, users have to turn it off (sacrificing speed).&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;TPU (Google):&#039;&#039;&#039; TPUs have native support for BF16 matrix ops with FP32 accumulators. They were designed specifically for the mixed precision approach in training. For inference, TPUs also support int8 and other quantized ops (e.g., via Edge TPU for lower-power). If exporting ONNX to run on a TPU (via some bridge), one might get automatic BF16 usage. BF16 has the advantage of no loss in range, so TPUs can more straightforwardly run models in BF16 without modifications.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;FPGAs and ASICs:&#039;&#039;&#039; Some custom accelerators might use fixed-point or block floating-point arithmetic for efficiency. They often treat the network in quantized terms. The ONNX quantization specification (QuantizeLinear/DequantizeLinear ops) exists for representing quantized models (INT8 etc.), which is another domain beyond floating-point, but it overlaps as a precision issue. In those systems, accumulators for INT8 × INT8 multiplies are usually 32-bit (to avoid overflow for dot products up to certain lengths). The trade-off there is between integer vs float representation, which can give big speedups on CPUs/DSAs via SIMD instructions (many CPUs have vector int8 multiply-add instructions now for inference).&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Memory bandwidth on hardware:&#039;&#039;&#039; Some accelerators include special high-bandwidth memory or cache specifically for lower precision. For example, an accelerator might fetch two FP16 values in one 32-bit memory transaction – effectively doubling memory bandwidth for half precision. On some hardware, using FP16 can even reduce memory &#039;&#039;latency&#039;&#039; if multiple values can be packed.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Parallelism and Vectorization:&#039;&#039;&#039; Many hardware units process data in fixed vector widths (e.g., 256-bit registers). That could be 8 floats at once or 16 half-floats at once in the same 256 bits. If your code is in FP16, you might use the full vector width (16 values) whereas FP32 might only use half (8 values). This means FP16 can make better use of vector units. Similarly, matrix units often have fixed tile sizes, and smaller data types allow larger tiles or more tiles concurrently.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Energy efficiency:&#039;&#039;&#039; Lower precision often means less energy per operation, which on large deployments (data centers or battery-powered devices) is a significant advantage. This isn&#039;t directly performance, but it&#039;s an important trade: you might choose a slightly lower precision to save power even if accuracy drops a tiny bit.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Software overhead:&#039;&#039;&#039; Using mixed precision on hardware involves some overhead of casting and moving data between precisions. For instance, if you have to frequently cast FP16 to FP32 and back, that adds instructions and memory copies. A well-designed accelerator (like GPUs with unified memory for FP16/32 or special hardware casting) minimize this. Still, the runtime might insert extra operations (as we saw, ONNX Runtime CPU inserted many Cast nodes converting FP16 to FP32 and back&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;, which actually &#039;&#039;slowed down&#039;&#039; that FP16 model by 4x vs FP32 on a CPU, because the CPU had no native FP16). So ironically, on some hardware, using lower precision in ONNX might degrade performance if not supported (the CPU case). Always ensure the target hardware actually supports the low precision you plan to use; otherwise, you&#039;ll pay overhead for emulation.&lt;br /&gt;
&lt;br /&gt;
To illustrate trade-off: Suppose you have a model that achieves 99% accuracy in FP32 and drops to 98.5% in FP16. If FP16 runs 2x faster, many would accept the 0.5% accuracy drop for double throughput. If int8 runs 4x faster but accuracy drops to 95%, that might be too much drop for some applications, but acceptable for others that value speed. It&#039;s application-dependent.&lt;br /&gt;
&lt;br /&gt;
In critical applications, one might maintain higher precision accumulators or run certain validation steps in high precision to ensure nothing drifted. In research, sometimes a model is trained in mixed precision but validated in double to ensure any small numeric differences don’t influence the reported result (rare, but for scientific computing integration maybe).&lt;br /&gt;
&lt;br /&gt;
In the ONNX world, performance-vs-accuracy is often managed via &#039;&#039;&#039;Execution Providers&#039;&#039;&#039; (EPs) that target specific hardware. For example, the TensorRT EP in ONNX Runtime can take an ONNX model and run it using NVIDIA&#039;s high-performance inference library, which will use FP16 or INT8 if allowed, to maximize speed. The user can configure the EP with precision flags. If the accuracy drop is too high, one might restrict it to FP32. If it&#039;s fine, allow FP16. Tools exist to measure the accuracy difference (like calibrating int8 quantization).&lt;br /&gt;
&lt;br /&gt;
To conclude, the trade-offs revolve around picking the lowest precision that yields acceptable accuracy, and utilizing hardware features fully. ONNX being a high-level spec abstracts this, but it&#039;s ultimately up to the deployment to decide and sometimes up to the exporter to incorporate quantization. ONNX’s role is to be able to &#039;&#039;represent&#039;&#039; these choices (they have added quantized types, FP8 types, etc., to allow the graph to include those when needed).&lt;br /&gt;
&lt;br /&gt;
== Best Practices and Design Guidelines ==&lt;br /&gt;
&lt;br /&gt;
Given the above insights, here are some best practices and guidelines for dealing with numerical precision and accumulators in AI inference (and training) design:&lt;br /&gt;
&lt;br /&gt;
=== 8.1. Choosing an Accumulator Precision ===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Match precision to data length and range:&#039;&#039;&#039; If an operation accumulates a large number of terms, consider using a higher precision accumulator than the input data. For example, for summing &amp;gt;1000 values of type FP16, an FP32 accumulator is recommended to avoid large rounding error or overflow. If summing &amp;gt; million values of FP32, consider double (FP64) accumulation if high accuracy is needed (though this is rare in DL). &#039;&#039;Rule of thumb:&#039;&#039; if $N \cdot \varepsilon$ is not tiny (say &amp;gt;1e-3), you may benefit from higher precision. For FP16 ($\varepsilon \approx 1e-3$), that threshold N is a few hundred. For FP32 ($\varepsilon \approx 1e-7$), N would be in the millions before 1e-3 error.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Use higher precision in critical layers:&#039;&#039;&#039; Identify if certain layers of the model are more sensitive to precision. For instance, the output layer or a layer that feeds into a very sharp decision boundary might require more precision. If a small change there could flip the prediction, you don&#039;t want excessive numerical error. Using FP32 for logits and softmax is a common practice to maintain classification confidence stability.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Follow hardware defaults, but override if needed:&#039;&#039;&#039; Many hardware accelerators have sensible defaults (like accumulate in FP32 for tensor ops). It&#039;s usually best to use those defaults as they were chosen to balance accuracy and speed. Only override if you have evidence of a problem. For example, if using ONNX Runtime on CPU, you discovered it upcasts to float for you – great. But if using some custom accelerator that does pure FP16, you might manually insert casts in the ONNX graph around certain ops to force higher precision on those ops.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Maintain precision for gradient accumulation (training):&#039;&#039;&#039; This is more training-oriented, but worth noting: when summing gradients over batches or updating weights, use higher precision (FP32 or even 64) for those accumulations. This ensures training doesn&#039;t diverge due to numeric error. Many frameworks keep a &amp;quot;master copy&amp;quot; of weights in FP32 when training in FP16&amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;. For inference, this translates to: any running statistics or counters in the model (like if you had a running average inside the model) should probably be in higher precision to avoid drift.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Be mindful of intermediate cast costs:&#039;&#039;&#039; If you do decide to cast to higher precision for an operation in ONNX, remember it incurs memory copies and compute overhead. Make sure it&#039;s worth it. For example, if you cast a huge tensor to double, that could slow things drastically. Usually FP16-&amp;gt;FP32 is fine (hardware handles it well), but FP32-&amp;gt;FP64 might be expensive on GPUs.&lt;br /&gt;
&lt;br /&gt;
=== 8.2. Safe Reduction Lengths and Heuristics ===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Chunk long reductions:&#039;&#039;&#039; If you must do a very long sum in low precision (due to memory constraints or hardware), chunk the sum into parts, sum each to a higher precision local accumulator, then sum the partial sums. This is essentially pairwise summation. Many libraries use a fixed chunk size heuristic (e.g., sum in blocks of 256 in registers, then add those up). This bounds the error growth. If implementing manually, you can choose a chunk size such that chunk_length &#039;&#039; ε is small (like 256 &#039;&#039; 1e-3 = 0.256 for FP16, hmm maybe choose 100).&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Sort by magnitude if possible:&#039;&#039;&#039; In some cases (not typical in DL, but possible in post-processing), if you have numbers varying by orders of magnitude, summing from smallest to largest improves accuracy. It prevents small ones from being swallowed by big ones. This is more relevant if you output a sum of mixed-scale values (like some algorithm mixing big and small contributions). It’s not always feasible in real-time inference to sort, but keep in mind.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Use compensation for critical sums:&#039;&#039;&#039; If you find a particular sum or difference in your inference computation is crucial (like computing a small residual from two close numbers), consider using Kahan summation or another compensated scheme just for that part. For example, if you had an ONNX custom function that needs to sum a lot of almost cancelling terms, you might implement it with compensation internally.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Heuristics for FP8/low-bit:&#039;&#039;&#039; When using extremely low precision like FP8, adopt the recommended practices: use scaling (don&#039;t feed raw data into FP8, normalize it first), and perhaps only use FP8 on portions of the network that are empirically robust. The industry is still establishing heuristics, but one might be: use FP8 for matrix multiplications, but accumulate in FP16 or FP32; keep normalization in FP16; use FP16 for first and last layer maybe. This is evolving, so keep an eye on literature.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Keep an eye on sum length in softmax and normalization:&#039;&#039;&#039; If you have extremely large class counts or group sizes (e.g., a softmax over 1e6 items, or layer norm over tens of thousands of features), realize this is a giant reduction. You may want to ensure that reduction is done in higher precision. ONNX Softmax by default will likely not handle a million-class case in FP16 well (the sum would overflow since 1e6 &amp;gt; 65504). So either do that on CPU in FP32 or break it down (could do hierarchical softmax, etc.). This is a corner case, but relevant for huge vocab language models. In practice, vocab ~50k is fine as we said.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Test numeric differences:&#039;&#039;&#039; A practical tip: after quantizing or reducing precision, run some test inputs through both the high-precision model and low-precision model. Compare outputs (and maybe some intermediate signals if possible). If differences are consistently small relative to the application tolerance, you&#039;re good. If you see occasional large differences or instabilities (like outputs become NaN or Inf in low precision), that&#039;s a red flag. Use that testing to guide where you might need to increase precision or change algorithms.&lt;br /&gt;
&lt;br /&gt;
=== 8.3. When Higher Precision Is Required ===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Ill-conditioned calculations:&#039;&#039;&#039; If your model or pipeline performs any operation known to be ill-conditioned (matrix inversion, subtraction of nearly equal quantities, computing a tiny difference of large numbers), you likely need higher precision for that part. For example, if your ONNX model includes a custom op to solve a linear system, use double precision in that op if possible (or at least float with iterative refinement). In ML, this might occur in something like a physics-informed neural network or a differentiable simulation within the model.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Strict accuracy requirements:&#039;&#039;&#039; Some applications (medical, aerospace, finance) might have strict tolerances. If an inference model’s output needs to be correct to 5 decimal places, you probably cannot use FP16, which has only 3-4 decimal digits precision. FP32 might be barely enough (~7 digits). FP64 might be warranted to guarantee that level of accuracy in worst case. Most deep learning applications (like image classification) don&#039;t demand that level of numeric precision in outputs – as long as the top class is correct, it doesn&#039;t matter if its probability was 0.901 vs 0.899. But if you&#039;re doing something like summing probabilities to make a risk estimate or doing a cumulative sum over many predictions, be careful to accumulate precisely.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Large sums or products in post-processing:&#039;&#039;&#039; Sometimes the neural net is fine, but the &#039;&#039;post-processing&#039;&#039; of results might need precision. E.g., summing up probabilities of many events, or computing confidence intervals, etc. Do those calculations in double if needed outside the model.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Preventing drift in long sequences:&#039;&#039;&#039; For sequential models (RNNs processing thousands of steps), rounding error can accumulate over steps. If each step has a small error, after 10000 steps it could add up. In such cases, using a higher precision for the state might reduce drift. Some practitioners use FP64 for the hidden state accumulation in very long sequence processing to maintain numerical stability (though this is not common due to speed loss). Alternatively, periodically renormalize or reset small errors.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Simulations or differential equations:&#039;&#039;&#039; If your ONNX model involves simulating something (like a differential equation solver inside a model, or an ODE/RNN hybrid), those often need double precision to be stable for long durations. Recognize those situations and allocate precision accordingly.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Verification and debugging:&#039;&#039;&#039; Use higher precision as a tool to verify correctness. For example, run your model in FP64 (if possible) and compare to FP32 to see if there are differences. If not, FP32 is fine. If yes, you might find a particular calculation losing too much info in FP32, implying maybe you should keep it in FP64 in production as well. This is rare in inference but can be part of rigorous validation.&lt;br /&gt;
&lt;br /&gt;
In ONNX, using higher precision may involve casting to &amp;lt;code&amp;gt;tensor(double)&amp;lt;/code&amp;gt; for certain ops. Not all runtimes support double on GPU though (some do via emulation or just slower path). ONNX CPU does support double for general ops. So be mindful of the target.&lt;br /&gt;
&lt;br /&gt;
Finally, &#039;&#039;&#039;document your precision choices&#039;&#039;&#039;. If you deliver an ONNX model that uses mixed types (float16 inputs, float32 accumulations, int8 weights, etc.), ensure that is clear to whoever maintains or uses it. Precision issues can be sneaky, so clarity helps.&lt;br /&gt;
&lt;br /&gt;
== Limitations and Open Problems ==&lt;br /&gt;
&lt;br /&gt;
Despite advances in handling numerical precision, there remain limitations and open challenges in this domain:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Lack of Precision Specification in ONNX:&#039;&#039;&#039; As noted, ONNX currently has no way to formally specify that an op &#039;&#039;should&#039;&#039; use a higher precision internally. This can lead to nondeterministic differences across platforms&amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;. While proposals exist to add attributes for this&amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;, it is not yet standard. Thus an ONNX model might behave slightly differently on two compliant backends, and there&#039;s no built-in mechanism to ensure identical results. This is a limitation for applications requiring cross-platform consistency. Solving it may involve extending ONNX (e.g., an attribute like &amp;lt;code&amp;gt;accumulatePrecision=&amp;quot;FLOAT32&amp;quot;&amp;lt;/code&amp;gt; for MatMul) or tightening the spec, but those come with backward compatibility and implementation burden concerns.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Reproducibility and Determinism:&#039;&#039;&#039; Even with the same precision, parallel computation can introduce nondeterminism (due to different summation orders). Ensuring bit-identical results across runs (important for some sensitive domains) is non-trivial. Frameworks often provide &amp;quot;deterministic mode&amp;quot; which might use fixed ordering at cost of speed. In ONNX, there&#039;s no global flag for this; it&#039;s up to the runtime. More research could be done on algorithms that produce bit-wise identical results independent of parallel execution order (perhaps using techniques like pairwise summation or consistent partitioning). Stochastic rounding is another area: sometimes using probabilistic rounding can reduce bias in accumulated error, but it introduces randomness – reconciling that with reproducibility is tricky.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Ultra-low Precision (FP8 and below):&#039;&#039;&#039; While FP8 is being explored, going to 4-bit floats or mixed schemes is an open challenge. The OpenAI community is investigating 4-bit (quantization-wise) for LLMs with some success, but a true 4-bit float (like E3M1) would be extremely limited. Perhaps hybrid schemes (like block floating point, log scales, etc.) will come. ONNX already has some support (they mention 4-bit in documentation placeholders&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;), but it&#039;s nascent. Research into training techniques, calibration, and new number formats (like &#039;&#039;&#039;posit numbers&#039;&#039;&#039; or tapering precision floats) is ongoing. If a new format proves useful, ONNX will need to adapt to represent it.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Quantization Noise and Model Robustness:&#039;&#039;&#039; Reducing precision can sometimes expose models to issues like adversarial vulnerability or fragility – e.g., a slight perturbation might flip a decision if the margin was small and precision loss nudged it. Ensuring model robustness under quantization is an open problem. It might require re-training the model with quantization in the loop (quantization-aware training) or designing models that have large &amp;quot;margin&amp;quot; between classes to tolerate quantization noise.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Dynamic range during training vs inference:&#039;&#039;&#039; In training, activations can have wider distributions (especially early in training). Models often learn to constrain their ranges (via activation functions saturating or normalization), which makes inference more stable in lower precision. But if one tries to train in FP8 from scratch, it&#039;s very hard. This gap between what&#039;s possible in a post-trained inference model vs training is an open area – how to train directly in low precision without significant accuracy loss. Solving that could simplify workflows (no need for high-precision training then quantize; just train low-prec end to end). Techniques like loss scaling, gradient clipping, etc., are partial solutions. The development of optimizers that are resilient to low precision is ongoing.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Hardware variability:&#039;&#039;&#039; As new hardware emerges (with new numeric capabilities or quirks), ensuring that ONNX models run consistently is an ongoing task. For instance, one device might implement a fused op that reduces rounding error, another does it separately. Even something like convolution might use a different algorithm (FFT-based vs direct) which has different numeric properties. Open problem: could ONNX allow specifying numeric tolerances per op or preferred algorithms? That may be overkill for most, but for some HPC ML use cases, maybe desired.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Testing and validation of numerics:&#039;&#039;&#039; There isn&#039;t a standardized set of &amp;quot;numeric compliance&amp;quot; tests for ONNX runtimes beyond simple correctness. Perhaps in the future, test suites might include challenging numeric scenarios (large sums, nearly singular matrices) to ensure a runtime handles them gracefully (maybe by switching precision). Right now, it&#039;s mostly on the user to test their model under expected conditions.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Combining quantization with floating point:&#039;&#039;&#039; Many production models use a mix (e.g., int8 for some layers, FP16 for others). ONNX can represent this (with QuantizeLinear ops or MixedPrecision through casting), but tooling to automatically find the optimal mix is still developing. It&#039;s somewhat trial-and-error or using heuristics. An open question is how to automate precision assignment: given a model and a target hardware, how to automatically choose which layers can be int8, which should be FP16, etc., to meet an accuracy target. This crosses into NAS (neural architecture search) territory or automated quantization. &lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Numerical debugging tools:&#039;&#039;&#039; When a model goes wrong due to precision, it&#039;s often hard to pinpoint where. Better tooling to analyze numerical error layer by layer could be useful. For instance, one could run a model in double and float and diff the intermediate results to see where the largest divergence happens. Doing this systematically for big nets is an open tooling challenge. Some frameworks have hooks for this, but not standardized.&lt;br /&gt;
&lt;br /&gt;
In conclusion, while we&#039;ve come far with using lower precision in AI without sacrificing much accuracy, the landscape is always evolving. The key open problem is how to push precision lower (for efficiency gains) without crossing the threshold where accuracy falls off a cliff. Each new generation of hardware and techniques (like FP8, novel quantization schemes) extends that boundary, but also brings new questions on how to manage the numerical errors. ONNX as an interchange will need to keep pace, possibly by adding more expressive power for these numeric considerations.&lt;br /&gt;
&lt;br /&gt;
== See Also ==&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;IEEE 754 Floating-Point&#039;&#039;&#039; – The standard defining binary16, binary32, binary64 formats and rounding behavior.&lt;br /&gt;
* &#039;&#039;&#039;Floating-Point Arithmetic&#039;&#039;&#039; – General topic on how floating-point math works, common issues (overflow, underflow, cancellation).&lt;br /&gt;
* &#039;&#039;&#039;Kahan Summation Algorithm&#039;&#039;&#039; – Algorithm for compensated summation to reduce numerical error&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;.&lt;br /&gt;
* &#039;&#039;&#039;BFloat16&#039;&#039;&#039; – A short-format float with 8-bit exponent, used in TPUs and some GPUs for training networks.&lt;br /&gt;
* &#039;&#039;&#039;Mixed Precision Training&#039;&#039;&#039; – Technique of using FP16 (or BF16) along with FP32 accumulators to accelerate training&amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;.&lt;br /&gt;
* &#039;&#039;&#039;Quantization in Deep Learning&#039;&#039;&#039; – Converting networks to lower bit integer arithmetic for inference (INT8 quantization and calibration techniques).&lt;br /&gt;
* &#039;&#039;&#039;ONNX Operator Specification&#039;&#039;&#039; – Official docs for ONNX ops, which describe their mathematical function but not the required numeric precision (a point of potential extension in future).&lt;br /&gt;
* &#039;&#039;&#039;CUDA Tensor Cores &amp;amp; TF32&#039;&#039;&#039; – NVIDIA documentation on how tensor cores perform mixed precision and the TF32 format for accelerating FP32.&lt;br /&gt;
* &#039;&#039;&#039;Pairwise Summation&#039;&#039;&#039; – Summation algorithm with logN error growth&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;, used in many numerical libraries to improve accuracy of reductions.&lt;br /&gt;
* &#039;&#039;&#039;Softmax Stability&#039;&#039;&#039; – Articles on numerically stable softmax and log-sum-exp implementations to avoid overflow.&lt;br /&gt;
* &#039;&#039;&#039;Performance Tuning for Low Precision&#039;&#039;&#039; – Guides (e.g., NVIDIA or Intel whitepapers) on using low precision and the trade-offs involved in various hardware.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;references&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref1&amp;quot;&amp;gt;Accelerate and simplify Scikit-learn model inference with ONNX Runtime - Microsoft Open Source Blog — opensource.microsoft.com — https://opensource.microsoft.com/blog/2020/12/17/accelerate-simplify-scikit-learn-model-inference-onnx-runtime&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref2&amp;quot;&amp;gt;Floating Point Precision: Understanding FP64, FP32, and FP16 in Large Language Models - DEV Community — dev.to — https://dev.to/lukehinds/floating-point-precision-understanding-fp64-fp32-and-fp16-in-large-language-models-3gk6&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref3&amp;quot;&amp;gt;Floating Point Precision and Its Limitations | by Umair Akbar | Medium — akbu.medium.com — https://akbu.medium.com/floating-point-precision-and-its-limitations-cfb7247d7789&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref4&amp;quot;&amp;gt;Kahan summation algorithm - Wikipedia — en.wikipedia.org — https://en.wikipedia.org/wiki/Kahan_summation_algorithm&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref5&amp;quot;&amp;gt;Pairwise summation - Wikipedia — en.wikipedia.org — https://en.wikipedia.org/wiki/Pairwise_summation&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref6&amp;quot;&amp;gt;Numerical behavior of NVIDIA tensor cores - PMC — pmc.ncbi.nlm.nih.gov — https://pmc.ncbi.nlm.nih.gov/articles/PMC7959640/&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref7&amp;quot;&amp;gt;Mixed precision and performance-accuracy tuning (neuron-cc) — AWS Neuron Documentation — awsdocs-neuron.readthedocs-hosted.com — https://awsdocs-neuron.readthedocs-hosted.com/en/latest/about-neuron/appnotes/neuron-cc/mixed-precision.html&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref8&amp;quot;&amp;gt;Create Float16 and Mixed Precision Models - ONNX Runtime — onnxruntime.ai — https://onnxruntime.ai/docs/performance/model-optimizations/float16.html&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref9&amp;quot;&amp;gt;[Performance] ONNX FP16 model is having performance bottle neck when compared to FP32 variant · Issue #25824 · microsoft/onnxruntime · GitHub — github.com — https://github.com/microsoft/onnxruntime/issues/25824&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref10&amp;quot;&amp;gt;Float stored in 8 bits - ONNX 1.21.0 documentation — onnx.ai — https://onnx.ai/onnx/technical/float8.html&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref11&amp;quot;&amp;gt;[2209.05433] FP8 Formats for Deep Learning — arxiv.org — https://arxiv.org/abs/2209.05433&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref12&amp;quot;&amp;gt;Should MatMul op allow specification of accumulation precision? · Issue #7072 · onnx/onnx · GitHub — github.com — https://github.com/onnx/onnx/issues/7072&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref13&amp;quot;&amp;gt;Quantize ONNX models | onnxruntime — onnxruntime.ai — https://onnxruntime.ai/docs/performance/model-optimizations/quantization.html&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref14&amp;quot;&amp;gt;Keep Attention Softmax FP32 during FP16/ZeRO Training · Issue #1485 · hpcaitech/ColossalAI · GitHub — github.com — https://github.com/hpcaitech/ColossalAI/issues/1485&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;/references&amp;gt;&lt;/div&gt;</summary>
		<author><name>Timo.stripf</name></author>
	</entry>
	<entry>
		<id>https://www.emmtrix.com/w139/index.php?title=Numerical_Precision_in_ONNX_and_AI_Inference&amp;diff=2893</id>
		<title>Numerical Precision in ONNX and AI Inference</title>
		<link rel="alternate" type="text/html" href="https://www.emmtrix.com/w139/index.php?title=Numerical_Precision_in_ONNX_and_AI_Inference&amp;diff=2893"/>
		<updated>2026-01-30T15:20:22Z</updated>

		<summary type="html">&lt;p&gt;Timo.stripf: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&#039;&#039;&#039;Open Neural Network Exchange (ONNX)&#039;&#039;&#039; is an open standard format for representing machine learning models and neural network computations across different frameworks and hardware&amp;lt;ref name=&amp;quot;ref1&amp;quot;/&amp;gt;. As models are exported and deployed via ONNX, the &#039;&#039;&#039;numerical precision&#039;&#039;&#039; of computations becomes critical. Deep learning inference involves a variety of floating-point operations, and small numerical discrepancies can accumulate and affect model accuracy or reproducibility. This article provides an in-depth overview of floating-point precision issues in numerical computing, with a focus on how they manifest in AI inference and the ONNX ecosystem. We cover floating-point formats and rounding behavior, accumulation strategies for summations, the use of low-precision formats (FP16, BFloat16, FP8) in modern AI, and how ONNX defines (or leaves unspecified) the precision of accumulators in operations. Strategies for maintaining numerical stability in deep neural networks are discussed, along with the trade-offs between computational performance and arithmetic accuracy. The goal is to give engineers and researchers a comprehensive, encyclopedic reference on numerical precision in ONNX-based AI inference systems.&lt;br /&gt;
&lt;br /&gt;
== Floating-Point Precision in Numerical Computing ==&lt;br /&gt;
&lt;br /&gt;
=== 2.1. IEEE 754 Floating-Point Formats ===&lt;br /&gt;
&lt;br /&gt;
Modern computers represent real numbers using the IEEE 754 floating-point standard, which defines formats like 32-bit &#039;&#039;single precision&#039;&#039; (FP32) and 64-bit &#039;&#039;double precision&#039;&#039; (FP64). In IEEE 754 binary formats, a floating-point number is composed of three fields: 1 sign bit, several exponent bits (with a bias), and several fraction (mantissa) bits. For example, FP32 consists of 1 sign bit, 8 exponent bits, and 23 fraction bits (24 bits of significand precision including the implicit leading 1)&amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;. This provides roughly 7 decimal digits of precision and an exponent range allowing values on the order of $10^{\pm38}$. FP64 allocates 1 sign bit, 11 exponent bits, and 52 fraction bits (53-bit precision), yielding about 15–16 decimal digits of precision and a vastly larger range (~$10^{\pm308}$)&amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;. These larger formats (especially FP64) are the &amp;quot;gold standard&amp;quot; for numerical accuracy in scientific computing, as they reduce round-off error in complex calculations&amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;. However, the increased precision comes at the cost of doubled storage and typically reduced computational throughput (e.g. many GPUs execute FP64 instructions at a much lower rate than FP32&amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
In addition to single and double precision, the IEEE 754-2008 standard introduced a 16-bit &#039;&#039;half precision&#039;&#039; format (FP16, or binary16). FP16 has 1 sign bit, 5 exponent bits, and 10 fraction bits (11-bit significand including the implicit 1)&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. This format covers a much smaller dynamic range (approximately $6.1\times10^{-5}$ to $6.5\times10^4$ for normalized values&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;) and offers only around 3 decimal digits of precision. Half precision was originally used in graphics, but it has gained popularity in machine learning for its speed and memory advantages (discussed later in Section 4). Another format relevant to AI is &#039;&#039;&#039;BFloat16&#039;&#039;&#039; (Brain Float 16), a 16-bit float with &#039;&#039;&#039;8 exponent bits and 7 fraction bits&#039;&#039;&#039;&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. BFloat16 sacrifices precision (only ~7 fraction bits ≈ 2–3 decimal digits) in order to have the &#039;&#039;&#039;same exponent range as FP32&#039;&#039;&#039;&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. This means BFloat16 can represent very large or very small numbers (∼$10^{\pm38}$, similar range as FP32) but with much less precision between representable values&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. BFloat16 was introduced for deep learning by Google (for TPUs) to allow reduced precision &#039;&#039;training&#039;&#039; without frequent overflow/underflow, relying on the wide range and accepting the coarse precision.&lt;br /&gt;
&lt;br /&gt;
Each floating-point format has a &#039;&#039;&#039;unit roundoff&#039;&#039;&#039; or machine precision, denoted $\epsilon$. This is effectively the spacing between 1.0 and the next representable number. For FP32, $\epsilon \approx 2^{-23} \approx 1.19\times10^{-7}$, corresponding to about 7–8 decimal digits of precision&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. In FP64, $\epsilon = 2^{-52} \approx 2.22\times10^{-16}$ (15–16 decimal digits). Half precision is much less precise: FP16 has $\epsilon = 2^{-10} \approx 9.76\times10^{-4}$&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;, and BFloat16 has $\epsilon = 2^{-7} = 7.8125\times10^{-3}$&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. These values quantify how finely the continuum of real numbers is quantized in each format.&lt;br /&gt;
&lt;br /&gt;
=== 2.2. Machine Epsilon and Rounding Error ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Machine epsilon ($\varepsilon$)&#039;&#039;&#039; is formally defined as the smallest positive number such that $1.0 + \varepsilon &amp;gt; 1.0$ in the floating-point format&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. It effectively measures the &#039;&#039;&#039;precision&#039;&#039;&#039; of the representation. In FP32, $\varepsilon = 2^{-23} \approx 1.19\times10^{-7}$, meaning any result is rounded to about 7 decimal digits. FP16’s $\varepsilon = 2^{-10} \approx 9.77\times10^{-4}$, over 1000× larger (so only ~3 decimal digits of precision), and BFloat16’s $\varepsilon = 2^{-7} \approx 7.8\times10^{-3}$, reflecting its mere ~8-bit precision&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. Rounding in IEEE 754 by default uses &#039;&#039;&#039;round-to-nearest (ties to even)&#039;&#039;&#039;, which ensures the rounding error for any single operation is at most half a unit in the last place (0.5 ULP). We can model a floating-point addition as: &lt;br /&gt;
&lt;br /&gt;
$$\text{fl}(a+b) = (a+b)\,(1 + \delta),$$ &lt;br /&gt;
&lt;br /&gt;
where $|\delta| \le \varepsilon$ for that format&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. Here $\varepsilon/2$ is the maximum &#039;&#039;&#039;relative error&#039;&#039;&#039; in one rounding (also called unit roundoff). For example, when adding two FP32 numbers, the result is accurate to within about $10^{-7}$ of the true sum (relative), whereas in BF16, the result can deviate by up to $\sim7.8\times10^{-3}$ (nearly 0.78% relative error per operation)&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. Thus, &#039;&#039;&#039;each arithmetic operation incurs a small rounding error&#039;&#039;&#039; (on the order of machine epsilon) that can propagate or accumulate through a sequence of calculations.&lt;br /&gt;
&lt;br /&gt;
Because floating-point representations have finite exponent range, we must also consider &#039;&#039;&#039;overflow&#039;&#039;&#039; and &#039;&#039;&#039;underflow&#039;&#039;&#039;. If a result’s magnitude exceeds the maximum representable value, it overflows to infinity; if it falls below the minimum normal (or minimum subnormal) value, it underflows to zero (or a denormalized tiny value). FP32 and BF16 share an exponent size (8 bits), so their range for normal numbers is roughly $[10^{-38}, 10^{38}]$ (approx $~\!3.4\times10^{38}$ max)&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. FP16’s 5-bit exponent allows max $\sim 6.55\times10^4$ and min normal $\sim 6.10\times10^{-5}$&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. In practice, &#039;&#039;&#039;underflow/overflow&#039;&#039;&#039; can occur in low-precision arithmetic if values are not appropriately scaled. For instance, summing a large number of moderately sized terms in FP16 could overflow where FP32 would not. As an example, summing on the order of $10^4$ values of magnitude ~$10^1$ (say, adding 10,000 terms around 10 each) would produce a total $\sim10^5$, which exceeds FP16’s max finite value ~$6.5\times10^4$ and would overflow to $+\infty$ in half precision&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. Underflow is usually a concern when subtracting nearly equal numbers (catastrophic cancellation yielding a tiny difference that might flush to zero in low precision).&lt;br /&gt;
&lt;br /&gt;
=== 2.3. Accumulation Error in Reductions ===&lt;br /&gt;
&lt;br /&gt;
Floating-point addition is &#039;&#039;&#039;not associative&#039;&#039;&#039;: $(a+b)+c$ can differ from $a+(b+c)$ due to rounding, and thus the order in which we sum a list of numbers can change the result slightly. When summing a large sequence of $N$ numbers $x_1, x_2, ..., x_N$, the &#039;&#039;&#039;accumulated rounding error&#039;&#039;&#039; can be significant. In the &#039;&#039;worst case&#039;&#039; (e.g., adding many positive and negative terms that cause maximal cancellation or always rounding one way), the error can grow on the order of $O(N \varepsilon)$ relative to the exact sum&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;. Simply summing $N$ numbers in sequence (the &#039;&#039;&#039;naive linear summation&#039;&#039;&#039;) has a worst-case error bound proportional to $N\,\varepsilon$&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. This worst-case occurs for adversarial arrangements of values; in more typical scenarios with rounding errors of random signs, one can model the error as a random walk, yielding an expected root-mean-square (RMS) error growth on the order of $O(\sqrt{N}\,\varepsilon)$&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;. Still, with very large $N$, even $\sqrt{N}$ times $\varepsilon$ can become large enough to matter.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Cancellation&#039;&#039;&#039; is a particular problem: if we add two nearly equal numbers of opposite sign, most leading digits cancel and the result loses significant bits of precision. For example, in single precision, subtracting two numbers that agree in the first 6 decimal digits will yield a result with only ~1 digit of accuracy left. Catastrophic cancellation can dramatically increase relative error. Summation is especially sensitive to cancellation if large positive and negative terms are summed naively. The &#039;&#039;&#039;condition number&#039;&#039;&#039; of a summation (the ratio of the sum of absolute values $\sum|x_i|$ to the absolute value of the true sum $|\sum x_i|$) indicates how sensitive the result is to perturbations&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;. If the numbers have mixed signs and nearly cancel out (making the condition number large), even an optimal summation order will have a large relative error. For instance, summing [1.0, $10^{100}$, 1.0, $-10^{100}$] in double precision has an exact sum of 2.0, but naive summation may yield 0.0 due to cancellation; an improved summation algorithm yields the correct 2.0&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
In practical terms, when summing a long list of floating-point numbers (such as accumulating a dot product or reducing a tensor), the &#039;&#039;&#039;accumulation error&#039;&#039;&#039; can become noticeable if no precautions are taken. Techniques to mitigate this error include carefully choosing the summation order or using higher precision for intermediate sums. We will explore these strategies in the next section.&lt;br /&gt;
&lt;br /&gt;
== Accumulators and Reduction Strategies ==&lt;br /&gt;
&lt;br /&gt;
When performing reductions (sums, dot products, etc.), the method of accumulation can greatly affect the accuracy of the result. Below, we discuss various strategies:&lt;br /&gt;
&lt;br /&gt;
=== 3.1. Naive Linear Accumulation ===&lt;br /&gt;
&lt;br /&gt;
The simplest accumulation strategy is to use a single accumulator and add each element sequentially. In C-like pseudocode:&lt;br /&gt;
&lt;br /&gt;
This &#039;&#039;&#039;naive linear summation&#039;&#039;&#039; adds values one by one to the running total. While straightforward and fast, it is prone to the rounding error accumulation described earlier. The partial sum &amp;lt;code&amp;gt;sum&amp;lt;/code&amp;gt; continually grows (in absolute value) as terms are added, so new small terms may fall outside its precision. For example, if &amp;lt;code&amp;gt;sum&amp;lt;/code&amp;gt; is large and positive, adding a comparatively tiny positive number may produce no change because the small term&#039;s significant digits vanish in the presence of the large sum (the increment is below the rounding threshold of &amp;lt;code&amp;gt;sum&amp;lt;/code&amp;gt;). Conversely, adding a small number of opposite sign can suffer from cancellation. The order of addition matters: summing from smallest magnitude to largest (ascending order) tends to yield a more accurate result than summing in descending order, because adding small terms first preserves their contributions before the sum becomes very large. The naive loop above implicitly sums in the given index order; if the data is unsorted, this order is essentially arbitrary.&lt;br /&gt;
&lt;br /&gt;
The performance of linear accumulation is excellent (minimal overhead), and for many applications the resulting error is within acceptable limits. However, for &#039;&#039;&#039;very large N&#039;&#039;&#039; or ill-conditioned sums, the inaccuracy can become a problem. In a reduction involving millions of elements, a single-precision sum could lose several digits of accuracy. In deep learning, this could translate to small discrepancies in layer outputs or aggregated gradients if done naively in low precision.&lt;br /&gt;
&lt;br /&gt;
=== 3.2. Pairwise and Tree-Based Reductions ===&lt;br /&gt;
&lt;br /&gt;
A better approach to summation is &#039;&#039;&#039;pairwise summation&#039;&#039;&#039;, which sums numbers in a binary tree structure rather than linearly. The idea is to recursively split the array into halves, sum each half, then add the two partial sums. This can be implemented as:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;!--&lt;br /&gt;
&amp;lt;syntaxhighlight&amp;gt;&lt;br /&gt;
function pairwise_sum(x[1..n]):&lt;br /&gt;
    if n == 0:&lt;br /&gt;
        return 0&lt;br /&gt;
    if n == 1:&lt;br /&gt;
        return x[1]&lt;br /&gt;
    m = floor(n/2)&lt;br /&gt;
    left_sum  = pairwise_sum(x[1..m])&lt;br /&gt;
    right_sum = pairwise_sum(x[m+1..n])&lt;br /&gt;
    return left_sum + right_sum&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
--&amp;gt;&lt;br /&gt;
&lt;br /&gt;
By always adding numbers of similar magnitudes first (small subsets are reduced to subtotals, then those are added, etc.), pairwise summation avoids the situation of a very large running total absorbing small additions until the final steps. The error of pairwise summation grows logarithmically with $N$ in the worst case (roughly $O(\varepsilon \log_2 N)$)&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;, instead of linearly. Intuitively, at each level of the tree, rounding error is constrained, and there are about $\log_2 N$ levels. In fact, the worst-case relative error bound for pairwise summation (base case of 1 element) is:&lt;br /&gt;
&lt;br /&gt;
\[ |E_N| \le \frac{\varepsilon \log_2 N}{1 - \varepsilon \log_2 N} \sum_{i=1}^N |x_i|, \]&lt;br /&gt;
&lt;br /&gt;
which for practical $N$ simplifies to on the order of $\varepsilon \log_2 N$ times the condition number of the sum&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;. In typical random-error scenarios, the error behaves even better (on the order of $\sqrt{\log N}$)&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;. The key is that &#039;&#039;&#039;pairwise (tree) reduction balances the accumulations&#039;&#039;&#039;, preventing one sum from overwhelming the others too early.&lt;br /&gt;
&lt;br /&gt;
From a performance standpoint, pairwise summation has the same $O(N)$ total additions as linear summation, just a different order. It is also highly amenable to parallelization: independent partial sums can be computed in parallel at each level of the tree. Many numerical libraries and BLAS implementations use a form of blocked or pairwise summation in dot products for improved accuracy at negligible performance cost. For example, NumPy and Julia perform partial pairwise summation by default for better numerical behavior&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=== 3.3. Compensated Summation (Kahan, Neumaier) ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Compensated summation&#039;&#039;&#039; is an alternative technique to improve summation accuracy by tracking the small “lost” quantities. The most famous algorithm is the &#039;&#039;&#039;Kahan summation algorithm&#039;&#039;&#039; (also called Kahan–Babuška)&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. Kahan summation uses an extra variable to accumulate the round-off error. In pseudocode:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;c&amp;quot;&amp;gt;&lt;br /&gt;
double sum = 0.0;&lt;br /&gt;
double c = 0.0;    // compensation for lost low-order bits&lt;br /&gt;
for (i = 1; i &amp;lt;= N; ++i) {&lt;br /&gt;
    double y = x[i] - c;    // recover low-order bits by subtracting compensation&lt;br /&gt;
    double t = sum + y;     // perform the addition&lt;br /&gt;
    c = (t - sum) - y;      // compute new compensation (the error in t)&lt;br /&gt;
    sum = t;&lt;br /&gt;
}&lt;br /&gt;
return sum;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Here &amp;lt;code&amp;gt;c&amp;lt;/code&amp;gt; accumulates the tiny leftovers that would otherwise be dropped&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. Each iteration essentially subtracts the last error from the new addend (&amp;lt;code&amp;gt;y = x[i] - c&amp;lt;/code&amp;gt;) so that &amp;lt;code&amp;gt;t = sum + y&amp;lt;/code&amp;gt; includes the small bits from the previous operation. The new error is then stored in &amp;lt;code&amp;gt;c&amp;lt;/code&amp;gt; for the next iteration. The result is that the final &amp;lt;code&amp;gt;sum&amp;lt;/code&amp;gt; is as if computed with higher precision than the machine’s: Kahan’s algorithm can &#039;&#039;&#039;greatly reduce the error&#039;&#039;&#039; in summing a sequence of floats. In fact, with exact arithmetic for the compensation, Kahan summation yields a result accurate to within 1 or 2 ULP of the true sum, regardless of N (assuming the sequence of partial sums doesn’t overflow)&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. More precisely, Kahan summation achieves a &#039;&#039;&#039;worst-case error independent of N&#039;&#039;&#039; (error bound depends only on machine precision, not the number of terms)&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. In practice, using a double accumulator for summing single-precision numbers via Kahan can give near double-precision accuracy.&lt;br /&gt;
&lt;br /&gt;
An important subtlety is that the compensation variable &amp;lt;code&amp;gt;c&amp;lt;/code&amp;gt; should be kept in &#039;&#039;full precision&#039;&#039;. In the implementation above, we used &amp;lt;code&amp;gt;double&amp;lt;/code&amp;gt; for both sum and c. If summing FP32 values, one might use &amp;lt;code&amp;gt;float sum&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;float c&amp;lt;/code&amp;gt; (both 32-bit), which still improves accuracy, but using a higher precision for the accumulator and compensation (i.e. double) yields even better results. Many implementations of Kahan summation use the same type for sum and c, but it&#039;s assumed to be higher precision than the inputs when possible.&lt;br /&gt;
&lt;br /&gt;
Kahan’s algorithm carries a performance cost: each iteration does five arithmetic operations (two adds, one subtract, etc.) and has a data dependency, which can hinder parallelism or pipelining&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. Thus, it runs slower than naive summation (roughly 2–3× slower or more, depending on CPU pipeline). In loops where summation is a bottleneck, this may be significant. Nonetheless, for modest N or where maximum accuracy is required, compensated summation is extremely useful. In fact, Python’s &amp;lt;code&amp;gt;math.fsum&amp;lt;/code&amp;gt; function uses a variant of Kahan’s algorithm (actually a more advanced multi-part compensation) to sum iterables with high precision.&lt;br /&gt;
&lt;br /&gt;
There are variations and improvements on basic Kahan summation. &#039;&#039;&#039;Neumaier’s algorithm&#039;&#039;&#039; (sometimes called &#039;&#039;Kahan-Babuška-Neumaier&#039;&#039;) improves Kahan’s method by also handling cases where the next input is larger in magnitude than the running sum&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. In such cases, the original Kahan formula could suffer error; Neumaier’s version effectively swaps roles when a huge value comes in. Its pseudocode is:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight&amp;gt;&lt;br /&gt;
function NeumaierSum(array):&lt;br /&gt;
    sum = 0.0&lt;br /&gt;
    compensation = 0.0&lt;br /&gt;
    for each x in array:&lt;br /&gt;
        t = sum + x&lt;br /&gt;
        if (fabs(sum) &amp;gt;= fabs(x))&lt;br /&gt;
            compensation += (sum - t) + x;&lt;br /&gt;
        else&lt;br /&gt;
            compensation += (x - t) + sum;&lt;br /&gt;
        sum = t;&lt;br /&gt;
    return sum + compensation;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This algorithm ensures the compensation covers both scenarios: when the sum is the bigger addend or when the new term is bigger&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. In the end, it adds the final &amp;lt;code&amp;gt;compensation&amp;lt;/code&amp;gt; once to the result. Neumaier’s method can produce the correct result in cases where basic Kahan fails. For instance, summing [1.0, $+10^{100}$, 1.0, $-10^{100}$] in double precision: regular Kahan summation might give 0.0 (losing the small terms), whereas Neumaier’s algorithm yields 2.0 exactly&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. There are also higher-order compensation methods (carrying multiple compensation terms, e.g., the &#039;&#039;&#039;Klein summation&#039;&#039;&#039; variant&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt; or using arbitrary precision for intermediate sums) that further reduce error at increased computational cost&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
In summary, compensated summation algorithms trade extra computation for greatly improved numerical accuracy. They are especially valuable when summing large arrays of numbers with varying magnitudes, where naive summation could lose most of the low-order bits. Many scientific computing environments provide compensated sum routines for this reason. In deep learning, however, a different approach to improved precision is often taken: &#039;&#039;&#039;using a wider accumulator type&#039;&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
=== 3.4. Mixed-Precision Accumulation ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Mixed-precision accumulation&#039;&#039;&#039; refers to performing arithmetic in a higher precision than the input data. A common scenario is accumulating sums in 32-bit floats when the operands are 16-bit floats. Rather than implementing a compensation algorithm, one can simply use a wider accumulator so that intermediate rounding error is smaller. This is effectively what modern GPUs do for tensor operations: for example, NVIDIA &#039;&#039;&#039;Tensor Cores operate on FP16 input data with an FP32 accumulator&#039;&#039;&#039; (the 16-bit products are accumulated in 32-bit)&amp;lt;ref name=&amp;quot;ref6&amp;quot;/&amp;gt;. In this way, summing 2048 products in a dot product has far less precision loss than it would with a 16-bit accumulator. Hardware that supports mixed precision can give the best of both worlds: fast low-precision multiplies and high-precision accumulation.&lt;br /&gt;
&lt;br /&gt;
Software can also leverage mixed precision. For instance, one can sum an array of &amp;lt;code&amp;gt;float&amp;lt;/code&amp;gt; values into a &amp;lt;code&amp;gt;double&amp;lt;/code&amp;gt; variable, accumulating the result in double precision and then converting back to float at the end:&lt;br /&gt;
&lt;br /&gt;
This approach often yields a more accurate &amp;lt;code&amp;gt;result&amp;lt;/code&amp;gt; than summing in single precision directly, because the intermediate sum carries 52-bit precision. The final rounding to float happens only once at the end (minimizing overall rounding error). In fact, the above is one way to implement &amp;lt;code&amp;gt;math.fsum&amp;lt;/code&amp;gt; in Python (it internally does something similar with multiple partial sums in double precision).&lt;br /&gt;
&lt;br /&gt;
In machine learning training and inference, &#039;&#039;&#039;mixed precision&#039;&#039;&#039; is widely used. When using FP16 for speed, it&#039;s common to accumulate with FP32. For example, &#039;&#039;&#039;Google&#039;s TPUs use BFloat16 for multiplication but accumulate in FP32&#039;&#039;&#039; for matrix ops&amp;lt;ref name=&amp;quot;ref6&amp;quot;/&amp;gt;. AWS’s Inferentia (Neuron) hardware similarly supports FP16/BF16 matrix multiplication with FP32 accumulation&amp;lt;ref name=&amp;quot;ref7&amp;quot;/&amp;gt;. The motivation is that summing hundreds or thousands of half-precision values would otherwise introduce intolerable error or risk overflow. With an FP32 accumulator, the reduction error is drastically reduced (by about $2^{13}$ times, since FP32 has 13 more precision bits than FP16). Mixed-precision accumulation thus provides a simple and effective form of compensation: &#039;&#039;&#039;use a larger bucket to collect many small contributions&#039;&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
There is a spectrum of options: summing int8 or int16 integers might use 32-bit integers as accumulators for the same reason (to avoid overflow). Some BLAS libraries even offer &#039;&#039;extended precision accumulators&#039;&#039; for single precision dot products (accumulating in double). The downside of mixed precision is the extra memory and maybe throughput cost of the wider type. But on many platforms, the add latency is dominated by hardware design rather than bit-width, so using a 32-bit adder vs 16-bit has negligible speed impact if supported natively. The key is whether data movement or register pressure increases.&lt;br /&gt;
&lt;br /&gt;
In summary, &#039;&#039;&#039;using a higher precision accumulator is often the easiest way to improve numerical accuracy&#039;&#039;&#039; for reductions. It is a primary strategy in deep learning hardware to maintain stability while exploiting low precision for storage and initial computations.&lt;br /&gt;
&lt;br /&gt;
== Precision Formats in AI and Machine Learning ==&lt;br /&gt;
&lt;br /&gt;
Modern AI systems use a variety of numeric formats to balance speed, memory, and accuracy. We discuss common precisions and their roles:&lt;br /&gt;
&lt;br /&gt;
=== 4.1. FP32 and FP64 in Training and Inference ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;FP32 (single precision)&#039;&#039;&#039; has been the workhorse of deep learning for years. Most training scripts in frameworks like TensorFlow and PyTorch used FP32 by default, as it offers sufficient precision to propagate gradients and accumulate weight updates over millions of iterations without excessive error. With ~7 decimal digits of precision and a wide exponent range, FP32 can handle the dynamic range of activations and gradients in typical neural networks. In inference, FP32 is often more precision than strictly necessary for final predictions, but it has remained common because of its ease and the fact that many GPU operations were optimized for FP32.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;FP64 (double precision)&#039;&#039;&#039; is rarely used in deep learning model training or inference, except in specific scenarios requiring extreme precision. FP64 is common in scientific computing and certain types of numerical simulations. In neural networks, however, the extra precision is usually not needed for model accuracy—using doubles tends to yield negligible improvements in model metrics but at a large performance cost (since on GPUs FP64 throughput can be 1/2, 1/8, or even 1/32 of FP32 throughput depending on hardware). One exception might be when integrating neural nets into larger physics simulations or when solving ill-conditioned problems as part of the model. Some researchers use FP64 to diagnose numerical issues or ensure reproducibility. Generally though, deep learning has favored lower precision for efficiency, and FP64 is considered overkill for most networks&amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
That said, certain classical machine learning algorithms benefit from double precision. For example, computing the inverse of a matrix (as in a Gaussian Process regression or Kalman filter embedded in a model) can incur large rounding errors in single precision, so using FP64 can significantly improve the result&amp;lt;ref name=&amp;quot;ref1&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref1&amp;quot;/&amp;gt;. The ONNX standard supports a float64 tensor type for such needs. In ONNX Runtime, however, emphasis has been on FP32 performance, and some pipelines default to float even if the original model was double&amp;lt;ref name=&amp;quot;ref1&amp;quot;/&amp;gt;. If a model’s prediction computation involves operations that are numerically sensitive (like matrix inversion, Cholesky decomposition, or summing extremely large counts), then FP64 may be required to avoid noticeable discrepancies&amp;lt;ref name=&amp;quot;ref1&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref1&amp;quot;/&amp;gt;. A good practice is to test a model in both float and double if you suspect precision issues; significant differences indicate an ill-conditioned calculation that might need higher precision.&lt;br /&gt;
&lt;br /&gt;
In summary, FP32 remains the &#039;&#039;&#039;default&#039;&#039;&#039; format for most training and inference due to its balance of accuracy and efficiency&amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;, while FP64 is reserved for niche cases or precision-critical subroutines. FP32 typically achieves the same accuracy as FP64 for end-to-end deep learning tasks, because network training can often compensate for small noise, and other sources of error (like generalization error) dominate. Therefore, the community trend has been moving to &#039;&#039;&#039;lower&#039;&#039;&#039; precisions, not higher, to gain speed — provided the models still converge and perform well.&lt;br /&gt;
&lt;br /&gt;
=== 4.2. FP16 and BFloat16 in Deep Learning ===&lt;br /&gt;
&lt;br /&gt;
To further speed up neural network training and inference, &#039;&#039;&#039;half-precision (FP16)&#039;&#039;&#039; was adopted, especially with the advent of GPUs supporting FP16 math at twice the rate of FP32. Using FP16 reduces memory storage and bandwidth by 50% compared to FP32, which is significant for large models. However, FP16’s small exponent range and precision posed challenges: training a large network purely in FP16 can fail due to gradient underflow/overflow or weight updates not registering (because changes are below the 0.001 quantization step). To address this, frameworks introduced &#039;&#039;&#039;mixed-precision training&#039;&#039;&#039;, where compute-intensive parts (matrix multiplies, convolutions) run in FP16, but certain accumulations and model parameters remain in FP32. For instance, NVIDIA’s &#039;&#039;&#039;Tensor Core&#039;&#039;&#039; units require FP16 (or BF16) inputs but accumulate results in FP32, effectively ensuring that reductions are done in higher precision&amp;lt;ref name=&amp;quot;ref6&amp;quot;/&amp;gt;. Research showed that with proper loss scaling and mixed precision, models can train to FP32-level accuracy using mostly FP16 arithmetic, achieving significant speedups.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;BFloat16 (BF16)&#039;&#039;&#039; emerged as an alternative 16-bit format primarily from Google’s TPU training systems. BF16 has a larger exponent (8 bits, same as FP32) but only 7 fraction bits&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. This means BF16 can handle the &#039;&#039;&#039;same dynamic range&#039;&#039;&#039; as FP32 (no overflows where FP32 wouldn’t) but at the cost of even lower precision than FP16 (BF16 has about 2–3 decimal digits of precision vs FP16’s ~3–4). Despite its coarse precision, BF16 turns out to be effective for training deep networks because the algorithms (stochastic gradient descent, etc.) are somewhat robust to that noise, and having the wide range prevents catastrophic loss of signal (no gradient will overflow to Inf unless it was going to in FP32 anyway). Many training frameworks (PyTorch, TensorFlow) support BF16 training on hardware like &#039;&#039;&#039;TPU, NVIDIA A100+, Intel CPUs with AVX-512 BF16, etc&#039;&#039;&#039;. Typically, when using BF16, &#039;&#039;&#039;accumulations are still done in FP32&#039;&#039;&#039; (for example, a TPU v2/v3 multiplies BF16 matrices and accumulates into an FP32 result for the dot product&amp;lt;ref name=&amp;quot;ref6&amp;quot;/&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
In inference, both FP16 and BF16 are used to deploy models for faster speed or lower memory. FP16 is popular on NVIDIA GPUs (which have specialized FP16 units). BF16 is advantageous on TPUs and some CPUs that support it, and increasingly on GPUs (Ampere and newer GPUs also support BF16 arithmetic). The choice between FP16 and BF16 can depend on hardware: BF16’s advantage is easier software conversion (you can often take an FP32-trained model and cast weights to BF16 with minimal loss, since the dynamic range is intact), whereas FP16 might require adjusting the model or ensuring values fit in range (sometimes needing techniques like maintaining a master copy of weights in FP32 during training, or handling out-of-range values).&lt;br /&gt;
&lt;br /&gt;
For ONNX, both Float16 and BFloat16 types are part of the standard. ONNX models can be converted to FP16 (truncating weights and inserting cast nodes for computations) using tools like &amp;lt;code&amp;gt;float16_converter&amp;lt;/code&amp;gt; in ONNX Runtime&amp;lt;ref name=&amp;quot;ref8&amp;quot;/&amp;gt;. ONNX Runtime on GPUs will typically execute those FP16 ops with mixed precision on tensor cores (FP16 input, FP32 accumulate), whereas on CPU it may emulate FP16 or upcast to FP32 (as many CPU implementations do not natively support half-precision math)&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;. In fact, as an example, running an ONNX model in FP16 on a CPU might insert hidden Cast operations to FP32 behind the scenes, because the CPU kernel for that op only exists in FP32&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;. This ensures correctness (the computation is done in higher precision) but can reduce performance. We will revisit such backend-dependent behavior in Section 5.3.&lt;br /&gt;
&lt;br /&gt;
In summary, &#039;&#039;&#039;FP16 and BF16&#039;&#039;&#039; are now established as important low-precision formats in deep learning. FP16 offers more precision bits, which can matter for e.g. small differences, but has a limited range (care needed to avoid overflow). BF16 offers safety in range but less precision (which can slightly affect convergence or final accuracy in some cases). Both typically rely on FP32 for accumulation to achieve acceptable accuracy. When deploying models, one should choose a format supported efficiently by the target hardware and verify that the accuracy is not significantly degraded. Often, CNNs and transformers can run in FP16/BF16 with virtually no drop in score, but certain operations (like softmax, see Section 6.3) might need special handling.&lt;br /&gt;
&lt;br /&gt;
=== 4.3. FP8 Formats (E4M3, E5M2) and Scaling ===&lt;br /&gt;
&lt;br /&gt;
The push for ever lower precision has led to experimental &#039;&#039;&#039;8-bit floating point (FP8)&#039;&#039;&#039; formats for deep learning. In 2022, researchers from NVIDIA, Intel, and Arm proposed an FP8 standard with two variants: &#039;&#039;&#039;E4M3&#039;&#039;&#039; (4 exponent bits, 3 fraction bits) and &#039;&#039;&#039;E5M2&#039;&#039;&#039; (5 exponent bits, 2 fraction bits)&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;. Both have 1 sign bit, for a total of 8 bits. E5M2 has a wider exponent range (5 bits gives bias 15) but one fewer mantissa bit than E4M3. The idea is to use E4M3 for numbers that need more precision (like weights or activations around 1.0) and E5M2 for numbers that need more dynamic range (like gradient updates which can be very small or large). In fact, the study found that using E4M3 for forward activations/weights and E5M2 for backward gradients can allow training of large networks with FP8 to accuracy on par with FP16 training&amp;lt;ref name=&amp;quot;ref11&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
FP8 formats are &#039;&#039;not&#039;&#039; IEEE 754 standard (though IEEE is considering lower precision in its 2019 revision). Notably, the E4M3 format as used by NVIDIA does not allocate any bit-pattern for $+\infty$ or $-\infty$ – it has only NaN for invalid results, using that freed bit pattern to extend the representable range by one exponent value&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;. Essentially, E4M3 can represent numbers up to ~$2^{15}$ (around 3e4) instead of capping at infinity at $2^{16}$, by foregoing infinities&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;. Variations also exist regarding negative zero and how NaNs are handled&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;. For instance, GraphCore’s IPU uses FP8 formats that disallow negative zero and infinities, to maximize useable range&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Using 8-bit floats for neural networks requires additional techniques, chiefly &#039;&#039;&#039;scaling&#039;&#039;&#039;. Because 8-bit has very limited precision and range, one typically applies a power-of-two scale factor to groups of values to normalize them into an FP8-friendly range. This is akin to block floating-point or “microscaling” per layer or per tensor. For example, one might keep a FP32 scale for each channel or tensor, and represent values as $(FP8\_value) \times 2^{scale}$. The ONNX standard has introduced support for FP8 in version 1.15, including types for E4M3 and E5M2 (with variations FNUZ: finite-only and no negative zero)&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;, and even a type called &#039;&#039;&#039;E8M0&#039;&#039;&#039; which is an 8-bit exponent-only number used purely as a scale factor&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;. The E8M0 type has 8 exponent bits, no mantissa, effectively representing powers of two from $2^{-127}$ to $2^{128}$ for scaling purposes&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;. This indicates an approach where an FP8 tensor would be accompanied by an E8M0 scale (or scales) to interpret its value correctly.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Rounding&#039;&#039;&#039; becomes critical when casting values to FP8. The quantization error is much larger relative to FP16 or FP32. Studies have shown that choosing the right rounding mode (e.g., round-to-nearest, stochastic rounding, etc.) and clamping behavior (saturating to max instead of inf) can affect model accuracy&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;. ONNX added a &amp;lt;code&amp;gt;round_mode&amp;lt;/code&amp;gt; attribute to its Cast operator to allow controlling this for FP8&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;. In particular, one paper found that &#039;&#039;&#039;rounding up with saturation&#039;&#039;&#039; (round-to-nearest ties away from 0, and clamp overflows to max finite rather than inf) gave better accuracy in large language model training than other rounding modes&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
At present, FP8 is on the cutting edge. NVIDIA’s H100 GPU supports FP8 tensor cores (for both E4M3 and E5M2), and early results show minimal accuracy loss on some vision and NLP tasks when properly tuned. However, using FP8 requires fine-tuning hyperparameters like scaling factors for each layer and perhaps tweaking training procedures. ONNX’s inclusion of FP8 types means you can represent and transport models using FP8 weights or activations, but actual hardware support is limited and typically paired with calibration or dynamic scaling logic outside of ONNX graph (or using custom ONNX functions for scaling). It remains an &#039;&#039;&#039;open research area&#039;&#039;&#039; how to fully automate FP8 quantization for arbitrary models without accuracy loss.&lt;br /&gt;
&lt;br /&gt;
In summary, FP8 formats promise further compression of neural network computation and data. They are an order of magnitude more error-prone than FP16, but when combined with careful scaling, mixed usage of E4M3/E5M2, and maybe modified training techniques, they have shown surprising success. ONNX is actively evolving to support these ultra-low precision types, signaling their potential importance in future AI inference deployments.&lt;br /&gt;
&lt;br /&gt;
== Accumulation Semantics in ONNX ==&lt;br /&gt;
&lt;br /&gt;
One of the roles of the ONNX specification is to define the mathematical behavior of operators (Conv, MatMul, Sum, etc.) in a framework-agnostic way. However, ONNX mostly defines &#039;&#039;what&#039;&#039; an operator computes in exact arithmetic, not &#039;&#039;how&#039;&#039; the computation is carried out in finite precision. This leaves certain details – like accumulator precision or summation order – up to the implementation (runtime or hardware). We&#039;ll examine how ONNX treats accumulation and what guarantees (or lack thereof) are provided.&lt;br /&gt;
&lt;br /&gt;
=== 5.1. Operator Definitions and Numerical Guarantees ===&lt;br /&gt;
&lt;br /&gt;
Each ONNX operator is described as performing an ideal mathematical function on its inputs. For example, &amp;lt;code&amp;gt;MatMul&amp;lt;/code&amp;gt; is defined as the standard matrix product of two tensors, and &amp;lt;code&amp;gt;ReduceSum&amp;lt;/code&amp;gt; computes a summation along given axes. The ONNX spec, as of current versions, &#039;&#039;&#039;does not mandate the precision of intermediate calculations&#039;&#039;&#039; beyond the types of the inputs and outputs. If an operator’s inputs and outputs are float16, the spec implies the computation is done in float16 in a sense, but it does not forbid an implementation from using higher precision internally – it simply doesn&#039;t mention it. There is &#039;&#039;&#039;no explicit guarantee of identical bit-for-bit results&#039;&#039;&#039; across different ONNX backends for floating-point ops. Instead, results are expected to be numerically close, within tolerances one would normally expect from floating-point differences (due to different hardware or algorithms). The ONNX Backend Test suite typically uses a relative or absolute error tolerance when validating outputs for this reason.&lt;br /&gt;
&lt;br /&gt;
Because of this, important details like the use of extended precision or accumulation in higher precision are left ambiguous. This has led to discussions in the ONNX community about making such behavior more explicit. For example, a proposal was raised to allow specification of accumulation precision for certain ops like MatMul&amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;. The reasoning is that as lower-precision types (float16, bfloat16, float8) become more common, knowing or controlling whether an op internally accumulates in a higher precision is important for both accuracy and reproducibility&amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;. In June 2025, an issue on the ONNX GitHub suggested adding an attribute to MatMul (and similarly Gemm, Softmax, etc.) to specify the accumulator type&amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;. By default it would use input precision (status quo), but an attribute could request (for instance) 32-bit accumulation even if inputs are 16-bit&amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;. As of this writing, however, no such attribute exists in the official spec – it&#039;s an open issue. Thus, the numerical semantics are effectively: &#039;&#039;“compute the result as if in the same type as inputs, but actual implementations may use more precision internally as an optimization.”&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
From a guarantee standpoint, ONNX operators promise the mathematically correct result assuming real-number arithmetic, but they &#039;&#039;&#039;do not guarantee bit-exact results&#039;&#039;&#039; due to floating-point effects. There is an understanding that differences can arise from different summation orders, fused operations (e.g., an implementation might use an FMA (fused multiply-add) which has one rounding instead of two), or use of extended precision registers. ONNX does not formally define rounding modes either – it relies on IEEE 754 defaults, but if a backend used something like deterministic summation or alternative rounding, ONNX has no mechanism to express that.&lt;br /&gt;
&lt;br /&gt;
In summary, ONNX&#039;s current stance is largely &#039;&#039;&#039;implementation-dependent&#039;&#039;&#039; for numerical precision. It provides minimal explicit numerical guarantees beyond type constraints. This is why an ONNX model run on two different hardware or runtime libraries can have slightly different results, especially in reduced precision. The burden is on backends to produce results &amp;quot;close enough&amp;quot; to each other that they don&#039;t affect the model&#039;s decisions in any significant way.&lt;br /&gt;
&lt;br /&gt;
=== 5.2. MatMul, Conv, and Reduction Operators ===&lt;br /&gt;
&lt;br /&gt;
Consider ONNX &amp;lt;code&amp;gt;MatMul&amp;lt;/code&amp;gt;: it takes two tensors (often two matrices) and produces their matrix product. In pure math, $C_{ij} = \sum_k A_{ik} &#039;&#039; B_{kj}$. If A and B are float16, should that sum be computed in float16 or float32? As discussed, ONNX doesn’t specify – it simply says the output is a float16 matrix containing the product. In practice, many backends will &#039;&#039;promote&#039;&#039; the computation: for example, NVIDIA GPUs will do the multiplication and addition in a mixed FP16/FP32 mode by default (because of tensor cores), then convert the final result back to float16. ONNX Runtime on CPU might not support float16 multiplication natively, so it will insert cast ops to float32, perform the MatMul in float32, then cast back to float16&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;. From ONNX’s perspective, as long as the final result is within acceptable error of the “ideal” float16 computation, it’s fine. But note, an “ideal” float16 computation (where every multiply and add is rounded to FP16) might actually be &#039;&#039;less accurate* than what the backend did with FP32. This means that a backend using higher precision can produce a result that is not bit-equal to a pure FP16 implementation – it&#039;s actually closer to the true real number result. This is usually considered a good thing (improved accuracy), but it does highlight a lack of consistency: another backend might do everything in pure FP16 and get a slightly different answer.&lt;br /&gt;
&lt;br /&gt;
The same situation occurs for &#039;&#039;&#039;convolution&#039;&#039;&#039; (&amp;lt;code&amp;gt;Conv&amp;lt;/code&amp;gt; op) which is essentially a batched sum of products (like MatMul in sliding window), and for explicit reduction ops like &amp;lt;code&amp;gt;ReduceSum&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;ReduceMean&amp;lt;/code&amp;gt;, etc. If you have an ONNX &amp;lt;code&amp;gt;ReduceSum&amp;lt;/code&amp;gt; that sums 1000 float16 values, the spec doesn’t say how to sum them. A GPU kernel might sum in a tree using FP32 accumulators; a naive implementation might sum in order with FP16 and possibly overflow or underflow intermediate steps. Both are allowed as long as they output a float16 result that is “reasonable.” In practice, ONNX Runtime and others take care to avoid obviously bad behavior (like overflow) by using higher precision or by splitting the sum. But again, it’s not mandated by the ONNX standard explicitly.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Softmax&#039;&#039;&#039; is an interesting case: Softmax involves an internal sum of exponentials. ONNX defines the Softmax operator as $$\mathrm{Softmax}(x)_i = \frac{\exp(x_i)}{\sum_j \exp(x_j)},$$ typically applied along a specified axis. Directly computing this in float16 can be problematic for large vectors, as discussed in Section 6.3. Indeed, the issue of accumulation precision arises: should the sum $\sum_j \exp(x_j)$ be done in FP16 or can it be in FP32? Many implementations will do it in FP32 even if $x$ is FP16, because the dynamic range of $\exp(x)$ is large. The ONNX Softmax spec doesn’t detail this, but it expects the result to be a proper probability distribution (summing to 1.0 within normal floating error). A pure FP16 softmax on, say, 1000 elements could sum to something like 0.996 or 1.004 due to rounding, whereas FP32 accumulation would be much closer to 1.000. ONNX likely allows both; the small difference is usually inconsequential, but if it were large (say the FP16 sum overflowed to inf), that would be a correctness issue. A robust backend will prevent that (by using the common max-subtraction trick and higher precision accumulation). The ONNX issue 7072 indeed calls out Softmax as another op where explicit control of accumulation precision would be useful&amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
In summary, &#039;&#039;&#039;MatMul, Conv, and reduction ops&#039;&#039;&#039; in ONNX are defined mathematically but with &#039;&#039;implicit&#039;&#039; numerical behavior. Implementations commonly use higher precision for intermediate sums especially when input is low precision, but this is not standardized. Consequently, results can differ slightly between implementations. For critical applications, one must be aware of these differences. A model exported to ONNX with float16 weights might perform slightly differently on one runtime vs another if one does all float16 math and the other uses float32 accumulations. Both conform to ONNX. As a user, if you require a certain precision for accumulation, currently you have to ensure it by the way you export or run the model (for example, insert manual cast nodes in the ONNX graph to force certain ops to FP32). We may see future ONNX versions address this with attributes or metadata as mentioned, but for now these ops are effectively “compute with best effort accuracy”.&lt;br /&gt;
&lt;br /&gt;
=== 5.3. Backend and Hardware-Dependent Behavior ===&lt;br /&gt;
&lt;br /&gt;
Because ONNX is implemented by many backends (ONNX Runtime, TensorRT, CoreML, PyTorch JIT, etc.), and these in turn run on varied hardware (CPU, GPU, accelerators), the numeric behavior can be backend-dependent. Some examples:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;CPU backends:&#039;&#039;&#039; Many CPUs do not have native float16 vector arithmetic (with a few exceptions in recent CPUs). ONNX Runtime’s default CPU execution provider will often upcast float16 to float32, compute, then downcast&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;. This ensures accuracy but means that if you thought you were getting a full float16 pipeline, you’re not – the CPU is essentially ignoring the low precision in compute. On the other hand, x86 CPUs have extended 80-bit precision in x87 registers (though less used now) and 64-bit SIMD registers for doubles; some compiler optimizations might carry intermediate results in higher precision. This could make CPU results slightly different from a GPU’s if, say, a sum is done in 80-bit on CPU versus 32-bit on GPU. Generally, modern compilers avoid such extended precision unless explicitly asked, to improve reproducibility.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;GPU backends:&#039;&#039;&#039; GPUs often perform summations in parallel using tree reductions. The order of operations can change depending on how threads are scheduled. This means even on the same GPU, summing an array in one launch versus another might not give bit-identical results if the internal parallel reduction algorithm differs (though it should be deterministically the same given the same library version and input, but if you change GPU model or BLAS library, it might differ). Also, GPUs have fused multiply-add (FMA) instructions that do $(a*b + c)$ in one step with one rounding, which can produce slightly different results than doing the multiply and add separately (which would round twice). If an ONNX backend uses a fused kernel (like a GEMM library) it might yield different low-order bits than a simple loop.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Vendor libraries:&#039;&#039;&#039; ONNX Runtime can use vendor BLAS or DNN libraries (MKL, cuDNN, TensorRT, etc.). These libraries often implement their own strategies for numerical stability. For example, NVIDIA’s cuDNN might use FP32 accumulation for FP16 inputs by default. TensorRT (NVIDIA’s inference engine) will sometimes automatically promote precisions or insert scaling to maintain stability&amp;lt;ref name=&amp;quot;ref13&amp;quot;/&amp;gt;. On the other hand, if you explicitly request FP16 in TensorRT with no FP32 fallback, it might do the entire op in FP16 and you’d see more error. Some libraries offer knobs: TensorRT allows choosing FP32, FP16, or INT8 precision for each layer, and will use FP32 internally for safety in some cases unless forced otherwise.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Hardware-specific quirks:&#039;&#039;&#039; As another example, NVIDIA’s &#039;&#039;&#039;TensorFloat-32 (TF32)&#039;&#039;&#039; on Ampere GPUs is a precision mode where matrix multiplications use 10-bit mantissa (so about FP16 precision for the multiply) but keep 8-bit exponent (range like FP32). By default, CUDA libraries use TF32 for FP32 matrix multiplies on Ampere to speed them up, unless you disable it. This means if you run an ONNX model with FP32 on an A100 GPU, your matmuls might actually be lower precision than FP32 (though higher than FP16)&amp;lt;ref name=&amp;quot;ref6&amp;quot;/&amp;gt;. It’s an example of hardware choosing an intermediate precision for performance. From ONNX’s perspective, the output is still FP32 and (hopefully) within error tolerances, but a user might be surprised that it’s not a “true” FP32 computation internally. If exactness is needed, one has to turn off TF32.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Non-determinism:&#039;&#039;&#039; Some backends (especially on GPUs) allow non-deterministic execution for performance. For instance, atomic adds in parallel reductions can produce results that vary run-to-run because the order of accumulation is race-condition-dependent. ONNX’s spec doesn’t cover this explicitly, but it’s something to consider. In ONNX Runtime, there are settings for deterministic computing, but by default it may use all available performance optimizations, even if results vary at the 1e-7 level.&lt;br /&gt;
&lt;br /&gt;
In short, the numerical behavior under ONNX can depend on the combination of backend software and hardware capabilities. The lack of a strict spec for accumulation means implementations have freedom to use clever strategies (higher precision, reordering) to improve accuracy or speed. Usually this is positive (e.g. getting better accuracy than a naive approach). But it could also lead to slight inconsistencies. For example, you might find that exporting a PyTorch model to ONNX and running it with one engine yields outputs slightly different from PyTorch’s – not because ONNX is wrong, but because the summation or convolution was done differently (maybe PyTorch summed in FP32 as well, but maybe not in exactly the same way).&lt;br /&gt;
&lt;br /&gt;
The key takeaway is that ONNX &#039;&#039;by design&#039;&#039; abstracts away these details, which is good for portability but means you need to &#039;&#039;&#039;trust the backend&#039;&#039;&#039; or verify its precision behavior for sensitive models. If necessary, you can enforce certain behavior by inserting ops (for example, if you want to ensure FP32 accumulation, you could insert a cast to FP32 for inputs of a MatMul and cast the result back to FP16 – this way the ONNX graph itself mandates FP32 compute in between). This of course sacrifices some performance or device-specific magic.&lt;br /&gt;
&lt;br /&gt;
Looking forward, as mentioned, there are proposals to enrich ONNX’s specification regarding precision. This might include explicit flags for “exact summation” or “accumulate in int32” etc., which would make models more self-descriptive but also potentially less portable (if a backend doesn’t support that, what then?). For now, users should be aware of these backend differences. Most of the time, well-engineered ONNX runtimes yield results close enough that they don&#039;t affect the application (e.g., classification top-1 remains the same even if low-level sums differ by 1e-5). But for high-stakes numerical computations, testing and maybe adjusting the model or backend settings is prudent.&lt;br /&gt;
&lt;br /&gt;
== Numerical Stability in Deep Learning Models ==&lt;br /&gt;
&lt;br /&gt;
Deep learning computations involve many operations that, while theoretically well-defined, can be sensitive to numerical issues when implemented in finite precision. We&#039;ll discuss a few common scenarios in neural networks where numerical stability is a concern, and how precision and accumulation play a role.&lt;br /&gt;
&lt;br /&gt;
=== 6.1. Dot Products and Large-Scale Reductions ===&lt;br /&gt;
&lt;br /&gt;
Neural networks are full of dot products: every fully-connected layer computes $\mathbf{y} = W\mathbf{x}$, which is a series of inner products between weight vectors and the input. Convolutional layers compute weighted sums of local regions. Attention mechanisms compute large matrix products of query/key/value matrices. All these boil down to summing a lot of terms: $\sum_{k=1}^K a_k b_k$. When $K$ is large (it could be in the thousands or more for a single neuron in a fully connected layer of a large model), the potential for floating-point error in that sum increases.&lt;br /&gt;
&lt;br /&gt;
If using FP32, the error may still be negligible in many cases, but in lower precision like FP16, it can become significant. For example, consider a dot product of length 2048 in FP16. Each product is in FP16 (with ~3 decimal digits precision) and if accumulated in FP16, the worst-case error could be on the order of $2048 \times \varepsilon_{FP16} \approx 2048 \times 0.0009766 \approx 2$ (i.e., you could lose a couple units in the third decimal place relative accuracy in worst case). Even in random error accumulation, you might see error on the order of $\sqrt{2048} \times 0.0009766 \approx 0.044$ or 4.4% (though this is a rough estimate). That’s quite significant. Fortunately, as discussed, most hardware will accumulate such sums in a higher precision like FP32, dramatically reducing that error (~7 orders of magnitude smaller $\varepsilon$). But if it didn’t, the network’s outputs could get noisy. A fully-connected layer with many inputs might output slightly different results depending on summation order if done in low precision.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Large-scale reductions&#039;&#039;&#039; also occur in operations like global average pooling (summing all elements of a feature map) or computing the total loss by summing loss over batch samples. If one sums 100,000 values in FP32, the worst-case error could be on the order of 100,000 &#039;&#039; 1e-7 ≈ 1e-2 (1% uncertainty), though typical error would be much less. In FP16, summing 100k values is likely impossible without overflow: as noted, adding ~10^4 moderate numbers can overflow FP16&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. So large reductions &#039;&#039;must* be handled carefully (either in chunks or in higher precision). Frameworks generally do: for example, when performing a sum reduction on GPU, libraries break it into parallel blocks that accumulate in registers (usually 32-bit registers, even if inputs are 16-bit). &lt;br /&gt;
&lt;br /&gt;
Another aspect is that many dot products in neural nets involve values of varying magnitude. If the distribution of terms is such that some are much larger than others, the smaller terms might not contribute much if summed last. Ideally, one might sort by magnitude or use pairwise summation to improve that. Most BLAS libraries effectively do a pairwise strategy due to their parallel structure. So, while worst-case analyses are grim, practical implementations mitigate a lot of it.&lt;br /&gt;
&lt;br /&gt;
However, certain layers can suffer if not handled properly. One famous example is in RNNs or LSTMs: if one uses FP16 naive summation for the gating mechanism, errors could accumulate over many time steps and cause drift or instability. In modern practice, recurrent layers are often run in FP16 with FP32 accumulators to avoid this.&lt;br /&gt;
&lt;br /&gt;
In summary, &#039;&#039;&#039;dot products and large reductions need sufficient precision&#039;&#039;&#039; or robust algorithms. If you are implementing a custom kernel (say a custom ONNX op) and you have to sum a huge array, you should consider using a technique from Section 3 (pairwise summation, Kahan, or at least double accumulator if possible). Many deep learning computations, by virtue of hardware and library design, already do something like this under the hood. But it&#039;s always good to be aware, especially if pushing into lower precision territories.&lt;br /&gt;
&lt;br /&gt;
=== 6.2. Cancellation, Dynamic Range, and Conditioning ===&lt;br /&gt;
&lt;br /&gt;
We introduced &#039;&#039;cancellation&#039;&#039; earlier in the context of summation. In deep learning, cancellation can appear in a few places. One is in the computation of certain norms or variances. For instance, Batch Normalization computes mean and variance of activations. The variance formula $\mathrm{Var}(x) = E[x^2] - (E[x])^2$ is subject to catastrophic cancellation if $E[x]$ and $\sqrt{E[x^2]}$ are close in magnitude (which can happen if the variance is small compared to the mean). If implemented naively in single precision, you might get a negative variance due to numerical error (which would then be clamped to zero or sqrt would yield NaN). Robust implementations compute variance in a single pass or increase precision. Indeed, in many frameworks, batchnorm accumulation is done in FP32 even if the activations are FP16, precisely to avoid numerical issues in computing mean and variance. ONNX itself doesn’t specify this, but an ONNX backend is likely to do batchnorm stats in higher precision for safety. The &#039;&#039;conditioning&#039;&#039; of the variance calculation is poor when variance is tiny relative to mean, so it&#039;s a spot where higher precision is needed.&lt;br /&gt;
&lt;br /&gt;
Another example is in certain loss functions or regularizations. If you subtract two large numbers to get a small difference, you lose precision. Say you have two nearly identical predictions and you subtract them as part of some operation – if done in float16, you might get zero whereas float32 would have a slight difference. This could affect, say, a gradient if that difference was supposed to propagate information. Fortunately, neural nets often avoid directly subtracting large nearly equal numbers; but it can happen. In attention mechanisms, the &#039;&#039;score normalization&#039;&#039; (softmax) subtracts a max to avoid huge exponentials, which is good for range but if multiple scores are close to the max, you subtract numbers that are close. However, those subtractions are not catastrophic in the same way; they&#039;re intentional to reduce range and are typically fine in FP32 or even FP16.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Dynamic range&#039;&#039;&#039; issues are everywhere in deep learning. Activation functions like ReLU and exponential can produce very large or very small values. If subsequent computations don’t account for that, you get infinities or zeros. For example, consider a network output that produces a logit of 100 in FP16. $\exp(100)$ in FP16 overflows to infinity (since FP16 max ~ 6e4 and $e^{100} \sim 3.7e43$). If you then do softmax, that one infinity will dominate the sum and you might get a probability of 1 for that class (or NaNs if not handled). The standard softmax implementation subtracts the max logit, so in this case it would subtract 100 from all logits, making the largest exponent $\exp(0)=1$ and others tiny – this avoids overflow. But what about underflow? If you had a very negative logit, $\exp(-100)$ in FP16 would underflow to 0. In softmax, that just contributes ~0 probability, which is fine (it effectively is zero anyway). Underflow tends to be less dangerous than overflow in inference — underflow usually just means “this probability/neuron is effectively 0,” whereas overflow can poison the results with NaNs or Infs. &lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Conditioning&#039;&#039;&#039; refers to how sensitive a function is to input perturbations. Neural networks themselves as functions can be poorly conditioned (e.g., tiny changes in weights cause big changes in output for some architectures), but typically they are designed to be reasonably well-behaved. Certain internal computations like matrix inversion (if one does a pseudoinverse or something for a layer) can be extremely ill-conditioned if the matrix is near singular. That again would need double precision perhaps. In ML, one common ill-conditioned operation is the &#039;&#039;&#039;softmax denominator&#039;&#039;&#039;: $\sum_j e^{x_j}$ can be very large and also very sensitive to changes in the largest $x_j$. However, the softmax is more stable if computed carefully (max trick). Another is &#039;&#039;&#039;normalization&#039;&#039;&#039;: computing $\frac{x - \mu}{\sigma}$ for layer norm or batch norm – if $\sigma$ is very small, then the division blows up noise. In those cases, a small $\epsilon$ is usually added to $\sigma^2$ to prevent instability (this is a model-level fix, not a numeric one per se, but it acknowledges numeric limits).&lt;br /&gt;
&lt;br /&gt;
In training, &#039;&#039;&#039;optimizers&#039;&#039;&#039; can suffer from precision issues. For example, the Adam optimizer keeps a moving average of squared gradients; in half precision, these small updates might flush to zero or not change at some point. Some research found that certain adaptive optimizers break in FP16 because the small differences are lost (&amp;quot;Why FP16 training breaks NAdam&amp;quot; etc.). The solution is often to keep the optimizer states in FP32 while doing the bulk of compute in FP16. This again is a form of mixed precision.&lt;br /&gt;
&lt;br /&gt;
From an inference perspective, once the model is trained, the main concerns are avoiding overflow in activations and ensuring summations (like in softmax or reductions) are done with enough precision. The model’s weights are fixed, so we don&#039;t have to worry about cumulative error over iterations as in training, just the forward pass.&lt;br /&gt;
&lt;br /&gt;
=== 6.3. Normalization Layers and Softmax ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Normalization layers&#039;&#039;&#039; (BatchNorm, LayerNorm, etc.) and &#039;&#039;&#039;Softmax&#039;&#039;&#039; are specifically worth examining for numerical stability:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Batch Normalization:&#039;&#039;&#039; During inference, batchnorm applies an affine transform using precomputed mean and variance (from training). It’s basically: $y = \frac{x - \mu}{\sqrt{\sigma^2 + \epsilon}} \gamma + \beta$. This is a stable operation as long as $\epsilon$ is provided (to avoid division by zero). Since $\mu, \sigma^2$ are constants in inference, it reduces to linear ops which are fine. The main numerical work (computing mean/variance) happened during training or model calibration, and frameworks usually did that in FP32. So batchnorm at inference time is not a source of instability (it’s just a scale and shift of the data).&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Layer Normalization / Group Normalization:&#039;&#039;&#039; These compute mean/var on the fly for each sample (and possibly channel group). In inference, they still need to compute these statistics for each forward pass. If implemented in low precision, the variance calculation can suffer cancellation as mentioned. However, it&#039;s likely that inference engines compute the mean and variance in FP32 even if inputs are FP16 (especially since these are per-sample operations, not huge batch reductions – it&#039;s feasible to do in higher precision). If someone implemented layer norm purely in FP16, they might see issues when the variance is very small. Usually there’s an $\epsilon$ added which might dominate a tiny variance and thus mitigate catastrophic cancellation (the result might just output something mostly from $\epsilon$). In summary, normalization layers are usually fine as long as the implementation uses enough precision for the summation of squares.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Softmax:&#039;&#039;&#039; Softmax is notorious if done improperly. The safe algorithm: subtract the maximum input from all inputs (to bring the largest exponent to 1.0), exponentiate, sum them up, divide each exponent by the sum. The max subtraction handles overflow in $\exp()$. But what about the sum? As discussed, if there are $N$ elements in the softmax, the sum is at most $N$ (when all inputs equal the max). In classification, $N$ might be the number of classes. For ImageNet, $N=1000$, so worst sum ~1000 (which is fine in FP16, as 1000 &amp;lt;&amp;lt; 65504). For an extreme case like a language model, $N$ could be 50,000 (vocab size). Sum could be 50,000 in worst case (everyone equal probability). 50,000 is still below 65504, so actually an FP16 sum would &#039;&#039;not&#039;&#039; overflow in that scenario, and the rounding error on a sum of 50k identical terms would be maybe a few ULPs (which at 50k magnitude might be ~0.5 difference worst-case). So maybe softmax summation in FP16 is not catastrophic for output probabilities – you’d get essentially 1.0 after normalization instead of 0.99999 in some case. Usually fine.&lt;br /&gt;
&lt;br /&gt;
:The bigger issue is if one or a few terms dominate the softmax. If one term is much larger, the others underflow to 0 after the max subtraction. That’s fine: you get a one-hot distribution essentially. If the model expected a softer distribution, that could be a problem. But if the logits were really that far apart, even FP32 would give nearly one-hot. So not really a precision issue, more of a model property.&lt;br /&gt;
&lt;br /&gt;
:However, the &#039;&#039;&#039;stability during training&#039;&#039;&#039; is a big issue. The GitHub thread we saw (ColossalAI) mentioned training a 130B model where FP16 softmax in attention caused training to occasionally blow up&amp;lt;ref name=&amp;quot;ref14&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref14&amp;quot;/&amp;gt;. They observed attention scores in some heads ranging up to 1e4 or as low as 1e-3&amp;lt;ref name=&amp;quot;ref14&amp;quot;/&amp;gt;. 1e4 exponent in FP16 is definitely Inf (because $\exp(10^4)$ is astronomically huge, but even exp(100) is inf in half). The method CogView introduced (Precision Bottleneck Relaxation) involved subtracting the max &#039;&#039;per head&#039;&#039; to reduce range&amp;lt;ref name=&amp;quot;ref14&amp;quot;/&amp;gt;—which is basically the softmax max trick at a finer granularity or something similar. In the end, they found &#039;&#039;&#039;the simplest fix was to compute softmax in FP32&#039;&#039;&#039; for those layers&amp;lt;ref name=&amp;quot;ref14&amp;quot;/&amp;gt;. They reported that it had negligible speed impact but significantly improved stability&amp;lt;ref name=&amp;quot;ref14&amp;quot;/&amp;gt;. This highlights a general principle: certain operations like softmax (and sometimes matrix multiplications in attention) benefit from higher precision, and using FP32 in a few critical spots can save a mixed-precision training from crashing. In inference, using FP32 for softmax is also a safeguard if one is uncertain. It costs little because softmax is not a heavy compute compared to matrix multiplies.&lt;br /&gt;
&lt;br /&gt;
:ONNX does have a Softmax operator; if the input is float16, a backend might do exactly what they described: internally cast to float32 for the softmax computation, then cast output back to float16. Some inference engines (like DeepSpeed’s inference or others) keep attention softmax in FP32 even if rest is FP16, for robustness&amp;lt;ref name=&amp;quot;ref14&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Apart from softmax, other places where exponentials occur are in loss functions like cross-entropy (which uses log-softmax internally), or in activation functions like Sigmoid and Tanh (which are basically scaled logistic functions). Sigmoid($x)=1/(1+e^{-x})$. If $x$ is moderately large positive, $e^{-x}$ underflows to 0 in FP16 if $x&amp;gt;~11$. That gives a sigmoid output of 1.0 exactly. In FP32, $e^{-x}$ for $x=11$ is about $5e-5$, not underflow, so sigmoid ~0.99995. So FP16 will saturate some outputs to exactly 0 or 1 sooner than FP32 would. This can slightly change behaviors (e.g., a neuron might go to full saturation a bit early). Usually, this isn’t a huge deal, but it could make gradients zero earlier. In inference, we don’t care about gradients, just the output value. Getting exactly 1.0 vs 0.99995 probably has no practical difference for a classification output. But one should be aware that &#039;&#039;&#039;activation functions can saturate faster&#039;&#039;&#039; in lower precision due to limited range or precision. ReLU has no issue (it’s linear or 0), but exponential-based activations do. In practice, again, frameworks often simply compute those in the given precision and accept the slight difference. If it were a problem, one could do those in FP32 as well.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Summary:&#039;&#039;&#039; Numerical stability in inference is maintained by using robust computation methods: subtracting maxima in softmax/LogSumExp, adding epsilon in denominators for normalization, and using higher precision for summations or critical steps. Most of these are built into the algorithms. ONNX models assume these standard practices (they don&#039;t need to explicitly do them in the graph because the backend inherently will for ops like Softmax). It&#039;s wise when building or exporting models to ONNX to test them in the target precision and ensure there are no surprises (like an output becomes NaN or Inf due to precision). Usually if training was done in mixed precision, the model will have been vetted in that environment already.&lt;br /&gt;
&lt;br /&gt;
== Performance vs. Accuracy Trade-offs ==&lt;br /&gt;
&lt;br /&gt;
There is an inherent trade-off between numerical precision and computational performance (speed, memory usage) in AI systems. Choosing lower precision can dramatically increase throughput and reduce memory, but it may introduce arithmetic errors. Engineers must balance these considerations:&lt;br /&gt;
&lt;br /&gt;
=== 7.1. Throughput, Memory Bandwidth, and Precision ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Throughput (speed)&#039;&#039;&#039; is often higher for lower-precision operations. Modern hardware can execute more low-precision ops in parallel than high-precision ops. For example, an NVIDIA V100 GPU can perform 2× more half-precision FLOPs than single-precision per clock, and newer architectures with Tensor Cores can do 8× more (since a Tensor Core warp operation might perform 64 FP16 FMAs per cycle vs 16 FP32). Similarly, int8 operations can be even faster and more energy efficient. Lower precision numbers also use less memory bandwidth – fetching 16-bit data is twice as fast as 32-bit data from memory (assuming the same bus width), which can relieve memory bottlenecks. Many deep learning workloads are memory-bound, so halving the data size can nearly double throughput if compute is not the limiting factor. This is a huge incentive to use lower precision.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Memory and storage&#039;&#039;&#039;: Using FP16 or int8 weights reduces model size, which is important for deploying large models to memory-limited environments (like edge devices or GPUs with limited VRAM). It also means you can fit larger batch sizes in memory, or more models on a chip, etc. There is also a cascade effect: smaller data means better cache utilization, which further improves performance.&lt;br /&gt;
&lt;br /&gt;
However, &#039;&#039;&#039;accuracy&#039;&#039;&#039; suffers if precision is too low. We have discussed how rounding error and limited range can cause deviations in outputs. The question is: do those deviations matter for the application? Often for neural networks, a small amount of noise or error in intermediate calculations does not change the final prediction. Neural nets are somewhat error-tolerant due to their redundant distributed representations. This is why quantization and FP16 techniques can work at all. The final model accuracy (e.g. top-1 accuracy on ImageNet) might drop only 0.5% or less when using FP16 instead of FP32, or int8 instead of FP16, if done properly. For many use-cases, this loss is acceptable given the speed gains.&lt;br /&gt;
&lt;br /&gt;
If one goes too low (like naive FP8 or int4 without special care), accuracy might drop severely – perhaps making the model unusable. Thus the trade-off: as precision decreases, performance increases but at some point accuracy degrades beyond an acceptable threshold. Engineers aim to find the lowest precision that still preserves accuracy.&lt;br /&gt;
&lt;br /&gt;
There is also the consideration of &#039;&#039;&#039;accumulation strategy&#039;&#039;&#039; affecting performance: for instance, Kahan summation (compensated) is more accurate but ~4× slower than naive summation. If you need that accuracy, you pay a performance cost. In many AI inference scenarios, the slight extra accuracy from something like Kahan isn&#039;t needed, so it’s not used. But in a scenario where you sum millions of elements for a final result that is thresholded, maybe you do need it.&lt;br /&gt;
&lt;br /&gt;
Another dimension is &#039;&#039;&#039;determinism vs speed&#039;&#039;&#039;. Enforcing a deterministic reduction order (for reproducibility) might require disabling some parallelism, which could slow things down. If exact repeatability is critical (like certain scientific or financial applications of neural nets), one might sacrifice some performance to ensure the summation happens the same way every time. ONNX allows both modes depending on the runtime.&lt;br /&gt;
&lt;br /&gt;
From a memory perspective, if precision is higher than needed, you waste memory. For example, storing intermediate activations in FP32 when they could be FP16 uses 2× memory, which might mean lower batch size or more memory traffic. So there&#039;s strong motivation to keep things as low-precision as possible &#039;&#039;except&#039;&#039; where accuracy truly demands.&lt;br /&gt;
&lt;br /&gt;
In summary, the trade-off often manifests as &#039;&#039;diminishing returns&#039;&#039;: FP64 vs FP32 – huge slowdown for usually negligible accuracy gain in ML; FP32 vs FP16 – moderate speedup for usually minor accuracy hit; FP16 vs int8 – another big speedup but maybe more accuracy loss unless calibration is done; int8 vs int4 – speed gain but often big accuracy drop that might not be worth it for current models.&lt;br /&gt;
&lt;br /&gt;
Engineers use techniques like &#039;&#039;&#039;mixed precision&#039;&#039;&#039; to target high precision only where needed. For example, run most of the model in FP16, but keep a few sensitive layers in FP32. This yields most of the performance benefits while ensuring critical parts are accurate. We already saw softmax as one example. Another might be final output layer: sometimes using FP32 for the logits and softmax can be safer.&lt;br /&gt;
&lt;br /&gt;
Hardware is also trending to support mixed modes – like FP8 Tensor Cores accumulate in FP32, etc. So hardware tries to give accuracy cheaply.&lt;br /&gt;
&lt;br /&gt;
=== 7.2. Hardware Accelerators and Tensor Cores ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Hardware accelerators&#039;&#039;&#039; (GPUs, TPUs, FPGAs, ASICs like GraphCore IPU, etc.) have specific support for certain precisions. Leveraging these can provide massive speedups:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;NVIDIA Tensor Cores:&#039;&#039;&#039; These started in Volta (V100) with support for 4x4 matrix multiply-adds on FP16 inputs with FP32 accumulation&amp;lt;ref name=&amp;quot;ref6&amp;quot;/&amp;gt;. Using Tensor Cores can give up to 8x throughput compared to using regular FP32 cores. But to use them, the data needs to be FP16 (or now BF16 or FP8 on newer GPUs) and aligned to certain dimensions (multiples of 8 etc. in matrix sizes, which frameworks handle by padding if needed). If an ONNX model is all FP32, by default it would not use Tensor Cores unless the runtime automatically converts some ops to mixed precision (which some do via a flag or environment variable enabling TF32 or FP16). The introduction of &#039;&#039;&#039;TensorFloat-32 (TF32)&#039;&#039;&#039; in Ampere GPUs is essentially a compromise: treat FP32 inputs as two 16-bit significand halves (10-bit mantissa actually) so that it can run on Tensor Cores, giving ~8x speed, while still outputting FP32. TF32 has about the precision of FP16 but range of FP32, and it was chosen because it typically doesn’t hurt convnet accuracy. ONNX Runtime or others on Ampere will use TF32 by default for conv and matmul unless disabled, giving a free speed boost at minimal accuracy cost. If exact FP32 is needed, users have to turn it off (sacrificing speed).&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;TPU (Google):&#039;&#039;&#039; TPUs have native support for BF16 matrix ops with FP32 accumulators. They were designed specifically for the mixed precision approach in training. For inference, TPUs also support int8 and other quantized ops (e.g., via Edge TPU for lower-power). If exporting ONNX to run on a TPU (via some bridge), one might get automatic BF16 usage. BF16 has the advantage of no loss in range, so TPUs can more straightforwardly run models in BF16 without modifications.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;FPGAs and ASICs:&#039;&#039;&#039; Some custom accelerators might use fixed-point or block floating-point arithmetic for efficiency. They often treat the network in quantized terms. The ONNX quantization specification (QuantizeLinear/DequantizeLinear ops) exists for representing quantized models (INT8 etc.), which is another domain beyond floating-point, but it overlaps as a precision issue. In those systems, accumulators for INT8 × INT8 multiplies are usually 32-bit (to avoid overflow for dot products up to certain lengths). The trade-off there is between integer vs float representation, which can give big speedups on CPUs/DSAs via SIMD instructions (many CPUs have vector int8 multiply-add instructions now for inference).&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Memory bandwidth on hardware:&#039;&#039;&#039; Some accelerators include special high-bandwidth memory or cache specifically for lower precision. For example, an accelerator might fetch two FP16 values in one 32-bit memory transaction – effectively doubling memory bandwidth for half precision. On some hardware, using FP16 can even reduce memory &#039;&#039;latency&#039;&#039; if multiple values can be packed.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Parallelism and Vectorization:&#039;&#039;&#039; Many hardware units process data in fixed vector widths (e.g., 256-bit registers). That could be 8 floats at once or 16 half-floats at once in the same 256 bits. If your code is in FP16, you might use the full vector width (16 values) whereas FP32 might only use half (8 values). This means FP16 can make better use of vector units. Similarly, matrix units often have fixed tile sizes, and smaller data types allow larger tiles or more tiles concurrently.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Energy efficiency:&#039;&#039;&#039; Lower precision often means less energy per operation, which on large deployments (data centers or battery-powered devices) is a significant advantage. This isn&#039;t directly performance, but it&#039;s an important trade: you might choose a slightly lower precision to save power even if accuracy drops a tiny bit.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Software overhead:&#039;&#039;&#039; Using mixed precision on hardware involves some overhead of casting and moving data between precisions. For instance, if you have to frequently cast FP16 to FP32 and back, that adds instructions and memory copies. A well-designed accelerator (like GPUs with unified memory for FP16/32 or special hardware casting) minimize this. Still, the runtime might insert extra operations (as we saw, ONNX Runtime CPU inserted many Cast nodes converting FP16 to FP32 and back&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;, which actually &#039;&#039;slowed down&#039;&#039; that FP16 model by 4x vs FP32 on a CPU, because the CPU had no native FP16). So ironically, on some hardware, using lower precision in ONNX might degrade performance if not supported (the CPU case). Always ensure the target hardware actually supports the low precision you plan to use; otherwise, you&#039;ll pay overhead for emulation.&lt;br /&gt;
&lt;br /&gt;
To illustrate trade-off: Suppose you have a model that achieves 99% accuracy in FP32 and drops to 98.5% in FP16. If FP16 runs 2x faster, many would accept the 0.5% accuracy drop for double throughput. If int8 runs 4x faster but accuracy drops to 95%, that might be too much drop for some applications, but acceptable for others that value speed. It&#039;s application-dependent.&lt;br /&gt;
&lt;br /&gt;
In critical applications, one might maintain higher precision accumulators or run certain validation steps in high precision to ensure nothing drifted. In research, sometimes a model is trained in mixed precision but validated in double to ensure any small numeric differences don’t influence the reported result (rare, but for scientific computing integration maybe).&lt;br /&gt;
&lt;br /&gt;
In the ONNX world, performance-vs-accuracy is often managed via &#039;&#039;&#039;Execution Providers&#039;&#039;&#039; (EPs) that target specific hardware. For example, the TensorRT EP in ONNX Runtime can take an ONNX model and run it using NVIDIA&#039;s high-performance inference library, which will use FP16 or INT8 if allowed, to maximize speed. The user can configure the EP with precision flags. If the accuracy drop is too high, one might restrict it to FP32. If it&#039;s fine, allow FP16. Tools exist to measure the accuracy difference (like calibrating int8 quantization).&lt;br /&gt;
&lt;br /&gt;
To conclude, the trade-offs revolve around picking the lowest precision that yields acceptable accuracy, and utilizing hardware features fully. ONNX being a high-level spec abstracts this, but it&#039;s ultimately up to the deployment to decide and sometimes up to the exporter to incorporate quantization. ONNX’s role is to be able to &#039;&#039;represent&#039;&#039; these choices (they have added quantized types, FP8 types, etc., to allow the graph to include those when needed).&lt;br /&gt;
&lt;br /&gt;
== Best Practices and Design Guidelines ==&lt;br /&gt;
&lt;br /&gt;
Given the above insights, here are some best practices and guidelines for dealing with numerical precision and accumulators in AI inference (and training) design:&lt;br /&gt;
&lt;br /&gt;
=== 8.1. Choosing an Accumulator Precision ===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Match precision to data length and range:&#039;&#039;&#039; If an operation accumulates a large number of terms, consider using a higher precision accumulator than the input data. For example, for summing &amp;gt;1000 values of type FP16, an FP32 accumulator is recommended to avoid large rounding error or overflow. If summing &amp;gt; million values of FP32, consider double (FP64) accumulation if high accuracy is needed (though this is rare in DL). &#039;&#039;Rule of thumb:&#039;&#039; if $N \cdot \varepsilon$ is not tiny (say &amp;gt;1e-3), you may benefit from higher precision. For FP16 ($\varepsilon \approx 1e-3$), that threshold N is a few hundred. For FP32 ($\varepsilon \approx 1e-7$), N would be in the millions before 1e-3 error.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Use higher precision in critical layers:&#039;&#039;&#039; Identify if certain layers of the model are more sensitive to precision. For instance, the output layer or a layer that feeds into a very sharp decision boundary might require more precision. If a small change there could flip the prediction, you don&#039;t want excessive numerical error. Using FP32 for logits and softmax is a common practice to maintain classification confidence stability.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Follow hardware defaults, but override if needed:&#039;&#039;&#039; Many hardware accelerators have sensible defaults (like accumulate in FP32 for tensor ops). It&#039;s usually best to use those defaults as they were chosen to balance accuracy and speed. Only override if you have evidence of a problem. For example, if using ONNX Runtime on CPU, you discovered it upcasts to float for you – great. But if using some custom accelerator that does pure FP16, you might manually insert casts in the ONNX graph around certain ops to force higher precision on those ops.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Maintain precision for gradient accumulation (training):&#039;&#039;&#039; This is more training-oriented, but worth noting: when summing gradients over batches or updating weights, use higher precision (FP32 or even 64) for those accumulations. This ensures training doesn&#039;t diverge due to numeric error. Many frameworks keep a &amp;quot;master copy&amp;quot; of weights in FP32 when training in FP16&amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;. For inference, this translates to: any running statistics or counters in the model (like if you had a running average inside the model) should probably be in higher precision to avoid drift.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Be mindful of intermediate cast costs:&#039;&#039;&#039; If you do decide to cast to higher precision for an operation in ONNX, remember it incurs memory copies and compute overhead. Make sure it&#039;s worth it. For example, if you cast a huge tensor to double, that could slow things drastically. Usually FP16-&amp;gt;FP32 is fine (hardware handles it well), but FP32-&amp;gt;FP64 might be expensive on GPUs.&lt;br /&gt;
&lt;br /&gt;
=== 8.2. Safe Reduction Lengths and Heuristics ===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Chunk long reductions:&#039;&#039;&#039; If you must do a very long sum in low precision (due to memory constraints or hardware), chunk the sum into parts, sum each to a higher precision local accumulator, then sum the partial sums. This is essentially pairwise summation. Many libraries use a fixed chunk size heuristic (e.g., sum in blocks of 256 in registers, then add those up). This bounds the error growth. If implementing manually, you can choose a chunk size such that chunk_length &#039;&#039; ε is small (like 256 &#039;&#039; 1e-3 = 0.256 for FP16, hmm maybe choose 100).&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Sort by magnitude if possible:&#039;&#039;&#039; In some cases (not typical in DL, but possible in post-processing), if you have numbers varying by orders of magnitude, summing from smallest to largest improves accuracy. It prevents small ones from being swallowed by big ones. This is more relevant if you output a sum of mixed-scale values (like some algorithm mixing big and small contributions). It’s not always feasible in real-time inference to sort, but keep in mind.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Use compensation for critical sums:&#039;&#039;&#039; If you find a particular sum or difference in your inference computation is crucial (like computing a small residual from two close numbers), consider using Kahan summation or another compensated scheme just for that part. For example, if you had an ONNX custom function that needs to sum a lot of almost cancelling terms, you might implement it with compensation internally.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Heuristics for FP8/low-bit:&#039;&#039;&#039; When using extremely low precision like FP8, adopt the recommended practices: use scaling (don&#039;t feed raw data into FP8, normalize it first), and perhaps only use FP8 on portions of the network that are empirically robust. The industry is still establishing heuristics, but one might be: use FP8 for matrix multiplications, but accumulate in FP16 or FP32; keep normalization in FP16; use FP16 for first and last layer maybe. This is evolving, so keep an eye on literature.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Keep an eye on sum length in softmax and normalization:&#039;&#039;&#039; If you have extremely large class counts or group sizes (e.g., a softmax over 1e6 items, or layer norm over tens of thousands of features), realize this is a giant reduction. You may want to ensure that reduction is done in higher precision. ONNX Softmax by default will likely not handle a million-class case in FP16 well (the sum would overflow since 1e6 &amp;gt; 65504). So either do that on CPU in FP32 or break it down (could do hierarchical softmax, etc.). This is a corner case, but relevant for huge vocab language models. In practice, vocab ~50k is fine as we said.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Test numeric differences:&#039;&#039;&#039; A practical tip: after quantizing or reducing precision, run some test inputs through both the high-precision model and low-precision model. Compare outputs (and maybe some intermediate signals if possible). If differences are consistently small relative to the application tolerance, you&#039;re good. If you see occasional large differences or instabilities (like outputs become NaN or Inf in low precision), that&#039;s a red flag. Use that testing to guide where you might need to increase precision or change algorithms.&lt;br /&gt;
&lt;br /&gt;
=== 8.3. When Higher Precision Is Required ===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Ill-conditioned calculations:&#039;&#039;&#039; If your model or pipeline performs any operation known to be ill-conditioned (matrix inversion, subtraction of nearly equal quantities, computing a tiny difference of large numbers), you likely need higher precision for that part. For example, if your ONNX model includes a custom op to solve a linear system, use double precision in that op if possible (or at least float with iterative refinement). In ML, this might occur in something like a physics-informed neural network or a differentiable simulation within the model.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Strict accuracy requirements:&#039;&#039;&#039; Some applications (medical, aerospace, finance) might have strict tolerances. If an inference model’s output needs to be correct to 5 decimal places, you probably cannot use FP16, which has only 3-4 decimal digits precision. FP32 might be barely enough (~7 digits). FP64 might be warranted to guarantee that level of accuracy in worst case. Most deep learning applications (like image classification) don&#039;t demand that level of numeric precision in outputs – as long as the top class is correct, it doesn&#039;t matter if its probability was 0.901 vs 0.899. But if you&#039;re doing something like summing probabilities to make a risk estimate or doing a cumulative sum over many predictions, be careful to accumulate precisely.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Large sums or products in post-processing:&#039;&#039;&#039; Sometimes the neural net is fine, but the &#039;&#039;post-processing&#039;&#039; of results might need precision. E.g., summing up probabilities of many events, or computing confidence intervals, etc. Do those calculations in double if needed outside the model.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Preventing drift in long sequences:&#039;&#039;&#039; For sequential models (RNNs processing thousands of steps), rounding error can accumulate over steps. If each step has a small error, after 10000 steps it could add up. In such cases, using a higher precision for the state might reduce drift. Some practitioners use FP64 for the hidden state accumulation in very long sequence processing to maintain numerical stability (though this is not common due to speed loss). Alternatively, periodically renormalize or reset small errors.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Simulations or differential equations:&#039;&#039;&#039; If your ONNX model involves simulating something (like a differential equation solver inside a model, or an ODE/RNN hybrid), those often need double precision to be stable for long durations. Recognize those situations and allocate precision accordingly.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Verification and debugging:&#039;&#039;&#039; Use higher precision as a tool to verify correctness. For example, run your model in FP64 (if possible) and compare to FP32 to see if there are differences. If not, FP32 is fine. If yes, you might find a particular calculation losing too much info in FP32, implying maybe you should keep it in FP64 in production as well. This is rare in inference but can be part of rigorous validation.&lt;br /&gt;
&lt;br /&gt;
In ONNX, using higher precision may involve casting to &amp;lt;code&amp;gt;tensor(double)&amp;lt;/code&amp;gt; for certain ops. Not all runtimes support double on GPU though (some do via emulation or just slower path). ONNX CPU does support double for general ops. So be mindful of the target.&lt;br /&gt;
&lt;br /&gt;
Finally, &#039;&#039;&#039;document your precision choices&#039;&#039;&#039;. If you deliver an ONNX model that uses mixed types (float16 inputs, float32 accumulations, int8 weights, etc.), ensure that is clear to whoever maintains or uses it. Precision issues can be sneaky, so clarity helps.&lt;br /&gt;
&lt;br /&gt;
== Limitations and Open Problems ==&lt;br /&gt;
&lt;br /&gt;
Despite advances in handling numerical precision, there remain limitations and open challenges in this domain:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Lack of Precision Specification in ONNX:&#039;&#039;&#039; As noted, ONNX currently has no way to formally specify that an op &#039;&#039;should&#039;&#039; use a higher precision internally. This can lead to nondeterministic differences across platforms&amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;. While proposals exist to add attributes for this&amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;, it is not yet standard. Thus an ONNX model might behave slightly differently on two compliant backends, and there&#039;s no built-in mechanism to ensure identical results. This is a limitation for applications requiring cross-platform consistency. Solving it may involve extending ONNX (e.g., an attribute like &amp;lt;code&amp;gt;accumulatePrecision=&amp;quot;FLOAT32&amp;quot;&amp;lt;/code&amp;gt; for MatMul) or tightening the spec, but those come with backward compatibility and implementation burden concerns.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Reproducibility and Determinism:&#039;&#039;&#039; Even with the same precision, parallel computation can introduce nondeterminism (due to different summation orders). Ensuring bit-identical results across runs (important for some sensitive domains) is non-trivial. Frameworks often provide &amp;quot;deterministic mode&amp;quot; which might use fixed ordering at cost of speed. In ONNX, there&#039;s no global flag for this; it&#039;s up to the runtime. More research could be done on algorithms that produce bit-wise identical results independent of parallel execution order (perhaps using techniques like pairwise summation or consistent partitioning). Stochastic rounding is another area: sometimes using probabilistic rounding can reduce bias in accumulated error, but it introduces randomness – reconciling that with reproducibility is tricky.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Ultra-low Precision (FP8 and below):&#039;&#039;&#039; While FP8 is being explored, going to 4-bit floats or mixed schemes is an open challenge. The OpenAI community is investigating 4-bit (quantization-wise) for LLMs with some success, but a true 4-bit float (like E3M1) would be extremely limited. Perhaps hybrid schemes (like block floating point, log scales, etc.) will come. ONNX already has some support (they mention 4-bit in documentation placeholders&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;), but it&#039;s nascent. Research into training techniques, calibration, and new number formats (like &#039;&#039;&#039;posit numbers&#039;&#039;&#039; or tapering precision floats) is ongoing. If a new format proves useful, ONNX will need to adapt to represent it.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Quantization Noise and Model Robustness:&#039;&#039;&#039; Reducing precision can sometimes expose models to issues like adversarial vulnerability or fragility – e.g., a slight perturbation might flip a decision if the margin was small and precision loss nudged it. Ensuring model robustness under quantization is an open problem. It might require re-training the model with quantization in the loop (quantization-aware training) or designing models that have large &amp;quot;margin&amp;quot; between classes to tolerate quantization noise.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Dynamic range during training vs inference:&#039;&#039;&#039; In training, activations can have wider distributions (especially early in training). Models often learn to constrain their ranges (via activation functions saturating or normalization), which makes inference more stable in lower precision. But if one tries to train in FP8 from scratch, it&#039;s very hard. This gap between what&#039;s possible in a post-trained inference model vs training is an open area – how to train directly in low precision without significant accuracy loss. Solving that could simplify workflows (no need for high-precision training then quantize; just train low-prec end to end). Techniques like loss scaling, gradient clipping, etc., are partial solutions. The development of optimizers that are resilient to low precision is ongoing.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Hardware variability:&#039;&#039;&#039; As new hardware emerges (with new numeric capabilities or quirks), ensuring that ONNX models run consistently is an ongoing task. For instance, one device might implement a fused op that reduces rounding error, another does it separately. Even something like convolution might use a different algorithm (FFT-based vs direct) which has different numeric properties. Open problem: could ONNX allow specifying numeric tolerances per op or preferred algorithms? That may be overkill for most, but for some HPC ML use cases, maybe desired.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Testing and validation of numerics:&#039;&#039;&#039; There isn&#039;t a standardized set of &amp;quot;numeric compliance&amp;quot; tests for ONNX runtimes beyond simple correctness. Perhaps in the future, test suites might include challenging numeric scenarios (large sums, nearly singular matrices) to ensure a runtime handles them gracefully (maybe by switching precision). Right now, it&#039;s mostly on the user to test their model under expected conditions.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Combining quantization with floating point:&#039;&#039;&#039; Many production models use a mix (e.g., int8 for some layers, FP16 for others). ONNX can represent this (with QuantizeLinear ops or MixedPrecision through casting), but tooling to automatically find the optimal mix is still developing. It&#039;s somewhat trial-and-error or using heuristics. An open question is how to automate precision assignment: given a model and a target hardware, how to automatically choose which layers can be int8, which should be FP16, etc., to meet an accuracy target. This crosses into NAS (neural architecture search) territory or automated quantization. &lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Numerical debugging tools:&#039;&#039;&#039; When a model goes wrong due to precision, it&#039;s often hard to pinpoint where. Better tooling to analyze numerical error layer by layer could be useful. For instance, one could run a model in double and float and diff the intermediate results to see where the largest divergence happens. Doing this systematically for big nets is an open tooling challenge. Some frameworks have hooks for this, but not standardized.&lt;br /&gt;
&lt;br /&gt;
In conclusion, while we&#039;ve come far with using lower precision in AI without sacrificing much accuracy, the landscape is always evolving. The key open problem is how to push precision lower (for efficiency gains) without crossing the threshold where accuracy falls off a cliff. Each new generation of hardware and techniques (like FP8, novel quantization schemes) extends that boundary, but also brings new questions on how to manage the numerical errors. ONNX as an interchange will need to keep pace, possibly by adding more expressive power for these numeric considerations.&lt;br /&gt;
&lt;br /&gt;
== See Also ==&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;IEEE 754 Floating-Point&#039;&#039;&#039; – The standard defining binary16, binary32, binary64 formats and rounding behavior.&lt;br /&gt;
* &#039;&#039;&#039;Floating-Point Arithmetic&#039;&#039;&#039; – General topic on how floating-point math works, common issues (overflow, underflow, cancellation).&lt;br /&gt;
* &#039;&#039;&#039;Kahan Summation Algorithm&#039;&#039;&#039; – Algorithm for compensated summation to reduce numerical error&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;.&lt;br /&gt;
* &#039;&#039;&#039;BFloat16&#039;&#039;&#039; – A short-format float with 8-bit exponent, used in TPUs and some GPUs for training networks.&lt;br /&gt;
* &#039;&#039;&#039;Mixed Precision Training&#039;&#039;&#039; – Technique of using FP16 (or BF16) along with FP32 accumulators to accelerate training&amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;.&lt;br /&gt;
* &#039;&#039;&#039;Quantization in Deep Learning&#039;&#039;&#039; – Converting networks to lower bit integer arithmetic for inference (INT8 quantization and calibration techniques).&lt;br /&gt;
* &#039;&#039;&#039;ONNX Operator Specification&#039;&#039;&#039; – Official docs for ONNX ops, which describe their mathematical function but not the required numeric precision (a point of potential extension in future).&lt;br /&gt;
* &#039;&#039;&#039;CUDA Tensor Cores &amp;amp; TF32&#039;&#039;&#039; – NVIDIA documentation on how tensor cores perform mixed precision and the TF32 format for accelerating FP32.&lt;br /&gt;
* &#039;&#039;&#039;Pairwise Summation&#039;&#039;&#039; – Summation algorithm with logN error growth&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;, used in many numerical libraries to improve accuracy of reductions.&lt;br /&gt;
* &#039;&#039;&#039;Softmax Stability&#039;&#039;&#039; – Articles on numerically stable softmax and log-sum-exp implementations to avoid overflow.&lt;br /&gt;
* &#039;&#039;&#039;Performance Tuning for Low Precision&#039;&#039;&#039; – Guides (e.g., NVIDIA or Intel whitepapers) on using low precision and the trade-offs involved in various hardware.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;references&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref1&amp;quot;&amp;gt;Accelerate and simplify Scikit-learn model inference with ONNX Runtime - Microsoft Open Source Blog — opensource.microsoft.com — https://opensource.microsoft.com/blog/2020/12/17/accelerate-simplify-scikit-learn-model-inference-onnx-runtime&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref2&amp;quot;&amp;gt;Floating Point Precision: Understanding FP64, FP32, and FP16 in Large Language Models - DEV Community — dev.to — https://dev.to/lukehinds/floating-point-precision-understanding-fp64-fp32-and-fp16-in-large-language-models-3gk6&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref3&amp;quot;&amp;gt;Floating Point Precision and Its Limitations | by Umair Akbar | Medium — akbu.medium.com — https://akbu.medium.com/floating-point-precision-and-its-limitations-cfb7247d7789&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref4&amp;quot;&amp;gt;Kahan summation algorithm - Wikipedia — en.wikipedia.org — https://en.wikipedia.org/wiki/Kahan_summation_algorithm&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref5&amp;quot;&amp;gt;Pairwise summation - Wikipedia — en.wikipedia.org — https://en.wikipedia.org/wiki/Pairwise_summation&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref6&amp;quot;&amp;gt;Numerical behavior of NVIDIA tensor cores - PMC — pmc.ncbi.nlm.nih.gov — https://pmc.ncbi.nlm.nih.gov/articles/PMC7959640/&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref7&amp;quot;&amp;gt;Mixed precision and performance-accuracy tuning (neuron-cc) — AWS Neuron Documentation — awsdocs-neuron.readthedocs-hosted.com — https://awsdocs-neuron.readthedocs-hosted.com/en/latest/about-neuron/appnotes/neuron-cc/mixed-precision.html&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref8&amp;quot;&amp;gt;Create Float16 and Mixed Precision Models - ONNX Runtime — onnxruntime.ai — https://onnxruntime.ai/docs/performance/model-optimizations/float16.html&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref9&amp;quot;&amp;gt;[Performance] ONNX FP16 model is having performance bottle neck when compared to FP32 variant · Issue #25824 · microsoft/onnxruntime · GitHub — github.com — https://github.com/microsoft/onnxruntime/issues/25824&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref10&amp;quot;&amp;gt;Float stored in 8 bits - ONNX 1.21.0 documentation — onnx.ai — https://onnx.ai/onnx/technical/float8.html&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref11&amp;quot;&amp;gt;[2209.05433] FP8 Formats for Deep Learning — arxiv.org — https://arxiv.org/abs/2209.05433&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref12&amp;quot;&amp;gt;Should MatMul op allow specification of accumulation precision? · Issue #7072 · onnx/onnx · GitHub — github.com — https://github.com/onnx/onnx/issues/7072&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref13&amp;quot;&amp;gt;Quantize ONNX models | onnxruntime — onnxruntime.ai — https://onnxruntime.ai/docs/performance/model-optimizations/quantization.html&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref14&amp;quot;&amp;gt;Keep Attention Softmax FP32 during FP16/ZeRO Training · Issue #1485 · hpcaitech/ColossalAI · GitHub — github.com — https://github.com/hpcaitech/ColossalAI/issues/1485&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;/references&amp;gt;&lt;/div&gt;</summary>
		<author><name>Timo.stripf</name></author>
	</entry>
	<entry>
		<id>https://www.emmtrix.com/w139/index.php?title=Numerical_Precision_in_ONNX_and_AI_Inference&amp;diff=2892</id>
		<title>Numerical Precision in ONNX and AI Inference</title>
		<link rel="alternate" type="text/html" href="https://www.emmtrix.com/w139/index.php?title=Numerical_Precision_in_ONNX_and_AI_Inference&amp;diff=2892"/>
		<updated>2026-01-30T15:20:09Z</updated>

		<summary type="html">&lt;p&gt;Timo.stripf: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&#039;&#039;&#039;Open Neural Network Exchange (ONNX)&#039;&#039;&#039; is an open standard format for representing machine learning models and neural network computations across different frameworks and hardware&amp;lt;ref name=&amp;quot;ref1&amp;quot;/&amp;gt;. As models are exported and deployed via ONNX, the &#039;&#039;&#039;numerical precision&#039;&#039;&#039; of computations becomes critical. Deep learning inference involves a variety of floating-point operations, and small numerical discrepancies can accumulate and affect model accuracy or reproducibility. This article provides an in-depth overview of floating-point precision issues in numerical computing, with a focus on how they manifest in AI inference and the ONNX ecosystem. We cover floating-point formats and rounding behavior, accumulation strategies for summations, the use of low-precision formats (FP16, BFloat16, FP8) in modern AI, and how ONNX defines (or leaves unspecified) the precision of accumulators in operations. Strategies for maintaining numerical stability in deep neural networks are discussed, along with the trade-offs between computational performance and arithmetic accuracy. The goal is to give engineers and researchers a comprehensive, encyclopedic reference on numerical precision in ONNX-based AI inference systems.&lt;br /&gt;
&lt;br /&gt;
== Floating-Point Precision in Numerical Computing ==&lt;br /&gt;
&lt;br /&gt;
=== 2.1. IEEE 754 Floating-Point Formats ===&lt;br /&gt;
&lt;br /&gt;
Modern computers represent real numbers using the IEEE 754 floating-point standard, which defines formats like 32-bit &#039;&#039;single precision&#039;&#039; (FP32) and 64-bit &#039;&#039;double precision&#039;&#039; (FP64). In IEEE 754 binary formats, a floating-point number is composed of three fields: 1 sign bit, several exponent bits (with a bias), and several fraction (mantissa) bits. For example, FP32 consists of 1 sign bit, 8 exponent bits, and 23 fraction bits (24 bits of significand precision including the implicit leading 1)&amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;. This provides roughly 7 decimal digits of precision and an exponent range allowing values on the order of $10^{\pm38}$. FP64 allocates 1 sign bit, 11 exponent bits, and 52 fraction bits (53-bit precision), yielding about 15–16 decimal digits of precision and a vastly larger range (~$10^{\pm308}$)&amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;. These larger formats (especially FP64) are the &amp;quot;gold standard&amp;quot; for numerical accuracy in scientific computing, as they reduce round-off error in complex calculations&amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;. However, the increased precision comes at the cost of doubled storage and typically reduced computational throughput (e.g. many GPUs execute FP64 instructions at a much lower rate than FP32&amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
In addition to single and double precision, the IEEE 754-2008 standard introduced a 16-bit &#039;&#039;half precision&#039;&#039; format (FP16, or binary16). FP16 has 1 sign bit, 5 exponent bits, and 10 fraction bits (11-bit significand including the implicit 1)&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. This format covers a much smaller dynamic range (approximately $6.1\times10^{-5}$ to $6.5\times10^4$ for normalized values&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;) and offers only around 3 decimal digits of precision. Half precision was originally used in graphics, but it has gained popularity in machine learning for its speed and memory advantages (discussed later in Section 4). Another format relevant to AI is &#039;&#039;&#039;BFloat16&#039;&#039;&#039; (Brain Float 16), a 16-bit float with &#039;&#039;&#039;8 exponent bits and 7 fraction bits&#039;&#039;&#039;&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. BFloat16 sacrifices precision (only ~7 fraction bits ≈ 2–3 decimal digits) in order to have the &#039;&#039;&#039;same exponent range as FP32&#039;&#039;&#039;&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. This means BFloat16 can represent very large or very small numbers (∼$10^{\pm38}$, similar range as FP32) but with much less precision between representable values&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. BFloat16 was introduced for deep learning by Google (for TPUs) to allow reduced precision &#039;&#039;training&#039;&#039; without frequent overflow/underflow, relying on the wide range and accepting the coarse precision.&lt;br /&gt;
&lt;br /&gt;
Each floating-point format has a &#039;&#039;&#039;unit roundoff&#039;&#039;&#039; or machine precision, denoted $\epsilon$. This is effectively the spacing between 1.0 and the next representable number. For FP32, $\epsilon \approx 2^{-23} \approx 1.19\times10^{-7}$, corresponding to about 7–8 decimal digits of precision&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. In FP64, $\epsilon = 2^{-52} \approx 2.22\times10^{-16}$ (15–16 decimal digits). Half precision is much less precise: FP16 has $\epsilon = 2^{-10} \approx 9.76\times10^{-4}$&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;, and BFloat16 has $\epsilon = 2^{-7} = 7.8125\times10^{-3}$&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. These values quantify how finely the continuum of real numbers is quantized in each format.&lt;br /&gt;
&lt;br /&gt;
=== 2.2. Machine Epsilon and Rounding Error ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Machine epsilon ($\varepsilon$)&#039;&#039;&#039; is formally defined as the smallest positive number such that $1.0 + \varepsilon &amp;gt; 1.0$ in the floating-point format&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. It effectively measures the &#039;&#039;&#039;precision&#039;&#039;&#039; of the representation. In FP32, $\varepsilon = 2^{-23} \approx 1.19\times10^{-7}$, meaning any result is rounded to about 7 decimal digits. FP16’s $\varepsilon = 2^{-10} \approx 9.77\times10^{-4}$, over 1000× larger (so only ~3 decimal digits of precision), and BFloat16’s $\varepsilon = 2^{-7} \approx 7.8\times10^{-3}$, reflecting its mere ~8-bit precision&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. Rounding in IEEE 754 by default uses &#039;&#039;&#039;round-to-nearest (ties to even)&#039;&#039;&#039;, which ensures the rounding error for any single operation is at most half a unit in the last place (0.5 ULP). We can model a floating-point addition as: &lt;br /&gt;
&lt;br /&gt;
$$\text{fl}(a+b) = (a+b)\,(1 + \delta),$$ &lt;br /&gt;
&lt;br /&gt;
where $|\delta| \le \varepsilon$ for that format&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. Here $\varepsilon/2$ is the maximum &#039;&#039;&#039;relative error&#039;&#039;&#039; in one rounding (also called unit roundoff). For example, when adding two FP32 numbers, the result is accurate to within about $10^{-7}$ of the true sum (relative), whereas in BF16, the result can deviate by up to $\sim7.8\times10^{-3}$ (nearly 0.78% relative error per operation)&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. Thus, &#039;&#039;&#039;each arithmetic operation incurs a small rounding error&#039;&#039;&#039; (on the order of machine epsilon) that can propagate or accumulate through a sequence of calculations.&lt;br /&gt;
&lt;br /&gt;
Because floating-point representations have finite exponent range, we must also consider &#039;&#039;&#039;overflow&#039;&#039;&#039; and &#039;&#039;&#039;underflow&#039;&#039;&#039;. If a result’s magnitude exceeds the maximum representable value, it overflows to infinity; if it falls below the minimum normal (or minimum subnormal) value, it underflows to zero (or a denormalized tiny value). FP32 and BF16 share an exponent size (8 bits), so their range for normal numbers is roughly $[10^{-38}, 10^{38}]$ (approx $~\!3.4\times10^{38}$ max)&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. FP16’s 5-bit exponent allows max $\sim 6.55\times10^4$ and min normal $\sim 6.10\times10^{-5}$&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. In practice, &#039;&#039;&#039;underflow/overflow&#039;&#039;&#039; can occur in low-precision arithmetic if values are not appropriately scaled. For instance, summing a large number of moderately sized terms in FP16 could overflow where FP32 would not. As an example, summing on the order of $10^4$ values of magnitude ~$10^1$ (say, adding 10,000 terms around 10 each) would produce a total $\sim10^5$, which exceeds FP16’s max finite value ~$6.5\times10^4$ and would overflow to $+\infty$ in half precision&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. Underflow is usually a concern when subtracting nearly equal numbers (catastrophic cancellation yielding a tiny difference that might flush to zero in low precision).&lt;br /&gt;
&lt;br /&gt;
=== 2.3. Accumulation Error in Reductions ===&lt;br /&gt;
&lt;br /&gt;
Floating-point addition is &#039;&#039;&#039;not associative&#039;&#039;&#039;: $(a+b)+c$ can differ from $a+(b+c)$ due to rounding, and thus the order in which we sum a list of numbers can change the result slightly. When summing a large sequence of $N$ numbers $x_1, x_2, ..., x_N$, the &#039;&#039;&#039;accumulated rounding error&#039;&#039;&#039; can be significant. In the &#039;&#039;worst case&#039;&#039; (e.g., adding many positive and negative terms that cause maximal cancellation or always rounding one way), the error can grow on the order of $O(N \varepsilon)$ relative to the exact sum&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;. Simply summing $N$ numbers in sequence (the &#039;&#039;&#039;naive linear summation&#039;&#039;&#039;) has a worst-case error bound proportional to $N\,\varepsilon$&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. This worst-case occurs for adversarial arrangements of values; in more typical scenarios with rounding errors of random signs, one can model the error as a random walk, yielding an expected root-mean-square (RMS) error growth on the order of $O(\sqrt{N}\,\varepsilon)$&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;. Still, with very large $N$, even $\sqrt{N}$ times $\varepsilon$ can become large enough to matter.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Cancellation&#039;&#039;&#039; is a particular problem: if we add two nearly equal numbers of opposite sign, most leading digits cancel and the result loses significant bits of precision. For example, in single precision, subtracting two numbers that agree in the first 6 decimal digits will yield a result with only ~1 digit of accuracy left. Catastrophic cancellation can dramatically increase relative error. Summation is especially sensitive to cancellation if large positive and negative terms are summed naively. The &#039;&#039;&#039;condition number&#039;&#039;&#039; of a summation (the ratio of the sum of absolute values $\sum|x_i|$ to the absolute value of the true sum $|\sum x_i|$) indicates how sensitive the result is to perturbations&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;. If the numbers have mixed signs and nearly cancel out (making the condition number large), even an optimal summation order will have a large relative error. For instance, summing [1.0, $10^{100}$, 1.0, $-10^{100}$] in double precision has an exact sum of 2.0, but naive summation may yield 0.0 due to cancellation; an improved summation algorithm yields the correct 2.0&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
In practical terms, when summing a long list of floating-point numbers (such as accumulating a dot product or reducing a tensor), the &#039;&#039;&#039;accumulation error&#039;&#039;&#039; can become noticeable if no precautions are taken. Techniques to mitigate this error include carefully choosing the summation order or using higher precision for intermediate sums. We will explore these strategies in the next section.&lt;br /&gt;
&lt;br /&gt;
== Accumulators and Reduction Strategies ==&lt;br /&gt;
&lt;br /&gt;
When performing reductions (sums, dot products, etc.), the method of accumulation can greatly affect the accuracy of the result. Below, we discuss various strategies:&lt;br /&gt;
&lt;br /&gt;
=== 3.1. Naive Linear Accumulation ===&lt;br /&gt;
&lt;br /&gt;
The simplest accumulation strategy is to use a single accumulator and add each element sequentially. In C-like pseudocode:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;c&amp;quot;&amp;gt;&lt;br /&gt;
float sum = 0.0f;&lt;br /&gt;
for (int i = 0; i &amp;lt; N; ++i) {&lt;br /&gt;
    sum += array[i];&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This &#039;&#039;&#039;naive linear summation&#039;&#039;&#039; adds values one by one to the running total. While straightforward and fast, it is prone to the rounding error accumulation described earlier. The partial sum &amp;lt;code&amp;gt;sum&amp;lt;/code&amp;gt; continually grows (in absolute value) as terms are added, so new small terms may fall outside its precision. For example, if &amp;lt;code&amp;gt;sum&amp;lt;/code&amp;gt; is large and positive, adding a comparatively tiny positive number may produce no change because the small term&#039;s significant digits vanish in the presence of the large sum (the increment is below the rounding threshold of &amp;lt;code&amp;gt;sum&amp;lt;/code&amp;gt;). Conversely, adding a small number of opposite sign can suffer from cancellation. The order of addition matters: summing from smallest magnitude to largest (ascending order) tends to yield a more accurate result than summing in descending order, because adding small terms first preserves their contributions before the sum becomes very large. The naive loop above implicitly sums in the given index order; if the data is unsorted, this order is essentially arbitrary.&lt;br /&gt;
&lt;br /&gt;
The performance of linear accumulation is excellent (minimal overhead), and for many applications the resulting error is within acceptable limits. However, for &#039;&#039;&#039;very large N&#039;&#039;&#039; or ill-conditioned sums, the inaccuracy can become a problem. In a reduction involving millions of elements, a single-precision sum could lose several digits of accuracy. In deep learning, this could translate to small discrepancies in layer outputs or aggregated gradients if done naively in low precision.&lt;br /&gt;
&lt;br /&gt;
=== 3.2. Pairwise and Tree-Based Reductions ===&lt;br /&gt;
&lt;br /&gt;
A better approach to summation is &#039;&#039;&#039;pairwise summation&#039;&#039;&#039;, which sums numbers in a binary tree structure rather than linearly. The idea is to recursively split the array into halves, sum each half, then add the two partial sums. This can be implemented as:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;!--&lt;br /&gt;
&amp;lt;syntaxhighlight&amp;gt;&lt;br /&gt;
function pairwise_sum(x[1..n]):&lt;br /&gt;
    if n == 0:&lt;br /&gt;
        return 0&lt;br /&gt;
    if n == 1:&lt;br /&gt;
        return x[1]&lt;br /&gt;
    m = floor(n/2)&lt;br /&gt;
    left_sum  = pairwise_sum(x[1..m])&lt;br /&gt;
    right_sum = pairwise_sum(x[m+1..n])&lt;br /&gt;
    return left_sum + right_sum&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
--&amp;gt;&lt;br /&gt;
&lt;br /&gt;
By always adding numbers of similar magnitudes first (small subsets are reduced to subtotals, then those are added, etc.), pairwise summation avoids the situation of a very large running total absorbing small additions until the final steps. The error of pairwise summation grows logarithmically with $N$ in the worst case (roughly $O(\varepsilon \log_2 N)$)&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;, instead of linearly. Intuitively, at each level of the tree, rounding error is constrained, and there are about $\log_2 N$ levels. In fact, the worst-case relative error bound for pairwise summation (base case of 1 element) is:&lt;br /&gt;
&lt;br /&gt;
\[ |E_N| \le \frac{\varepsilon \log_2 N}{1 - \varepsilon \log_2 N} \sum_{i=1}^N |x_i|, \]&lt;br /&gt;
&lt;br /&gt;
which for practical $N$ simplifies to on the order of $\varepsilon \log_2 N$ times the condition number of the sum&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;. In typical random-error scenarios, the error behaves even better (on the order of $\sqrt{\log N}$)&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;. The key is that &#039;&#039;&#039;pairwise (tree) reduction balances the accumulations&#039;&#039;&#039;, preventing one sum from overwhelming the others too early.&lt;br /&gt;
&lt;br /&gt;
From a performance standpoint, pairwise summation has the same $O(N)$ total additions as linear summation, just a different order. It is also highly amenable to parallelization: independent partial sums can be computed in parallel at each level of the tree. Many numerical libraries and BLAS implementations use a form of blocked or pairwise summation in dot products for improved accuracy at negligible performance cost. For example, NumPy and Julia perform partial pairwise summation by default for better numerical behavior&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=== 3.3. Compensated Summation (Kahan, Neumaier) ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Compensated summation&#039;&#039;&#039; is an alternative technique to improve summation accuracy by tracking the small “lost” quantities. The most famous algorithm is the &#039;&#039;&#039;Kahan summation algorithm&#039;&#039;&#039; (also called Kahan–Babuška)&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. Kahan summation uses an extra variable to accumulate the round-off error. In pseudocode:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;c&amp;quot;&amp;gt;&lt;br /&gt;
double sum = 0.0;&lt;br /&gt;
double c = 0.0;    // compensation for lost low-order bits&lt;br /&gt;
for (i = 1; i &amp;lt;= N; ++i) {&lt;br /&gt;
    double y = x[i] - c;    // recover low-order bits by subtracting compensation&lt;br /&gt;
    double t = sum + y;     // perform the addition&lt;br /&gt;
    c = (t - sum) - y;      // compute new compensation (the error in t)&lt;br /&gt;
    sum = t;&lt;br /&gt;
}&lt;br /&gt;
return sum;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Here &amp;lt;code&amp;gt;c&amp;lt;/code&amp;gt; accumulates the tiny leftovers that would otherwise be dropped&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. Each iteration essentially subtracts the last error from the new addend (&amp;lt;code&amp;gt;y = x[i] - c&amp;lt;/code&amp;gt;) so that &amp;lt;code&amp;gt;t = sum + y&amp;lt;/code&amp;gt; includes the small bits from the previous operation. The new error is then stored in &amp;lt;code&amp;gt;c&amp;lt;/code&amp;gt; for the next iteration. The result is that the final &amp;lt;code&amp;gt;sum&amp;lt;/code&amp;gt; is as if computed with higher precision than the machine’s: Kahan’s algorithm can &#039;&#039;&#039;greatly reduce the error&#039;&#039;&#039; in summing a sequence of floats. In fact, with exact arithmetic for the compensation, Kahan summation yields a result accurate to within 1 or 2 ULP of the true sum, regardless of N (assuming the sequence of partial sums doesn’t overflow)&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. More precisely, Kahan summation achieves a &#039;&#039;&#039;worst-case error independent of N&#039;&#039;&#039; (error bound depends only on machine precision, not the number of terms)&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. In practice, using a double accumulator for summing single-precision numbers via Kahan can give near double-precision accuracy.&lt;br /&gt;
&lt;br /&gt;
An important subtlety is that the compensation variable &amp;lt;code&amp;gt;c&amp;lt;/code&amp;gt; should be kept in &#039;&#039;full precision&#039;&#039;. In the implementation above, we used &amp;lt;code&amp;gt;double&amp;lt;/code&amp;gt; for both sum and c. If summing FP32 values, one might use &amp;lt;code&amp;gt;float sum&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;float c&amp;lt;/code&amp;gt; (both 32-bit), which still improves accuracy, but using a higher precision for the accumulator and compensation (i.e. double) yields even better results. Many implementations of Kahan summation use the same type for sum and c, but it&#039;s assumed to be higher precision than the inputs when possible.&lt;br /&gt;
&lt;br /&gt;
Kahan’s algorithm carries a performance cost: each iteration does five arithmetic operations (two adds, one subtract, etc.) and has a data dependency, which can hinder parallelism or pipelining&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. Thus, it runs slower than naive summation (roughly 2–3× slower or more, depending on CPU pipeline). In loops where summation is a bottleneck, this may be significant. Nonetheless, for modest N or where maximum accuracy is required, compensated summation is extremely useful. In fact, Python’s &amp;lt;code&amp;gt;math.fsum&amp;lt;/code&amp;gt; function uses a variant of Kahan’s algorithm (actually a more advanced multi-part compensation) to sum iterables with high precision.&lt;br /&gt;
&lt;br /&gt;
There are variations and improvements on basic Kahan summation. &#039;&#039;&#039;Neumaier’s algorithm&#039;&#039;&#039; (sometimes called &#039;&#039;Kahan-Babuška-Neumaier&#039;&#039;) improves Kahan’s method by also handling cases where the next input is larger in magnitude than the running sum&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. In such cases, the original Kahan formula could suffer error; Neumaier’s version effectively swaps roles when a huge value comes in. Its pseudocode is:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight&amp;gt;&lt;br /&gt;
function NeumaierSum(array):&lt;br /&gt;
    sum = 0.0&lt;br /&gt;
    compensation = 0.0&lt;br /&gt;
    for each x in array:&lt;br /&gt;
        t = sum + x&lt;br /&gt;
        if (fabs(sum) &amp;gt;= fabs(x))&lt;br /&gt;
            compensation += (sum - t) + x;&lt;br /&gt;
        else&lt;br /&gt;
            compensation += (x - t) + sum;&lt;br /&gt;
        sum = t;&lt;br /&gt;
    return sum + compensation;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This algorithm ensures the compensation covers both scenarios: when the sum is the bigger addend or when the new term is bigger&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. In the end, it adds the final &amp;lt;code&amp;gt;compensation&amp;lt;/code&amp;gt; once to the result. Neumaier’s method can produce the correct result in cases where basic Kahan fails. For instance, summing [1.0, $+10^{100}$, 1.0, $-10^{100}$] in double precision: regular Kahan summation might give 0.0 (losing the small terms), whereas Neumaier’s algorithm yields 2.0 exactly&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. There are also higher-order compensation methods (carrying multiple compensation terms, e.g., the &#039;&#039;&#039;Klein summation&#039;&#039;&#039; variant&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt; or using arbitrary precision for intermediate sums) that further reduce error at increased computational cost&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
In summary, compensated summation algorithms trade extra computation for greatly improved numerical accuracy. They are especially valuable when summing large arrays of numbers with varying magnitudes, where naive summation could lose most of the low-order bits. Many scientific computing environments provide compensated sum routines for this reason. In deep learning, however, a different approach to improved precision is often taken: &#039;&#039;&#039;using a wider accumulator type&#039;&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
=== 3.4. Mixed-Precision Accumulation ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Mixed-precision accumulation&#039;&#039;&#039; refers to performing arithmetic in a higher precision than the input data. A common scenario is accumulating sums in 32-bit floats when the operands are 16-bit floats. Rather than implementing a compensation algorithm, one can simply use a wider accumulator so that intermediate rounding error is smaller. This is effectively what modern GPUs do for tensor operations: for example, NVIDIA &#039;&#039;&#039;Tensor Cores operate on FP16 input data with an FP32 accumulator&#039;&#039;&#039; (the 16-bit products are accumulated in 32-bit)&amp;lt;ref name=&amp;quot;ref6&amp;quot;/&amp;gt;. In this way, summing 2048 products in a dot product has far less precision loss than it would with a 16-bit accumulator. Hardware that supports mixed precision can give the best of both worlds: fast low-precision multiplies and high-precision accumulation.&lt;br /&gt;
&lt;br /&gt;
Software can also leverage mixed precision. For instance, one can sum an array of &amp;lt;code&amp;gt;float&amp;lt;/code&amp;gt; values into a &amp;lt;code&amp;gt;double&amp;lt;/code&amp;gt; variable, accumulating the result in double precision and then converting back to float at the end:&lt;br /&gt;
&lt;br /&gt;
This approach often yields a more accurate &amp;lt;code&amp;gt;result&amp;lt;/code&amp;gt; than summing in single precision directly, because the intermediate sum carries 52-bit precision. The final rounding to float happens only once at the end (minimizing overall rounding error). In fact, the above is one way to implement &amp;lt;code&amp;gt;math.fsum&amp;lt;/code&amp;gt; in Python (it internally does something similar with multiple partial sums in double precision).&lt;br /&gt;
&lt;br /&gt;
In machine learning training and inference, &#039;&#039;&#039;mixed precision&#039;&#039;&#039; is widely used. When using FP16 for speed, it&#039;s common to accumulate with FP32. For example, &#039;&#039;&#039;Google&#039;s TPUs use BFloat16 for multiplication but accumulate in FP32&#039;&#039;&#039; for matrix ops&amp;lt;ref name=&amp;quot;ref6&amp;quot;/&amp;gt;. AWS’s Inferentia (Neuron) hardware similarly supports FP16/BF16 matrix multiplication with FP32 accumulation&amp;lt;ref name=&amp;quot;ref7&amp;quot;/&amp;gt;. The motivation is that summing hundreds or thousands of half-precision values would otherwise introduce intolerable error or risk overflow. With an FP32 accumulator, the reduction error is drastically reduced (by about $2^{13}$ times, since FP32 has 13 more precision bits than FP16). Mixed-precision accumulation thus provides a simple and effective form of compensation: &#039;&#039;&#039;use a larger bucket to collect many small contributions&#039;&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
There is a spectrum of options: summing int8 or int16 integers might use 32-bit integers as accumulators for the same reason (to avoid overflow). Some BLAS libraries even offer &#039;&#039;extended precision accumulators&#039;&#039; for single precision dot products (accumulating in double). The downside of mixed precision is the extra memory and maybe throughput cost of the wider type. But on many platforms, the add latency is dominated by hardware design rather than bit-width, so using a 32-bit adder vs 16-bit has negligible speed impact if supported natively. The key is whether data movement or register pressure increases.&lt;br /&gt;
&lt;br /&gt;
In summary, &#039;&#039;&#039;using a higher precision accumulator is often the easiest way to improve numerical accuracy&#039;&#039;&#039; for reductions. It is a primary strategy in deep learning hardware to maintain stability while exploiting low precision for storage and initial computations.&lt;br /&gt;
&lt;br /&gt;
== Precision Formats in AI and Machine Learning ==&lt;br /&gt;
&lt;br /&gt;
Modern AI systems use a variety of numeric formats to balance speed, memory, and accuracy. We discuss common precisions and their roles:&lt;br /&gt;
&lt;br /&gt;
=== 4.1. FP32 and FP64 in Training and Inference ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;FP32 (single precision)&#039;&#039;&#039; has been the workhorse of deep learning for years. Most training scripts in frameworks like TensorFlow and PyTorch used FP32 by default, as it offers sufficient precision to propagate gradients and accumulate weight updates over millions of iterations without excessive error. With ~7 decimal digits of precision and a wide exponent range, FP32 can handle the dynamic range of activations and gradients in typical neural networks. In inference, FP32 is often more precision than strictly necessary for final predictions, but it has remained common because of its ease and the fact that many GPU operations were optimized for FP32.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;FP64 (double precision)&#039;&#039;&#039; is rarely used in deep learning model training or inference, except in specific scenarios requiring extreme precision. FP64 is common in scientific computing and certain types of numerical simulations. In neural networks, however, the extra precision is usually not needed for model accuracy—using doubles tends to yield negligible improvements in model metrics but at a large performance cost (since on GPUs FP64 throughput can be 1/2, 1/8, or even 1/32 of FP32 throughput depending on hardware). One exception might be when integrating neural nets into larger physics simulations or when solving ill-conditioned problems as part of the model. Some researchers use FP64 to diagnose numerical issues or ensure reproducibility. Generally though, deep learning has favored lower precision for efficiency, and FP64 is considered overkill for most networks&amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
That said, certain classical machine learning algorithms benefit from double precision. For example, computing the inverse of a matrix (as in a Gaussian Process regression or Kalman filter embedded in a model) can incur large rounding errors in single precision, so using FP64 can significantly improve the result&amp;lt;ref name=&amp;quot;ref1&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref1&amp;quot;/&amp;gt;. The ONNX standard supports a float64 tensor type for such needs. In ONNX Runtime, however, emphasis has been on FP32 performance, and some pipelines default to float even if the original model was double&amp;lt;ref name=&amp;quot;ref1&amp;quot;/&amp;gt;. If a model’s prediction computation involves operations that are numerically sensitive (like matrix inversion, Cholesky decomposition, or summing extremely large counts), then FP64 may be required to avoid noticeable discrepancies&amp;lt;ref name=&amp;quot;ref1&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref1&amp;quot;/&amp;gt;. A good practice is to test a model in both float and double if you suspect precision issues; significant differences indicate an ill-conditioned calculation that might need higher precision.&lt;br /&gt;
&lt;br /&gt;
In summary, FP32 remains the &#039;&#039;&#039;default&#039;&#039;&#039; format for most training and inference due to its balance of accuracy and efficiency&amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;, while FP64 is reserved for niche cases or precision-critical subroutines. FP32 typically achieves the same accuracy as FP64 for end-to-end deep learning tasks, because network training can often compensate for small noise, and other sources of error (like generalization error) dominate. Therefore, the community trend has been moving to &#039;&#039;&#039;lower&#039;&#039;&#039; precisions, not higher, to gain speed — provided the models still converge and perform well.&lt;br /&gt;
&lt;br /&gt;
=== 4.2. FP16 and BFloat16 in Deep Learning ===&lt;br /&gt;
&lt;br /&gt;
To further speed up neural network training and inference, &#039;&#039;&#039;half-precision (FP16)&#039;&#039;&#039; was adopted, especially with the advent of GPUs supporting FP16 math at twice the rate of FP32. Using FP16 reduces memory storage and bandwidth by 50% compared to FP32, which is significant for large models. However, FP16’s small exponent range and precision posed challenges: training a large network purely in FP16 can fail due to gradient underflow/overflow or weight updates not registering (because changes are below the 0.001 quantization step). To address this, frameworks introduced &#039;&#039;&#039;mixed-precision training&#039;&#039;&#039;, where compute-intensive parts (matrix multiplies, convolutions) run in FP16, but certain accumulations and model parameters remain in FP32. For instance, NVIDIA’s &#039;&#039;&#039;Tensor Core&#039;&#039;&#039; units require FP16 (or BF16) inputs but accumulate results in FP32, effectively ensuring that reductions are done in higher precision&amp;lt;ref name=&amp;quot;ref6&amp;quot;/&amp;gt;. Research showed that with proper loss scaling and mixed precision, models can train to FP32-level accuracy using mostly FP16 arithmetic, achieving significant speedups.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;BFloat16 (BF16)&#039;&#039;&#039; emerged as an alternative 16-bit format primarily from Google’s TPU training systems. BF16 has a larger exponent (8 bits, same as FP32) but only 7 fraction bits&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. This means BF16 can handle the &#039;&#039;&#039;same dynamic range&#039;&#039;&#039; as FP32 (no overflows where FP32 wouldn’t) but at the cost of even lower precision than FP16 (BF16 has about 2–3 decimal digits of precision vs FP16’s ~3–4). Despite its coarse precision, BF16 turns out to be effective for training deep networks because the algorithms (stochastic gradient descent, etc.) are somewhat robust to that noise, and having the wide range prevents catastrophic loss of signal (no gradient will overflow to Inf unless it was going to in FP32 anyway). Many training frameworks (PyTorch, TensorFlow) support BF16 training on hardware like &#039;&#039;&#039;TPU, NVIDIA A100+, Intel CPUs with AVX-512 BF16, etc&#039;&#039;&#039;. Typically, when using BF16, &#039;&#039;&#039;accumulations are still done in FP32&#039;&#039;&#039; (for example, a TPU v2/v3 multiplies BF16 matrices and accumulates into an FP32 result for the dot product&amp;lt;ref name=&amp;quot;ref6&amp;quot;/&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
In inference, both FP16 and BF16 are used to deploy models for faster speed or lower memory. FP16 is popular on NVIDIA GPUs (which have specialized FP16 units). BF16 is advantageous on TPUs and some CPUs that support it, and increasingly on GPUs (Ampere and newer GPUs also support BF16 arithmetic). The choice between FP16 and BF16 can depend on hardware: BF16’s advantage is easier software conversion (you can often take an FP32-trained model and cast weights to BF16 with minimal loss, since the dynamic range is intact), whereas FP16 might require adjusting the model or ensuring values fit in range (sometimes needing techniques like maintaining a master copy of weights in FP32 during training, or handling out-of-range values).&lt;br /&gt;
&lt;br /&gt;
For ONNX, both Float16 and BFloat16 types are part of the standard. ONNX models can be converted to FP16 (truncating weights and inserting cast nodes for computations) using tools like &amp;lt;code&amp;gt;float16_converter&amp;lt;/code&amp;gt; in ONNX Runtime&amp;lt;ref name=&amp;quot;ref8&amp;quot;/&amp;gt;. ONNX Runtime on GPUs will typically execute those FP16 ops with mixed precision on tensor cores (FP16 input, FP32 accumulate), whereas on CPU it may emulate FP16 or upcast to FP32 (as many CPU implementations do not natively support half-precision math)&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;. In fact, as an example, running an ONNX model in FP16 on a CPU might insert hidden Cast operations to FP32 behind the scenes, because the CPU kernel for that op only exists in FP32&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;. This ensures correctness (the computation is done in higher precision) but can reduce performance. We will revisit such backend-dependent behavior in Section 5.3.&lt;br /&gt;
&lt;br /&gt;
In summary, &#039;&#039;&#039;FP16 and BF16&#039;&#039;&#039; are now established as important low-precision formats in deep learning. FP16 offers more precision bits, which can matter for e.g. small differences, but has a limited range (care needed to avoid overflow). BF16 offers safety in range but less precision (which can slightly affect convergence or final accuracy in some cases). Both typically rely on FP32 for accumulation to achieve acceptable accuracy. When deploying models, one should choose a format supported efficiently by the target hardware and verify that the accuracy is not significantly degraded. Often, CNNs and transformers can run in FP16/BF16 with virtually no drop in score, but certain operations (like softmax, see Section 6.3) might need special handling.&lt;br /&gt;
&lt;br /&gt;
=== 4.3. FP8 Formats (E4M3, E5M2) and Scaling ===&lt;br /&gt;
&lt;br /&gt;
The push for ever lower precision has led to experimental &#039;&#039;&#039;8-bit floating point (FP8)&#039;&#039;&#039; formats for deep learning. In 2022, researchers from NVIDIA, Intel, and Arm proposed an FP8 standard with two variants: &#039;&#039;&#039;E4M3&#039;&#039;&#039; (4 exponent bits, 3 fraction bits) and &#039;&#039;&#039;E5M2&#039;&#039;&#039; (5 exponent bits, 2 fraction bits)&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;. Both have 1 sign bit, for a total of 8 bits. E5M2 has a wider exponent range (5 bits gives bias 15) but one fewer mantissa bit than E4M3. The idea is to use E4M3 for numbers that need more precision (like weights or activations around 1.0) and E5M2 for numbers that need more dynamic range (like gradient updates which can be very small or large). In fact, the study found that using E4M3 for forward activations/weights and E5M2 for backward gradients can allow training of large networks with FP8 to accuracy on par with FP16 training&amp;lt;ref name=&amp;quot;ref11&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
FP8 formats are &#039;&#039;not&#039;&#039; IEEE 754 standard (though IEEE is considering lower precision in its 2019 revision). Notably, the E4M3 format as used by NVIDIA does not allocate any bit-pattern for $+\infty$ or $-\infty$ – it has only NaN for invalid results, using that freed bit pattern to extend the representable range by one exponent value&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;. Essentially, E4M3 can represent numbers up to ~$2^{15}$ (around 3e4) instead of capping at infinity at $2^{16}$, by foregoing infinities&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;. Variations also exist regarding negative zero and how NaNs are handled&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;. For instance, GraphCore’s IPU uses FP8 formats that disallow negative zero and infinities, to maximize useable range&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Using 8-bit floats for neural networks requires additional techniques, chiefly &#039;&#039;&#039;scaling&#039;&#039;&#039;. Because 8-bit has very limited precision and range, one typically applies a power-of-two scale factor to groups of values to normalize them into an FP8-friendly range. This is akin to block floating-point or “microscaling” per layer or per tensor. For example, one might keep a FP32 scale for each channel or tensor, and represent values as $(FP8\_value) \times 2^{scale}$. The ONNX standard has introduced support for FP8 in version 1.15, including types for E4M3 and E5M2 (with variations FNUZ: finite-only and no negative zero)&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;, and even a type called &#039;&#039;&#039;E8M0&#039;&#039;&#039; which is an 8-bit exponent-only number used purely as a scale factor&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;. The E8M0 type has 8 exponent bits, no mantissa, effectively representing powers of two from $2^{-127}$ to $2^{128}$ for scaling purposes&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;. This indicates an approach where an FP8 tensor would be accompanied by an E8M0 scale (or scales) to interpret its value correctly.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Rounding&#039;&#039;&#039; becomes critical when casting values to FP8. The quantization error is much larger relative to FP16 or FP32. Studies have shown that choosing the right rounding mode (e.g., round-to-nearest, stochastic rounding, etc.) and clamping behavior (saturating to max instead of inf) can affect model accuracy&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;. ONNX added a &amp;lt;code&amp;gt;round_mode&amp;lt;/code&amp;gt; attribute to its Cast operator to allow controlling this for FP8&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;. In particular, one paper found that &#039;&#039;&#039;rounding up with saturation&#039;&#039;&#039; (round-to-nearest ties away from 0, and clamp overflows to max finite rather than inf) gave better accuracy in large language model training than other rounding modes&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
At present, FP8 is on the cutting edge. NVIDIA’s H100 GPU supports FP8 tensor cores (for both E4M3 and E5M2), and early results show minimal accuracy loss on some vision and NLP tasks when properly tuned. However, using FP8 requires fine-tuning hyperparameters like scaling factors for each layer and perhaps tweaking training procedures. ONNX’s inclusion of FP8 types means you can represent and transport models using FP8 weights or activations, but actual hardware support is limited and typically paired with calibration or dynamic scaling logic outside of ONNX graph (or using custom ONNX functions for scaling). It remains an &#039;&#039;&#039;open research area&#039;&#039;&#039; how to fully automate FP8 quantization for arbitrary models without accuracy loss.&lt;br /&gt;
&lt;br /&gt;
In summary, FP8 formats promise further compression of neural network computation and data. They are an order of magnitude more error-prone than FP16, but when combined with careful scaling, mixed usage of E4M3/E5M2, and maybe modified training techniques, they have shown surprising success. ONNX is actively evolving to support these ultra-low precision types, signaling their potential importance in future AI inference deployments.&lt;br /&gt;
&lt;br /&gt;
== Accumulation Semantics in ONNX ==&lt;br /&gt;
&lt;br /&gt;
One of the roles of the ONNX specification is to define the mathematical behavior of operators (Conv, MatMul, Sum, etc.) in a framework-agnostic way. However, ONNX mostly defines &#039;&#039;what&#039;&#039; an operator computes in exact arithmetic, not &#039;&#039;how&#039;&#039; the computation is carried out in finite precision. This leaves certain details – like accumulator precision or summation order – up to the implementation (runtime or hardware). We&#039;ll examine how ONNX treats accumulation and what guarantees (or lack thereof) are provided.&lt;br /&gt;
&lt;br /&gt;
=== 5.1. Operator Definitions and Numerical Guarantees ===&lt;br /&gt;
&lt;br /&gt;
Each ONNX operator is described as performing an ideal mathematical function on its inputs. For example, &amp;lt;code&amp;gt;MatMul&amp;lt;/code&amp;gt; is defined as the standard matrix product of two tensors, and &amp;lt;code&amp;gt;ReduceSum&amp;lt;/code&amp;gt; computes a summation along given axes. The ONNX spec, as of current versions, &#039;&#039;&#039;does not mandate the precision of intermediate calculations&#039;&#039;&#039; beyond the types of the inputs and outputs. If an operator’s inputs and outputs are float16, the spec implies the computation is done in float16 in a sense, but it does not forbid an implementation from using higher precision internally – it simply doesn&#039;t mention it. There is &#039;&#039;&#039;no explicit guarantee of identical bit-for-bit results&#039;&#039;&#039; across different ONNX backends for floating-point ops. Instead, results are expected to be numerically close, within tolerances one would normally expect from floating-point differences (due to different hardware or algorithms). The ONNX Backend Test suite typically uses a relative or absolute error tolerance when validating outputs for this reason.&lt;br /&gt;
&lt;br /&gt;
Because of this, important details like the use of extended precision or accumulation in higher precision are left ambiguous. This has led to discussions in the ONNX community about making such behavior more explicit. For example, a proposal was raised to allow specification of accumulation precision for certain ops like MatMul&amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;. The reasoning is that as lower-precision types (float16, bfloat16, float8) become more common, knowing or controlling whether an op internally accumulates in a higher precision is important for both accuracy and reproducibility&amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;. In June 2025, an issue on the ONNX GitHub suggested adding an attribute to MatMul (and similarly Gemm, Softmax, etc.) to specify the accumulator type&amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;. By default it would use input precision (status quo), but an attribute could request (for instance) 32-bit accumulation even if inputs are 16-bit&amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;. As of this writing, however, no such attribute exists in the official spec – it&#039;s an open issue. Thus, the numerical semantics are effectively: &#039;&#039;“compute the result as if in the same type as inputs, but actual implementations may use more precision internally as an optimization.”&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
From a guarantee standpoint, ONNX operators promise the mathematically correct result assuming real-number arithmetic, but they &#039;&#039;&#039;do not guarantee bit-exact results&#039;&#039;&#039; due to floating-point effects. There is an understanding that differences can arise from different summation orders, fused operations (e.g., an implementation might use an FMA (fused multiply-add) which has one rounding instead of two), or use of extended precision registers. ONNX does not formally define rounding modes either – it relies on IEEE 754 defaults, but if a backend used something like deterministic summation or alternative rounding, ONNX has no mechanism to express that.&lt;br /&gt;
&lt;br /&gt;
In summary, ONNX&#039;s current stance is largely &#039;&#039;&#039;implementation-dependent&#039;&#039;&#039; for numerical precision. It provides minimal explicit numerical guarantees beyond type constraints. This is why an ONNX model run on two different hardware or runtime libraries can have slightly different results, especially in reduced precision. The burden is on backends to produce results &amp;quot;close enough&amp;quot; to each other that they don&#039;t affect the model&#039;s decisions in any significant way.&lt;br /&gt;
&lt;br /&gt;
=== 5.2. MatMul, Conv, and Reduction Operators ===&lt;br /&gt;
&lt;br /&gt;
Consider ONNX &amp;lt;code&amp;gt;MatMul&amp;lt;/code&amp;gt;: it takes two tensors (often two matrices) and produces their matrix product. In pure math, $C_{ij} = \sum_k A_{ik} &#039;&#039; B_{kj}$. If A and B are float16, should that sum be computed in float16 or float32? As discussed, ONNX doesn’t specify – it simply says the output is a float16 matrix containing the product. In practice, many backends will &#039;&#039;promote&#039;&#039; the computation: for example, NVIDIA GPUs will do the multiplication and addition in a mixed FP16/FP32 mode by default (because of tensor cores), then convert the final result back to float16. ONNX Runtime on CPU might not support float16 multiplication natively, so it will insert cast ops to float32, perform the MatMul in float32, then cast back to float16&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;. From ONNX’s perspective, as long as the final result is within acceptable error of the “ideal” float16 computation, it’s fine. But note, an “ideal” float16 computation (where every multiply and add is rounded to FP16) might actually be &#039;&#039;less accurate* than what the backend did with FP32. This means that a backend using higher precision can produce a result that is not bit-equal to a pure FP16 implementation – it&#039;s actually closer to the true real number result. This is usually considered a good thing (improved accuracy), but it does highlight a lack of consistency: another backend might do everything in pure FP16 and get a slightly different answer.&lt;br /&gt;
&lt;br /&gt;
The same situation occurs for &#039;&#039;&#039;convolution&#039;&#039;&#039; (&amp;lt;code&amp;gt;Conv&amp;lt;/code&amp;gt; op) which is essentially a batched sum of products (like MatMul in sliding window), and for explicit reduction ops like &amp;lt;code&amp;gt;ReduceSum&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;ReduceMean&amp;lt;/code&amp;gt;, etc. If you have an ONNX &amp;lt;code&amp;gt;ReduceSum&amp;lt;/code&amp;gt; that sums 1000 float16 values, the spec doesn’t say how to sum them. A GPU kernel might sum in a tree using FP32 accumulators; a naive implementation might sum in order with FP16 and possibly overflow or underflow intermediate steps. Both are allowed as long as they output a float16 result that is “reasonable.” In practice, ONNX Runtime and others take care to avoid obviously bad behavior (like overflow) by using higher precision or by splitting the sum. But again, it’s not mandated by the ONNX standard explicitly.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Softmax&#039;&#039;&#039; is an interesting case: Softmax involves an internal sum of exponentials. ONNX defines the Softmax operator as $$\mathrm{Softmax}(x)_i = \frac{\exp(x_i)}{\sum_j \exp(x_j)},$$ typically applied along a specified axis. Directly computing this in float16 can be problematic for large vectors, as discussed in Section 6.3. Indeed, the issue of accumulation precision arises: should the sum $\sum_j \exp(x_j)$ be done in FP16 or can it be in FP32? Many implementations will do it in FP32 even if $x$ is FP16, because the dynamic range of $\exp(x)$ is large. The ONNX Softmax spec doesn’t detail this, but it expects the result to be a proper probability distribution (summing to 1.0 within normal floating error). A pure FP16 softmax on, say, 1000 elements could sum to something like 0.996 or 1.004 due to rounding, whereas FP32 accumulation would be much closer to 1.000. ONNX likely allows both; the small difference is usually inconsequential, but if it were large (say the FP16 sum overflowed to inf), that would be a correctness issue. A robust backend will prevent that (by using the common max-subtraction trick and higher precision accumulation). The ONNX issue 7072 indeed calls out Softmax as another op where explicit control of accumulation precision would be useful&amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
In summary, &#039;&#039;&#039;MatMul, Conv, and reduction ops&#039;&#039;&#039; in ONNX are defined mathematically but with &#039;&#039;implicit&#039;&#039; numerical behavior. Implementations commonly use higher precision for intermediate sums especially when input is low precision, but this is not standardized. Consequently, results can differ slightly between implementations. For critical applications, one must be aware of these differences. A model exported to ONNX with float16 weights might perform slightly differently on one runtime vs another if one does all float16 math and the other uses float32 accumulations. Both conform to ONNX. As a user, if you require a certain precision for accumulation, currently you have to ensure it by the way you export or run the model (for example, insert manual cast nodes in the ONNX graph to force certain ops to FP32). We may see future ONNX versions address this with attributes or metadata as mentioned, but for now these ops are effectively “compute with best effort accuracy”.&lt;br /&gt;
&lt;br /&gt;
=== 5.3. Backend and Hardware-Dependent Behavior ===&lt;br /&gt;
&lt;br /&gt;
Because ONNX is implemented by many backends (ONNX Runtime, TensorRT, CoreML, PyTorch JIT, etc.), and these in turn run on varied hardware (CPU, GPU, accelerators), the numeric behavior can be backend-dependent. Some examples:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;CPU backends:&#039;&#039;&#039; Many CPUs do not have native float16 vector arithmetic (with a few exceptions in recent CPUs). ONNX Runtime’s default CPU execution provider will often upcast float16 to float32, compute, then downcast&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;. This ensures accuracy but means that if you thought you were getting a full float16 pipeline, you’re not – the CPU is essentially ignoring the low precision in compute. On the other hand, x86 CPUs have extended 80-bit precision in x87 registers (though less used now) and 64-bit SIMD registers for doubles; some compiler optimizations might carry intermediate results in higher precision. This could make CPU results slightly different from a GPU’s if, say, a sum is done in 80-bit on CPU versus 32-bit on GPU. Generally, modern compilers avoid such extended precision unless explicitly asked, to improve reproducibility.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;GPU backends:&#039;&#039;&#039; GPUs often perform summations in parallel using tree reductions. The order of operations can change depending on how threads are scheduled. This means even on the same GPU, summing an array in one launch versus another might not give bit-identical results if the internal parallel reduction algorithm differs (though it should be deterministically the same given the same library version and input, but if you change GPU model or BLAS library, it might differ). Also, GPUs have fused multiply-add (FMA) instructions that do $(a*b + c)$ in one step with one rounding, which can produce slightly different results than doing the multiply and add separately (which would round twice). If an ONNX backend uses a fused kernel (like a GEMM library) it might yield different low-order bits than a simple loop.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Vendor libraries:&#039;&#039;&#039; ONNX Runtime can use vendor BLAS or DNN libraries (MKL, cuDNN, TensorRT, etc.). These libraries often implement their own strategies for numerical stability. For example, NVIDIA’s cuDNN might use FP32 accumulation for FP16 inputs by default. TensorRT (NVIDIA’s inference engine) will sometimes automatically promote precisions or insert scaling to maintain stability&amp;lt;ref name=&amp;quot;ref13&amp;quot;/&amp;gt;. On the other hand, if you explicitly request FP16 in TensorRT with no FP32 fallback, it might do the entire op in FP16 and you’d see more error. Some libraries offer knobs: TensorRT allows choosing FP32, FP16, or INT8 precision for each layer, and will use FP32 internally for safety in some cases unless forced otherwise.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Hardware-specific quirks:&#039;&#039;&#039; As another example, NVIDIA’s &#039;&#039;&#039;TensorFloat-32 (TF32)&#039;&#039;&#039; on Ampere GPUs is a precision mode where matrix multiplications use 10-bit mantissa (so about FP16 precision for the multiply) but keep 8-bit exponent (range like FP32). By default, CUDA libraries use TF32 for FP32 matrix multiplies on Ampere to speed them up, unless you disable it. This means if you run an ONNX model with FP32 on an A100 GPU, your matmuls might actually be lower precision than FP32 (though higher than FP16)&amp;lt;ref name=&amp;quot;ref6&amp;quot;/&amp;gt;. It’s an example of hardware choosing an intermediate precision for performance. From ONNX’s perspective, the output is still FP32 and (hopefully) within error tolerances, but a user might be surprised that it’s not a “true” FP32 computation internally. If exactness is needed, one has to turn off TF32.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Non-determinism:&#039;&#039;&#039; Some backends (especially on GPUs) allow non-deterministic execution for performance. For instance, atomic adds in parallel reductions can produce results that vary run-to-run because the order of accumulation is race-condition-dependent. ONNX’s spec doesn’t cover this explicitly, but it’s something to consider. In ONNX Runtime, there are settings for deterministic computing, but by default it may use all available performance optimizations, even if results vary at the 1e-7 level.&lt;br /&gt;
&lt;br /&gt;
In short, the numerical behavior under ONNX can depend on the combination of backend software and hardware capabilities. The lack of a strict spec for accumulation means implementations have freedom to use clever strategies (higher precision, reordering) to improve accuracy or speed. Usually this is positive (e.g. getting better accuracy than a naive approach). But it could also lead to slight inconsistencies. For example, you might find that exporting a PyTorch model to ONNX and running it with one engine yields outputs slightly different from PyTorch’s – not because ONNX is wrong, but because the summation or convolution was done differently (maybe PyTorch summed in FP32 as well, but maybe not in exactly the same way).&lt;br /&gt;
&lt;br /&gt;
The key takeaway is that ONNX &#039;&#039;by design&#039;&#039; abstracts away these details, which is good for portability but means you need to &#039;&#039;&#039;trust the backend&#039;&#039;&#039; or verify its precision behavior for sensitive models. If necessary, you can enforce certain behavior by inserting ops (for example, if you want to ensure FP32 accumulation, you could insert a cast to FP32 for inputs of a MatMul and cast the result back to FP16 – this way the ONNX graph itself mandates FP32 compute in between). This of course sacrifices some performance or device-specific magic.&lt;br /&gt;
&lt;br /&gt;
Looking forward, as mentioned, there are proposals to enrich ONNX’s specification regarding precision. This might include explicit flags for “exact summation” or “accumulate in int32” etc., which would make models more self-descriptive but also potentially less portable (if a backend doesn’t support that, what then?). For now, users should be aware of these backend differences. Most of the time, well-engineered ONNX runtimes yield results close enough that they don&#039;t affect the application (e.g., classification top-1 remains the same even if low-level sums differ by 1e-5). But for high-stakes numerical computations, testing and maybe adjusting the model or backend settings is prudent.&lt;br /&gt;
&lt;br /&gt;
== Numerical Stability in Deep Learning Models ==&lt;br /&gt;
&lt;br /&gt;
Deep learning computations involve many operations that, while theoretically well-defined, can be sensitive to numerical issues when implemented in finite precision. We&#039;ll discuss a few common scenarios in neural networks where numerical stability is a concern, and how precision and accumulation play a role.&lt;br /&gt;
&lt;br /&gt;
=== 6.1. Dot Products and Large-Scale Reductions ===&lt;br /&gt;
&lt;br /&gt;
Neural networks are full of dot products: every fully-connected layer computes $\mathbf{y} = W\mathbf{x}$, which is a series of inner products between weight vectors and the input. Convolutional layers compute weighted sums of local regions. Attention mechanisms compute large matrix products of query/key/value matrices. All these boil down to summing a lot of terms: $\sum_{k=1}^K a_k b_k$. When $K$ is large (it could be in the thousands or more for a single neuron in a fully connected layer of a large model), the potential for floating-point error in that sum increases.&lt;br /&gt;
&lt;br /&gt;
If using FP32, the error may still be negligible in many cases, but in lower precision like FP16, it can become significant. For example, consider a dot product of length 2048 in FP16. Each product is in FP16 (with ~3 decimal digits precision) and if accumulated in FP16, the worst-case error could be on the order of $2048 \times \varepsilon_{FP16} \approx 2048 \times 0.0009766 \approx 2$ (i.e., you could lose a couple units in the third decimal place relative accuracy in worst case). Even in random error accumulation, you might see error on the order of $\sqrt{2048} \times 0.0009766 \approx 0.044$ or 4.4% (though this is a rough estimate). That’s quite significant. Fortunately, as discussed, most hardware will accumulate such sums in a higher precision like FP32, dramatically reducing that error (~7 orders of magnitude smaller $\varepsilon$). But if it didn’t, the network’s outputs could get noisy. A fully-connected layer with many inputs might output slightly different results depending on summation order if done in low precision.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Large-scale reductions&#039;&#039;&#039; also occur in operations like global average pooling (summing all elements of a feature map) or computing the total loss by summing loss over batch samples. If one sums 100,000 values in FP32, the worst-case error could be on the order of 100,000 &#039;&#039; 1e-7 ≈ 1e-2 (1% uncertainty), though typical error would be much less. In FP16, summing 100k values is likely impossible without overflow: as noted, adding ~10^4 moderate numbers can overflow FP16&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. So large reductions &#039;&#039;must* be handled carefully (either in chunks or in higher precision). Frameworks generally do: for example, when performing a sum reduction on GPU, libraries break it into parallel blocks that accumulate in registers (usually 32-bit registers, even if inputs are 16-bit). &lt;br /&gt;
&lt;br /&gt;
Another aspect is that many dot products in neural nets involve values of varying magnitude. If the distribution of terms is such that some are much larger than others, the smaller terms might not contribute much if summed last. Ideally, one might sort by magnitude or use pairwise summation to improve that. Most BLAS libraries effectively do a pairwise strategy due to their parallel structure. So, while worst-case analyses are grim, practical implementations mitigate a lot of it.&lt;br /&gt;
&lt;br /&gt;
However, certain layers can suffer if not handled properly. One famous example is in RNNs or LSTMs: if one uses FP16 naive summation for the gating mechanism, errors could accumulate over many time steps and cause drift or instability. In modern practice, recurrent layers are often run in FP16 with FP32 accumulators to avoid this.&lt;br /&gt;
&lt;br /&gt;
In summary, &#039;&#039;&#039;dot products and large reductions need sufficient precision&#039;&#039;&#039; or robust algorithms. If you are implementing a custom kernel (say a custom ONNX op) and you have to sum a huge array, you should consider using a technique from Section 3 (pairwise summation, Kahan, or at least double accumulator if possible). Many deep learning computations, by virtue of hardware and library design, already do something like this under the hood. But it&#039;s always good to be aware, especially if pushing into lower precision territories.&lt;br /&gt;
&lt;br /&gt;
=== 6.2. Cancellation, Dynamic Range, and Conditioning ===&lt;br /&gt;
&lt;br /&gt;
We introduced &#039;&#039;cancellation&#039;&#039; earlier in the context of summation. In deep learning, cancellation can appear in a few places. One is in the computation of certain norms or variances. For instance, Batch Normalization computes mean and variance of activations. The variance formula $\mathrm{Var}(x) = E[x^2] - (E[x])^2$ is subject to catastrophic cancellation if $E[x]$ and $\sqrt{E[x^2]}$ are close in magnitude (which can happen if the variance is small compared to the mean). If implemented naively in single precision, you might get a negative variance due to numerical error (which would then be clamped to zero or sqrt would yield NaN). Robust implementations compute variance in a single pass or increase precision. Indeed, in many frameworks, batchnorm accumulation is done in FP32 even if the activations are FP16, precisely to avoid numerical issues in computing mean and variance. ONNX itself doesn’t specify this, but an ONNX backend is likely to do batchnorm stats in higher precision for safety. The &#039;&#039;conditioning&#039;&#039; of the variance calculation is poor when variance is tiny relative to mean, so it&#039;s a spot where higher precision is needed.&lt;br /&gt;
&lt;br /&gt;
Another example is in certain loss functions or regularizations. If you subtract two large numbers to get a small difference, you lose precision. Say you have two nearly identical predictions and you subtract them as part of some operation – if done in float16, you might get zero whereas float32 would have a slight difference. This could affect, say, a gradient if that difference was supposed to propagate information. Fortunately, neural nets often avoid directly subtracting large nearly equal numbers; but it can happen. In attention mechanisms, the &#039;&#039;score normalization&#039;&#039; (softmax) subtracts a max to avoid huge exponentials, which is good for range but if multiple scores are close to the max, you subtract numbers that are close. However, those subtractions are not catastrophic in the same way; they&#039;re intentional to reduce range and are typically fine in FP32 or even FP16.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Dynamic range&#039;&#039;&#039; issues are everywhere in deep learning. Activation functions like ReLU and exponential can produce very large or very small values. If subsequent computations don’t account for that, you get infinities or zeros. For example, consider a network output that produces a logit of 100 in FP16. $\exp(100)$ in FP16 overflows to infinity (since FP16 max ~ 6e4 and $e^{100} \sim 3.7e43$). If you then do softmax, that one infinity will dominate the sum and you might get a probability of 1 for that class (or NaNs if not handled). The standard softmax implementation subtracts the max logit, so in this case it would subtract 100 from all logits, making the largest exponent $\exp(0)=1$ and others tiny – this avoids overflow. But what about underflow? If you had a very negative logit, $\exp(-100)$ in FP16 would underflow to 0. In softmax, that just contributes ~0 probability, which is fine (it effectively is zero anyway). Underflow tends to be less dangerous than overflow in inference — underflow usually just means “this probability/neuron is effectively 0,” whereas overflow can poison the results with NaNs or Infs. &lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Conditioning&#039;&#039;&#039; refers to how sensitive a function is to input perturbations. Neural networks themselves as functions can be poorly conditioned (e.g., tiny changes in weights cause big changes in output for some architectures), but typically they are designed to be reasonably well-behaved. Certain internal computations like matrix inversion (if one does a pseudoinverse or something for a layer) can be extremely ill-conditioned if the matrix is near singular. That again would need double precision perhaps. In ML, one common ill-conditioned operation is the &#039;&#039;&#039;softmax denominator&#039;&#039;&#039;: $\sum_j e^{x_j}$ can be very large and also very sensitive to changes in the largest $x_j$. However, the softmax is more stable if computed carefully (max trick). Another is &#039;&#039;&#039;normalization&#039;&#039;&#039;: computing $\frac{x - \mu}{\sigma}$ for layer norm or batch norm – if $\sigma$ is very small, then the division blows up noise. In those cases, a small $\epsilon$ is usually added to $\sigma^2$ to prevent instability (this is a model-level fix, not a numeric one per se, but it acknowledges numeric limits).&lt;br /&gt;
&lt;br /&gt;
In training, &#039;&#039;&#039;optimizers&#039;&#039;&#039; can suffer from precision issues. For example, the Adam optimizer keeps a moving average of squared gradients; in half precision, these small updates might flush to zero or not change at some point. Some research found that certain adaptive optimizers break in FP16 because the small differences are lost (&amp;quot;Why FP16 training breaks NAdam&amp;quot; etc.). The solution is often to keep the optimizer states in FP32 while doing the bulk of compute in FP16. This again is a form of mixed precision.&lt;br /&gt;
&lt;br /&gt;
From an inference perspective, once the model is trained, the main concerns are avoiding overflow in activations and ensuring summations (like in softmax or reductions) are done with enough precision. The model’s weights are fixed, so we don&#039;t have to worry about cumulative error over iterations as in training, just the forward pass.&lt;br /&gt;
&lt;br /&gt;
=== 6.3. Normalization Layers and Softmax ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Normalization layers&#039;&#039;&#039; (BatchNorm, LayerNorm, etc.) and &#039;&#039;&#039;Softmax&#039;&#039;&#039; are specifically worth examining for numerical stability:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Batch Normalization:&#039;&#039;&#039; During inference, batchnorm applies an affine transform using precomputed mean and variance (from training). It’s basically: $y = \frac{x - \mu}{\sqrt{\sigma^2 + \epsilon}} \gamma + \beta$. This is a stable operation as long as $\epsilon$ is provided (to avoid division by zero). Since $\mu, \sigma^2$ are constants in inference, it reduces to linear ops which are fine. The main numerical work (computing mean/variance) happened during training or model calibration, and frameworks usually did that in FP32. So batchnorm at inference time is not a source of instability (it’s just a scale and shift of the data).&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Layer Normalization / Group Normalization:&#039;&#039;&#039; These compute mean/var on the fly for each sample (and possibly channel group). In inference, they still need to compute these statistics for each forward pass. If implemented in low precision, the variance calculation can suffer cancellation as mentioned. However, it&#039;s likely that inference engines compute the mean and variance in FP32 even if inputs are FP16 (especially since these are per-sample operations, not huge batch reductions – it&#039;s feasible to do in higher precision). If someone implemented layer norm purely in FP16, they might see issues when the variance is very small. Usually there’s an $\epsilon$ added which might dominate a tiny variance and thus mitigate catastrophic cancellation (the result might just output something mostly from $\epsilon$). In summary, normalization layers are usually fine as long as the implementation uses enough precision for the summation of squares.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Softmax:&#039;&#039;&#039; Softmax is notorious if done improperly. The safe algorithm: subtract the maximum input from all inputs (to bring the largest exponent to 1.0), exponentiate, sum them up, divide each exponent by the sum. The max subtraction handles overflow in $\exp()$. But what about the sum? As discussed, if there are $N$ elements in the softmax, the sum is at most $N$ (when all inputs equal the max). In classification, $N$ might be the number of classes. For ImageNet, $N=1000$, so worst sum ~1000 (which is fine in FP16, as 1000 &amp;lt;&amp;lt; 65504). For an extreme case like a language model, $N$ could be 50,000 (vocab size). Sum could be 50,000 in worst case (everyone equal probability). 50,000 is still below 65504, so actually an FP16 sum would &#039;&#039;not&#039;&#039; overflow in that scenario, and the rounding error on a sum of 50k identical terms would be maybe a few ULPs (which at 50k magnitude might be ~0.5 difference worst-case). So maybe softmax summation in FP16 is not catastrophic for output probabilities – you’d get essentially 1.0 after normalization instead of 0.99999 in some case. Usually fine.&lt;br /&gt;
&lt;br /&gt;
:The bigger issue is if one or a few terms dominate the softmax. If one term is much larger, the others underflow to 0 after the max subtraction. That’s fine: you get a one-hot distribution essentially. If the model expected a softer distribution, that could be a problem. But if the logits were really that far apart, even FP32 would give nearly one-hot. So not really a precision issue, more of a model property.&lt;br /&gt;
&lt;br /&gt;
:However, the &#039;&#039;&#039;stability during training&#039;&#039;&#039; is a big issue. The GitHub thread we saw (ColossalAI) mentioned training a 130B model where FP16 softmax in attention caused training to occasionally blow up&amp;lt;ref name=&amp;quot;ref14&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref14&amp;quot;/&amp;gt;. They observed attention scores in some heads ranging up to 1e4 or as low as 1e-3&amp;lt;ref name=&amp;quot;ref14&amp;quot;/&amp;gt;. 1e4 exponent in FP16 is definitely Inf (because $\exp(10^4)$ is astronomically huge, but even exp(100) is inf in half). The method CogView introduced (Precision Bottleneck Relaxation) involved subtracting the max &#039;&#039;per head&#039;&#039; to reduce range&amp;lt;ref name=&amp;quot;ref14&amp;quot;/&amp;gt;—which is basically the softmax max trick at a finer granularity or something similar. In the end, they found &#039;&#039;&#039;the simplest fix was to compute softmax in FP32&#039;&#039;&#039; for those layers&amp;lt;ref name=&amp;quot;ref14&amp;quot;/&amp;gt;. They reported that it had negligible speed impact but significantly improved stability&amp;lt;ref name=&amp;quot;ref14&amp;quot;/&amp;gt;. This highlights a general principle: certain operations like softmax (and sometimes matrix multiplications in attention) benefit from higher precision, and using FP32 in a few critical spots can save a mixed-precision training from crashing. In inference, using FP32 for softmax is also a safeguard if one is uncertain. It costs little because softmax is not a heavy compute compared to matrix multiplies.&lt;br /&gt;
&lt;br /&gt;
:ONNX does have a Softmax operator; if the input is float16, a backend might do exactly what they described: internally cast to float32 for the softmax computation, then cast output back to float16. Some inference engines (like DeepSpeed’s inference or others) keep attention softmax in FP32 even if rest is FP16, for robustness&amp;lt;ref name=&amp;quot;ref14&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Apart from softmax, other places where exponentials occur are in loss functions like cross-entropy (which uses log-softmax internally), or in activation functions like Sigmoid and Tanh (which are basically scaled logistic functions). Sigmoid($x)=1/(1+e^{-x})$. If $x$ is moderately large positive, $e^{-x}$ underflows to 0 in FP16 if $x&amp;gt;~11$. That gives a sigmoid output of 1.0 exactly. In FP32, $e^{-x}$ for $x=11$ is about $5e-5$, not underflow, so sigmoid ~0.99995. So FP16 will saturate some outputs to exactly 0 or 1 sooner than FP32 would. This can slightly change behaviors (e.g., a neuron might go to full saturation a bit early). Usually, this isn’t a huge deal, but it could make gradients zero earlier. In inference, we don’t care about gradients, just the output value. Getting exactly 1.0 vs 0.99995 probably has no practical difference for a classification output. But one should be aware that &#039;&#039;&#039;activation functions can saturate faster&#039;&#039;&#039; in lower precision due to limited range or precision. ReLU has no issue (it’s linear or 0), but exponential-based activations do. In practice, again, frameworks often simply compute those in the given precision and accept the slight difference. If it were a problem, one could do those in FP32 as well.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Summary:&#039;&#039;&#039; Numerical stability in inference is maintained by using robust computation methods: subtracting maxima in softmax/LogSumExp, adding epsilon in denominators for normalization, and using higher precision for summations or critical steps. Most of these are built into the algorithms. ONNX models assume these standard practices (they don&#039;t need to explicitly do them in the graph because the backend inherently will for ops like Softmax). It&#039;s wise when building or exporting models to ONNX to test them in the target precision and ensure there are no surprises (like an output becomes NaN or Inf due to precision). Usually if training was done in mixed precision, the model will have been vetted in that environment already.&lt;br /&gt;
&lt;br /&gt;
== Performance vs. Accuracy Trade-offs ==&lt;br /&gt;
&lt;br /&gt;
There is an inherent trade-off between numerical precision and computational performance (speed, memory usage) in AI systems. Choosing lower precision can dramatically increase throughput and reduce memory, but it may introduce arithmetic errors. Engineers must balance these considerations:&lt;br /&gt;
&lt;br /&gt;
=== 7.1. Throughput, Memory Bandwidth, and Precision ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Throughput (speed)&#039;&#039;&#039; is often higher for lower-precision operations. Modern hardware can execute more low-precision ops in parallel than high-precision ops. For example, an NVIDIA V100 GPU can perform 2× more half-precision FLOPs than single-precision per clock, and newer architectures with Tensor Cores can do 8× more (since a Tensor Core warp operation might perform 64 FP16 FMAs per cycle vs 16 FP32). Similarly, int8 operations can be even faster and more energy efficient. Lower precision numbers also use less memory bandwidth – fetching 16-bit data is twice as fast as 32-bit data from memory (assuming the same bus width), which can relieve memory bottlenecks. Many deep learning workloads are memory-bound, so halving the data size can nearly double throughput if compute is not the limiting factor. This is a huge incentive to use lower precision.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Memory and storage&#039;&#039;&#039;: Using FP16 or int8 weights reduces model size, which is important for deploying large models to memory-limited environments (like edge devices or GPUs with limited VRAM). It also means you can fit larger batch sizes in memory, or more models on a chip, etc. There is also a cascade effect: smaller data means better cache utilization, which further improves performance.&lt;br /&gt;
&lt;br /&gt;
However, &#039;&#039;&#039;accuracy&#039;&#039;&#039; suffers if precision is too low. We have discussed how rounding error and limited range can cause deviations in outputs. The question is: do those deviations matter for the application? Often for neural networks, a small amount of noise or error in intermediate calculations does not change the final prediction. Neural nets are somewhat error-tolerant due to their redundant distributed representations. This is why quantization and FP16 techniques can work at all. The final model accuracy (e.g. top-1 accuracy on ImageNet) might drop only 0.5% or less when using FP16 instead of FP32, or int8 instead of FP16, if done properly. For many use-cases, this loss is acceptable given the speed gains.&lt;br /&gt;
&lt;br /&gt;
If one goes too low (like naive FP8 or int4 without special care), accuracy might drop severely – perhaps making the model unusable. Thus the trade-off: as precision decreases, performance increases but at some point accuracy degrades beyond an acceptable threshold. Engineers aim to find the lowest precision that still preserves accuracy.&lt;br /&gt;
&lt;br /&gt;
There is also the consideration of &#039;&#039;&#039;accumulation strategy&#039;&#039;&#039; affecting performance: for instance, Kahan summation (compensated) is more accurate but ~4× slower than naive summation. If you need that accuracy, you pay a performance cost. In many AI inference scenarios, the slight extra accuracy from something like Kahan isn&#039;t needed, so it’s not used. But in a scenario where you sum millions of elements for a final result that is thresholded, maybe you do need it.&lt;br /&gt;
&lt;br /&gt;
Another dimension is &#039;&#039;&#039;determinism vs speed&#039;&#039;&#039;. Enforcing a deterministic reduction order (for reproducibility) might require disabling some parallelism, which could slow things down. If exact repeatability is critical (like certain scientific or financial applications of neural nets), one might sacrifice some performance to ensure the summation happens the same way every time. ONNX allows both modes depending on the runtime.&lt;br /&gt;
&lt;br /&gt;
From a memory perspective, if precision is higher than needed, you waste memory. For example, storing intermediate activations in FP32 when they could be FP16 uses 2× memory, which might mean lower batch size or more memory traffic. So there&#039;s strong motivation to keep things as low-precision as possible &#039;&#039;except&#039;&#039; where accuracy truly demands.&lt;br /&gt;
&lt;br /&gt;
In summary, the trade-off often manifests as &#039;&#039;diminishing returns&#039;&#039;: FP64 vs FP32 – huge slowdown for usually negligible accuracy gain in ML; FP32 vs FP16 – moderate speedup for usually minor accuracy hit; FP16 vs int8 – another big speedup but maybe more accuracy loss unless calibration is done; int8 vs int4 – speed gain but often big accuracy drop that might not be worth it for current models.&lt;br /&gt;
&lt;br /&gt;
Engineers use techniques like &#039;&#039;&#039;mixed precision&#039;&#039;&#039; to target high precision only where needed. For example, run most of the model in FP16, but keep a few sensitive layers in FP32. This yields most of the performance benefits while ensuring critical parts are accurate. We already saw softmax as one example. Another might be final output layer: sometimes using FP32 for the logits and softmax can be safer.&lt;br /&gt;
&lt;br /&gt;
Hardware is also trending to support mixed modes – like FP8 Tensor Cores accumulate in FP32, etc. So hardware tries to give accuracy cheaply.&lt;br /&gt;
&lt;br /&gt;
=== 7.2. Hardware Accelerators and Tensor Cores ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Hardware accelerators&#039;&#039;&#039; (GPUs, TPUs, FPGAs, ASICs like GraphCore IPU, etc.) have specific support for certain precisions. Leveraging these can provide massive speedups:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;NVIDIA Tensor Cores:&#039;&#039;&#039; These started in Volta (V100) with support for 4x4 matrix multiply-adds on FP16 inputs with FP32 accumulation&amp;lt;ref name=&amp;quot;ref6&amp;quot;/&amp;gt;. Using Tensor Cores can give up to 8x throughput compared to using regular FP32 cores. But to use them, the data needs to be FP16 (or now BF16 or FP8 on newer GPUs) and aligned to certain dimensions (multiples of 8 etc. in matrix sizes, which frameworks handle by padding if needed). If an ONNX model is all FP32, by default it would not use Tensor Cores unless the runtime automatically converts some ops to mixed precision (which some do via a flag or environment variable enabling TF32 or FP16). The introduction of &#039;&#039;&#039;TensorFloat-32 (TF32)&#039;&#039;&#039; in Ampere GPUs is essentially a compromise: treat FP32 inputs as two 16-bit significand halves (10-bit mantissa actually) so that it can run on Tensor Cores, giving ~8x speed, while still outputting FP32. TF32 has about the precision of FP16 but range of FP32, and it was chosen because it typically doesn’t hurt convnet accuracy. ONNX Runtime or others on Ampere will use TF32 by default for conv and matmul unless disabled, giving a free speed boost at minimal accuracy cost. If exact FP32 is needed, users have to turn it off (sacrificing speed).&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;TPU (Google):&#039;&#039;&#039; TPUs have native support for BF16 matrix ops with FP32 accumulators. They were designed specifically for the mixed precision approach in training. For inference, TPUs also support int8 and other quantized ops (e.g., via Edge TPU for lower-power). If exporting ONNX to run on a TPU (via some bridge), one might get automatic BF16 usage. BF16 has the advantage of no loss in range, so TPUs can more straightforwardly run models in BF16 without modifications.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;FPGAs and ASICs:&#039;&#039;&#039; Some custom accelerators might use fixed-point or block floating-point arithmetic for efficiency. They often treat the network in quantized terms. The ONNX quantization specification (QuantizeLinear/DequantizeLinear ops) exists for representing quantized models (INT8 etc.), which is another domain beyond floating-point, but it overlaps as a precision issue. In those systems, accumulators for INT8 × INT8 multiplies are usually 32-bit (to avoid overflow for dot products up to certain lengths). The trade-off there is between integer vs float representation, which can give big speedups on CPUs/DSAs via SIMD instructions (many CPUs have vector int8 multiply-add instructions now for inference).&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Memory bandwidth on hardware:&#039;&#039;&#039; Some accelerators include special high-bandwidth memory or cache specifically for lower precision. For example, an accelerator might fetch two FP16 values in one 32-bit memory transaction – effectively doubling memory bandwidth for half precision. On some hardware, using FP16 can even reduce memory &#039;&#039;latency&#039;&#039; if multiple values can be packed.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Parallelism and Vectorization:&#039;&#039;&#039; Many hardware units process data in fixed vector widths (e.g., 256-bit registers). That could be 8 floats at once or 16 half-floats at once in the same 256 bits. If your code is in FP16, you might use the full vector width (16 values) whereas FP32 might only use half (8 values). This means FP16 can make better use of vector units. Similarly, matrix units often have fixed tile sizes, and smaller data types allow larger tiles or more tiles concurrently.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Energy efficiency:&#039;&#039;&#039; Lower precision often means less energy per operation, which on large deployments (data centers or battery-powered devices) is a significant advantage. This isn&#039;t directly performance, but it&#039;s an important trade: you might choose a slightly lower precision to save power even if accuracy drops a tiny bit.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Software overhead:&#039;&#039;&#039; Using mixed precision on hardware involves some overhead of casting and moving data between precisions. For instance, if you have to frequently cast FP16 to FP32 and back, that adds instructions and memory copies. A well-designed accelerator (like GPUs with unified memory for FP16/32 or special hardware casting) minimize this. Still, the runtime might insert extra operations (as we saw, ONNX Runtime CPU inserted many Cast nodes converting FP16 to FP32 and back&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;, which actually &#039;&#039;slowed down&#039;&#039; that FP16 model by 4x vs FP32 on a CPU, because the CPU had no native FP16). So ironically, on some hardware, using lower precision in ONNX might degrade performance if not supported (the CPU case). Always ensure the target hardware actually supports the low precision you plan to use; otherwise, you&#039;ll pay overhead for emulation.&lt;br /&gt;
&lt;br /&gt;
To illustrate trade-off: Suppose you have a model that achieves 99% accuracy in FP32 and drops to 98.5% in FP16. If FP16 runs 2x faster, many would accept the 0.5% accuracy drop for double throughput. If int8 runs 4x faster but accuracy drops to 95%, that might be too much drop for some applications, but acceptable for others that value speed. It&#039;s application-dependent.&lt;br /&gt;
&lt;br /&gt;
In critical applications, one might maintain higher precision accumulators or run certain validation steps in high precision to ensure nothing drifted. In research, sometimes a model is trained in mixed precision but validated in double to ensure any small numeric differences don’t influence the reported result (rare, but for scientific computing integration maybe).&lt;br /&gt;
&lt;br /&gt;
In the ONNX world, performance-vs-accuracy is often managed via &#039;&#039;&#039;Execution Providers&#039;&#039;&#039; (EPs) that target specific hardware. For example, the TensorRT EP in ONNX Runtime can take an ONNX model and run it using NVIDIA&#039;s high-performance inference library, which will use FP16 or INT8 if allowed, to maximize speed. The user can configure the EP with precision flags. If the accuracy drop is too high, one might restrict it to FP32. If it&#039;s fine, allow FP16. Tools exist to measure the accuracy difference (like calibrating int8 quantization).&lt;br /&gt;
&lt;br /&gt;
To conclude, the trade-offs revolve around picking the lowest precision that yields acceptable accuracy, and utilizing hardware features fully. ONNX being a high-level spec abstracts this, but it&#039;s ultimately up to the deployment to decide and sometimes up to the exporter to incorporate quantization. ONNX’s role is to be able to &#039;&#039;represent&#039;&#039; these choices (they have added quantized types, FP8 types, etc., to allow the graph to include those when needed).&lt;br /&gt;
&lt;br /&gt;
== Best Practices and Design Guidelines ==&lt;br /&gt;
&lt;br /&gt;
Given the above insights, here are some best practices and guidelines for dealing with numerical precision and accumulators in AI inference (and training) design:&lt;br /&gt;
&lt;br /&gt;
=== 8.1. Choosing an Accumulator Precision ===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Match precision to data length and range:&#039;&#039;&#039; If an operation accumulates a large number of terms, consider using a higher precision accumulator than the input data. For example, for summing &amp;gt;1000 values of type FP16, an FP32 accumulator is recommended to avoid large rounding error or overflow. If summing &amp;gt; million values of FP32, consider double (FP64) accumulation if high accuracy is needed (though this is rare in DL). &#039;&#039;Rule of thumb:&#039;&#039; if $N \cdot \varepsilon$ is not tiny (say &amp;gt;1e-3), you may benefit from higher precision. For FP16 ($\varepsilon \approx 1e-3$), that threshold N is a few hundred. For FP32 ($\varepsilon \approx 1e-7$), N would be in the millions before 1e-3 error.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Use higher precision in critical layers:&#039;&#039;&#039; Identify if certain layers of the model are more sensitive to precision. For instance, the output layer or a layer that feeds into a very sharp decision boundary might require more precision. If a small change there could flip the prediction, you don&#039;t want excessive numerical error. Using FP32 for logits and softmax is a common practice to maintain classification confidence stability.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Follow hardware defaults, but override if needed:&#039;&#039;&#039; Many hardware accelerators have sensible defaults (like accumulate in FP32 for tensor ops). It&#039;s usually best to use those defaults as they were chosen to balance accuracy and speed. Only override if you have evidence of a problem. For example, if using ONNX Runtime on CPU, you discovered it upcasts to float for you – great. But if using some custom accelerator that does pure FP16, you might manually insert casts in the ONNX graph around certain ops to force higher precision on those ops.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Maintain precision for gradient accumulation (training):&#039;&#039;&#039; This is more training-oriented, but worth noting: when summing gradients over batches or updating weights, use higher precision (FP32 or even 64) for those accumulations. This ensures training doesn&#039;t diverge due to numeric error. Many frameworks keep a &amp;quot;master copy&amp;quot; of weights in FP32 when training in FP16&amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;. For inference, this translates to: any running statistics or counters in the model (like if you had a running average inside the model) should probably be in higher precision to avoid drift.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Be mindful of intermediate cast costs:&#039;&#039;&#039; If you do decide to cast to higher precision for an operation in ONNX, remember it incurs memory copies and compute overhead. Make sure it&#039;s worth it. For example, if you cast a huge tensor to double, that could slow things drastically. Usually FP16-&amp;gt;FP32 is fine (hardware handles it well), but FP32-&amp;gt;FP64 might be expensive on GPUs.&lt;br /&gt;
&lt;br /&gt;
=== 8.2. Safe Reduction Lengths and Heuristics ===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Chunk long reductions:&#039;&#039;&#039; If you must do a very long sum in low precision (due to memory constraints or hardware), chunk the sum into parts, sum each to a higher precision local accumulator, then sum the partial sums. This is essentially pairwise summation. Many libraries use a fixed chunk size heuristic (e.g., sum in blocks of 256 in registers, then add those up). This bounds the error growth. If implementing manually, you can choose a chunk size such that chunk_length &#039;&#039; ε is small (like 256 &#039;&#039; 1e-3 = 0.256 for FP16, hmm maybe choose 100).&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Sort by magnitude if possible:&#039;&#039;&#039; In some cases (not typical in DL, but possible in post-processing), if you have numbers varying by orders of magnitude, summing from smallest to largest improves accuracy. It prevents small ones from being swallowed by big ones. This is more relevant if you output a sum of mixed-scale values (like some algorithm mixing big and small contributions). It’s not always feasible in real-time inference to sort, but keep in mind.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Use compensation for critical sums:&#039;&#039;&#039; If you find a particular sum or difference in your inference computation is crucial (like computing a small residual from two close numbers), consider using Kahan summation or another compensated scheme just for that part. For example, if you had an ONNX custom function that needs to sum a lot of almost cancelling terms, you might implement it with compensation internally.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Heuristics for FP8/low-bit:&#039;&#039;&#039; When using extremely low precision like FP8, adopt the recommended practices: use scaling (don&#039;t feed raw data into FP8, normalize it first), and perhaps only use FP8 on portions of the network that are empirically robust. The industry is still establishing heuristics, but one might be: use FP8 for matrix multiplications, but accumulate in FP16 or FP32; keep normalization in FP16; use FP16 for first and last layer maybe. This is evolving, so keep an eye on literature.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Keep an eye on sum length in softmax and normalization:&#039;&#039;&#039; If you have extremely large class counts or group sizes (e.g., a softmax over 1e6 items, or layer norm over tens of thousands of features), realize this is a giant reduction. You may want to ensure that reduction is done in higher precision. ONNX Softmax by default will likely not handle a million-class case in FP16 well (the sum would overflow since 1e6 &amp;gt; 65504). So either do that on CPU in FP32 or break it down (could do hierarchical softmax, etc.). This is a corner case, but relevant for huge vocab language models. In practice, vocab ~50k is fine as we said.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Test numeric differences:&#039;&#039;&#039; A practical tip: after quantizing or reducing precision, run some test inputs through both the high-precision model and low-precision model. Compare outputs (and maybe some intermediate signals if possible). If differences are consistently small relative to the application tolerance, you&#039;re good. If you see occasional large differences or instabilities (like outputs become NaN or Inf in low precision), that&#039;s a red flag. Use that testing to guide where you might need to increase precision or change algorithms.&lt;br /&gt;
&lt;br /&gt;
=== 8.3. When Higher Precision Is Required ===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Ill-conditioned calculations:&#039;&#039;&#039; If your model or pipeline performs any operation known to be ill-conditioned (matrix inversion, subtraction of nearly equal quantities, computing a tiny difference of large numbers), you likely need higher precision for that part. For example, if your ONNX model includes a custom op to solve a linear system, use double precision in that op if possible (or at least float with iterative refinement). In ML, this might occur in something like a physics-informed neural network or a differentiable simulation within the model.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Strict accuracy requirements:&#039;&#039;&#039; Some applications (medical, aerospace, finance) might have strict tolerances. If an inference model’s output needs to be correct to 5 decimal places, you probably cannot use FP16, which has only 3-4 decimal digits precision. FP32 might be barely enough (~7 digits). FP64 might be warranted to guarantee that level of accuracy in worst case. Most deep learning applications (like image classification) don&#039;t demand that level of numeric precision in outputs – as long as the top class is correct, it doesn&#039;t matter if its probability was 0.901 vs 0.899. But if you&#039;re doing something like summing probabilities to make a risk estimate or doing a cumulative sum over many predictions, be careful to accumulate precisely.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Large sums or products in post-processing:&#039;&#039;&#039; Sometimes the neural net is fine, but the &#039;&#039;post-processing&#039;&#039; of results might need precision. E.g., summing up probabilities of many events, or computing confidence intervals, etc. Do those calculations in double if needed outside the model.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Preventing drift in long sequences:&#039;&#039;&#039; For sequential models (RNNs processing thousands of steps), rounding error can accumulate over steps. If each step has a small error, after 10000 steps it could add up. In such cases, using a higher precision for the state might reduce drift. Some practitioners use FP64 for the hidden state accumulation in very long sequence processing to maintain numerical stability (though this is not common due to speed loss). Alternatively, periodically renormalize or reset small errors.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Simulations or differential equations:&#039;&#039;&#039; If your ONNX model involves simulating something (like a differential equation solver inside a model, or an ODE/RNN hybrid), those often need double precision to be stable for long durations. Recognize those situations and allocate precision accordingly.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Verification and debugging:&#039;&#039;&#039; Use higher precision as a tool to verify correctness. For example, run your model in FP64 (if possible) and compare to FP32 to see if there are differences. If not, FP32 is fine. If yes, you might find a particular calculation losing too much info in FP32, implying maybe you should keep it in FP64 in production as well. This is rare in inference but can be part of rigorous validation.&lt;br /&gt;
&lt;br /&gt;
In ONNX, using higher precision may involve casting to &amp;lt;code&amp;gt;tensor(double)&amp;lt;/code&amp;gt; for certain ops. Not all runtimes support double on GPU though (some do via emulation or just slower path). ONNX CPU does support double for general ops. So be mindful of the target.&lt;br /&gt;
&lt;br /&gt;
Finally, &#039;&#039;&#039;document your precision choices&#039;&#039;&#039;. If you deliver an ONNX model that uses mixed types (float16 inputs, float32 accumulations, int8 weights, etc.), ensure that is clear to whoever maintains or uses it. Precision issues can be sneaky, so clarity helps.&lt;br /&gt;
&lt;br /&gt;
== Limitations and Open Problems ==&lt;br /&gt;
&lt;br /&gt;
Despite advances in handling numerical precision, there remain limitations and open challenges in this domain:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Lack of Precision Specification in ONNX:&#039;&#039;&#039; As noted, ONNX currently has no way to formally specify that an op &#039;&#039;should&#039;&#039; use a higher precision internally. This can lead to nondeterministic differences across platforms&amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;. While proposals exist to add attributes for this&amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;, it is not yet standard. Thus an ONNX model might behave slightly differently on two compliant backends, and there&#039;s no built-in mechanism to ensure identical results. This is a limitation for applications requiring cross-platform consistency. Solving it may involve extending ONNX (e.g., an attribute like &amp;lt;code&amp;gt;accumulatePrecision=&amp;quot;FLOAT32&amp;quot;&amp;lt;/code&amp;gt; for MatMul) or tightening the spec, but those come with backward compatibility and implementation burden concerns.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Reproducibility and Determinism:&#039;&#039;&#039; Even with the same precision, parallel computation can introduce nondeterminism (due to different summation orders). Ensuring bit-identical results across runs (important for some sensitive domains) is non-trivial. Frameworks often provide &amp;quot;deterministic mode&amp;quot; which might use fixed ordering at cost of speed. In ONNX, there&#039;s no global flag for this; it&#039;s up to the runtime. More research could be done on algorithms that produce bit-wise identical results independent of parallel execution order (perhaps using techniques like pairwise summation or consistent partitioning). Stochastic rounding is another area: sometimes using probabilistic rounding can reduce bias in accumulated error, but it introduces randomness – reconciling that with reproducibility is tricky.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Ultra-low Precision (FP8 and below):&#039;&#039;&#039; While FP8 is being explored, going to 4-bit floats or mixed schemes is an open challenge. The OpenAI community is investigating 4-bit (quantization-wise) for LLMs with some success, but a true 4-bit float (like E3M1) would be extremely limited. Perhaps hybrid schemes (like block floating point, log scales, etc.) will come. ONNX already has some support (they mention 4-bit in documentation placeholders&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;), but it&#039;s nascent. Research into training techniques, calibration, and new number formats (like &#039;&#039;&#039;posit numbers&#039;&#039;&#039; or tapering precision floats) is ongoing. If a new format proves useful, ONNX will need to adapt to represent it.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Quantization Noise and Model Robustness:&#039;&#039;&#039; Reducing precision can sometimes expose models to issues like adversarial vulnerability or fragility – e.g., a slight perturbation might flip a decision if the margin was small and precision loss nudged it. Ensuring model robustness under quantization is an open problem. It might require re-training the model with quantization in the loop (quantization-aware training) or designing models that have large &amp;quot;margin&amp;quot; between classes to tolerate quantization noise.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Dynamic range during training vs inference:&#039;&#039;&#039; In training, activations can have wider distributions (especially early in training). Models often learn to constrain their ranges (via activation functions saturating or normalization), which makes inference more stable in lower precision. But if one tries to train in FP8 from scratch, it&#039;s very hard. This gap between what&#039;s possible in a post-trained inference model vs training is an open area – how to train directly in low precision without significant accuracy loss. Solving that could simplify workflows (no need for high-precision training then quantize; just train low-prec end to end). Techniques like loss scaling, gradient clipping, etc., are partial solutions. The development of optimizers that are resilient to low precision is ongoing.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Hardware variability:&#039;&#039;&#039; As new hardware emerges (with new numeric capabilities or quirks), ensuring that ONNX models run consistently is an ongoing task. For instance, one device might implement a fused op that reduces rounding error, another does it separately. Even something like convolution might use a different algorithm (FFT-based vs direct) which has different numeric properties. Open problem: could ONNX allow specifying numeric tolerances per op or preferred algorithms? That may be overkill for most, but for some HPC ML use cases, maybe desired.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Testing and validation of numerics:&#039;&#039;&#039; There isn&#039;t a standardized set of &amp;quot;numeric compliance&amp;quot; tests for ONNX runtimes beyond simple correctness. Perhaps in the future, test suites might include challenging numeric scenarios (large sums, nearly singular matrices) to ensure a runtime handles them gracefully (maybe by switching precision). Right now, it&#039;s mostly on the user to test their model under expected conditions.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Combining quantization with floating point:&#039;&#039;&#039; Many production models use a mix (e.g., int8 for some layers, FP16 for others). ONNX can represent this (with QuantizeLinear ops or MixedPrecision through casting), but tooling to automatically find the optimal mix is still developing. It&#039;s somewhat trial-and-error or using heuristics. An open question is how to automate precision assignment: given a model and a target hardware, how to automatically choose which layers can be int8, which should be FP16, etc., to meet an accuracy target. This crosses into NAS (neural architecture search) territory or automated quantization. &lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Numerical debugging tools:&#039;&#039;&#039; When a model goes wrong due to precision, it&#039;s often hard to pinpoint where. Better tooling to analyze numerical error layer by layer could be useful. For instance, one could run a model in double and float and diff the intermediate results to see where the largest divergence happens. Doing this systematically for big nets is an open tooling challenge. Some frameworks have hooks for this, but not standardized.&lt;br /&gt;
&lt;br /&gt;
In conclusion, while we&#039;ve come far with using lower precision in AI without sacrificing much accuracy, the landscape is always evolving. The key open problem is how to push precision lower (for efficiency gains) without crossing the threshold where accuracy falls off a cliff. Each new generation of hardware and techniques (like FP8, novel quantization schemes) extends that boundary, but also brings new questions on how to manage the numerical errors. ONNX as an interchange will need to keep pace, possibly by adding more expressive power for these numeric considerations.&lt;br /&gt;
&lt;br /&gt;
== See Also ==&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;IEEE 754 Floating-Point&#039;&#039;&#039; – The standard defining binary16, binary32, binary64 formats and rounding behavior.&lt;br /&gt;
* &#039;&#039;&#039;Floating-Point Arithmetic&#039;&#039;&#039; – General topic on how floating-point math works, common issues (overflow, underflow, cancellation).&lt;br /&gt;
* &#039;&#039;&#039;Kahan Summation Algorithm&#039;&#039;&#039; – Algorithm for compensated summation to reduce numerical error&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;.&lt;br /&gt;
* &#039;&#039;&#039;BFloat16&#039;&#039;&#039; – A short-format float with 8-bit exponent, used in TPUs and some GPUs for training networks.&lt;br /&gt;
* &#039;&#039;&#039;Mixed Precision Training&#039;&#039;&#039; – Technique of using FP16 (or BF16) along with FP32 accumulators to accelerate training&amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;.&lt;br /&gt;
* &#039;&#039;&#039;Quantization in Deep Learning&#039;&#039;&#039; – Converting networks to lower bit integer arithmetic for inference (INT8 quantization and calibration techniques).&lt;br /&gt;
* &#039;&#039;&#039;ONNX Operator Specification&#039;&#039;&#039; – Official docs for ONNX ops, which describe their mathematical function but not the required numeric precision (a point of potential extension in future).&lt;br /&gt;
* &#039;&#039;&#039;CUDA Tensor Cores &amp;amp; TF32&#039;&#039;&#039; – NVIDIA documentation on how tensor cores perform mixed precision and the TF32 format for accelerating FP32.&lt;br /&gt;
* &#039;&#039;&#039;Pairwise Summation&#039;&#039;&#039; – Summation algorithm with logN error growth&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;, used in many numerical libraries to improve accuracy of reductions.&lt;br /&gt;
* &#039;&#039;&#039;Softmax Stability&#039;&#039;&#039; – Articles on numerically stable softmax and log-sum-exp implementations to avoid overflow.&lt;br /&gt;
* &#039;&#039;&#039;Performance Tuning for Low Precision&#039;&#039;&#039; – Guides (e.g., NVIDIA or Intel whitepapers) on using low precision and the trade-offs involved in various hardware.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;references&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref1&amp;quot;&amp;gt;Accelerate and simplify Scikit-learn model inference with ONNX Runtime - Microsoft Open Source Blog — opensource.microsoft.com — https://opensource.microsoft.com/blog/2020/12/17/accelerate-simplify-scikit-learn-model-inference-onnx-runtime&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref2&amp;quot;&amp;gt;Floating Point Precision: Understanding FP64, FP32, and FP16 in Large Language Models - DEV Community — dev.to — https://dev.to/lukehinds/floating-point-precision-understanding-fp64-fp32-and-fp16-in-large-language-models-3gk6&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref3&amp;quot;&amp;gt;Floating Point Precision and Its Limitations | by Umair Akbar | Medium — akbu.medium.com — https://akbu.medium.com/floating-point-precision-and-its-limitations-cfb7247d7789&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref4&amp;quot;&amp;gt;Kahan summation algorithm - Wikipedia — en.wikipedia.org — https://en.wikipedia.org/wiki/Kahan_summation_algorithm&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref5&amp;quot;&amp;gt;Pairwise summation - Wikipedia — en.wikipedia.org — https://en.wikipedia.org/wiki/Pairwise_summation&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref6&amp;quot;&amp;gt;Numerical behavior of NVIDIA tensor cores - PMC — pmc.ncbi.nlm.nih.gov — https://pmc.ncbi.nlm.nih.gov/articles/PMC7959640/&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref7&amp;quot;&amp;gt;Mixed precision and performance-accuracy tuning (neuron-cc) — AWS Neuron Documentation — awsdocs-neuron.readthedocs-hosted.com — https://awsdocs-neuron.readthedocs-hosted.com/en/latest/about-neuron/appnotes/neuron-cc/mixed-precision.html&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref8&amp;quot;&amp;gt;Create Float16 and Mixed Precision Models - ONNX Runtime — onnxruntime.ai — https://onnxruntime.ai/docs/performance/model-optimizations/float16.html&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref9&amp;quot;&amp;gt;[Performance] ONNX FP16 model is having performance bottle neck when compared to FP32 variant · Issue #25824 · microsoft/onnxruntime · GitHub — github.com — https://github.com/microsoft/onnxruntime/issues/25824&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref10&amp;quot;&amp;gt;Float stored in 8 bits - ONNX 1.21.0 documentation — onnx.ai — https://onnx.ai/onnx/technical/float8.html&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref11&amp;quot;&amp;gt;[2209.05433] FP8 Formats for Deep Learning — arxiv.org — https://arxiv.org/abs/2209.05433&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref12&amp;quot;&amp;gt;Should MatMul op allow specification of accumulation precision? · Issue #7072 · onnx/onnx · GitHub — github.com — https://github.com/onnx/onnx/issues/7072&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref13&amp;quot;&amp;gt;Quantize ONNX models | onnxruntime — onnxruntime.ai — https://onnxruntime.ai/docs/performance/model-optimizations/quantization.html&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref14&amp;quot;&amp;gt;Keep Attention Softmax FP32 during FP16/ZeRO Training · Issue #1485 · hpcaitech/ColossalAI · GitHub — github.com — https://github.com/hpcaitech/ColossalAI/issues/1485&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;/references&amp;gt;&lt;/div&gt;</summary>
		<author><name>Timo.stripf</name></author>
	</entry>
	<entry>
		<id>https://www.emmtrix.com/w139/index.php?title=Numerical_Precision_in_ONNX_and_AI_Inference&amp;diff=2891</id>
		<title>Numerical Precision in ONNX and AI Inference</title>
		<link rel="alternate" type="text/html" href="https://www.emmtrix.com/w139/index.php?title=Numerical_Precision_in_ONNX_and_AI_Inference&amp;diff=2891"/>
		<updated>2026-01-30T15:19:26Z</updated>

		<summary type="html">&lt;p&gt;Timo.stripf: /* 3.2. Pairwise and Tree-Based Reductions */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&#039;&#039;&#039;Open Neural Network Exchange (ONNX)&#039;&#039;&#039; is an open standard format for representing machine learning models and neural network computations across different frameworks and hardware&amp;lt;ref name=&amp;quot;ref1&amp;quot;/&amp;gt;. As models are exported and deployed via ONNX, the &#039;&#039;&#039;numerical precision&#039;&#039;&#039; of computations becomes critical. Deep learning inference involves a variety of floating-point operations, and small numerical discrepancies can accumulate and affect model accuracy or reproducibility. This article provides an in-depth overview of floating-point precision issues in numerical computing, with a focus on how they manifest in AI inference and the ONNX ecosystem. We cover floating-point formats and rounding behavior, accumulation strategies for summations, the use of low-precision formats (FP16, BFloat16, FP8) in modern AI, and how ONNX defines (or leaves unspecified) the precision of accumulators in operations. Strategies for maintaining numerical stability in deep neural networks are discussed, along with the trade-offs between computational performance and arithmetic accuracy. The goal is to give engineers and researchers a comprehensive, encyclopedic reference on numerical precision in ONNX-based AI inference systems.&lt;br /&gt;
&lt;br /&gt;
== Floating-Point Precision in Numerical Computing ==&lt;br /&gt;
&lt;br /&gt;
=== 2.1. IEEE 754 Floating-Point Formats ===&lt;br /&gt;
&lt;br /&gt;
Modern computers represent real numbers using the IEEE 754 floating-point standard, which defines formats like 32-bit &#039;&#039;single precision&#039;&#039; (FP32) and 64-bit &#039;&#039;double precision&#039;&#039; (FP64). In IEEE 754 binary formats, a floating-point number is composed of three fields: 1 sign bit, several exponent bits (with a bias), and several fraction (mantissa) bits. For example, FP32 consists of 1 sign bit, 8 exponent bits, and 23 fraction bits (24 bits of significand precision including the implicit leading 1)&amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;. This provides roughly 7 decimal digits of precision and an exponent range allowing values on the order of $10^{\pm38}$. FP64 allocates 1 sign bit, 11 exponent bits, and 52 fraction bits (53-bit precision), yielding about 15–16 decimal digits of precision and a vastly larger range (~$10^{\pm308}$)&amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;. These larger formats (especially FP64) are the &amp;quot;gold standard&amp;quot; for numerical accuracy in scientific computing, as they reduce round-off error in complex calculations&amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;. However, the increased precision comes at the cost of doubled storage and typically reduced computational throughput (e.g. many GPUs execute FP64 instructions at a much lower rate than FP32&amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
In addition to single and double precision, the IEEE 754-2008 standard introduced a 16-bit &#039;&#039;half precision&#039;&#039; format (FP16, or binary16). FP16 has 1 sign bit, 5 exponent bits, and 10 fraction bits (11-bit significand including the implicit 1)&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. This format covers a much smaller dynamic range (approximately $6.1\times10^{-5}$ to $6.5\times10^4$ for normalized values&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;) and offers only around 3 decimal digits of precision. Half precision was originally used in graphics, but it has gained popularity in machine learning for its speed and memory advantages (discussed later in Section 4). Another format relevant to AI is &#039;&#039;&#039;BFloat16&#039;&#039;&#039; (Brain Float 16), a 16-bit float with &#039;&#039;&#039;8 exponent bits and 7 fraction bits&#039;&#039;&#039;&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. BFloat16 sacrifices precision (only ~7 fraction bits ≈ 2–3 decimal digits) in order to have the &#039;&#039;&#039;same exponent range as FP32&#039;&#039;&#039;&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. This means BFloat16 can represent very large or very small numbers (∼$10^{\pm38}$, similar range as FP32) but with much less precision between representable values&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. BFloat16 was introduced for deep learning by Google (for TPUs) to allow reduced precision &#039;&#039;training&#039;&#039; without frequent overflow/underflow, relying on the wide range and accepting the coarse precision.&lt;br /&gt;
&lt;br /&gt;
Each floating-point format has a &#039;&#039;&#039;unit roundoff&#039;&#039;&#039; or machine precision, denoted $\epsilon$. This is effectively the spacing between 1.0 and the next representable number. For FP32, $\epsilon \approx 2^{-23} \approx 1.19\times10^{-7}$, corresponding to about 7–8 decimal digits of precision&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. In FP64, $\epsilon = 2^{-52} \approx 2.22\times10^{-16}$ (15–16 decimal digits). Half precision is much less precise: FP16 has $\epsilon = 2^{-10} \approx 9.76\times10^{-4}$&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;, and BFloat16 has $\epsilon = 2^{-7} = 7.8125\times10^{-3}$&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. These values quantify how finely the continuum of real numbers is quantized in each format.&lt;br /&gt;
&lt;br /&gt;
=== 2.2. Machine Epsilon and Rounding Error ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Machine epsilon ($\varepsilon$)&#039;&#039;&#039; is formally defined as the smallest positive number such that $1.0 + \varepsilon &amp;gt; 1.0$ in the floating-point format&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. It effectively measures the &#039;&#039;&#039;precision&#039;&#039;&#039; of the representation. In FP32, $\varepsilon = 2^{-23} \approx 1.19\times10^{-7}$, meaning any result is rounded to about 7 decimal digits. FP16’s $\varepsilon = 2^{-10} \approx 9.77\times10^{-4}$, over 1000× larger (so only ~3 decimal digits of precision), and BFloat16’s $\varepsilon = 2^{-7} \approx 7.8\times10^{-3}$, reflecting its mere ~8-bit precision&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. Rounding in IEEE 754 by default uses &#039;&#039;&#039;round-to-nearest (ties to even)&#039;&#039;&#039;, which ensures the rounding error for any single operation is at most half a unit in the last place (0.5 ULP). We can model a floating-point addition as: &lt;br /&gt;
&lt;br /&gt;
$$\text{fl}(a+b) = (a+b)\,(1 + \delta),$$ &lt;br /&gt;
&lt;br /&gt;
where $|\delta| \le \varepsilon$ for that format&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. Here $\varepsilon/2$ is the maximum &#039;&#039;&#039;relative error&#039;&#039;&#039; in one rounding (also called unit roundoff). For example, when adding two FP32 numbers, the result is accurate to within about $10^{-7}$ of the true sum (relative), whereas in BF16, the result can deviate by up to $\sim7.8\times10^{-3}$ (nearly 0.78% relative error per operation)&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. Thus, &#039;&#039;&#039;each arithmetic operation incurs a small rounding error&#039;&#039;&#039; (on the order of machine epsilon) that can propagate or accumulate through a sequence of calculations.&lt;br /&gt;
&lt;br /&gt;
Because floating-point representations have finite exponent range, we must also consider &#039;&#039;&#039;overflow&#039;&#039;&#039; and &#039;&#039;&#039;underflow&#039;&#039;&#039;. If a result’s magnitude exceeds the maximum representable value, it overflows to infinity; if it falls below the minimum normal (or minimum subnormal) value, it underflows to zero (or a denormalized tiny value). FP32 and BF16 share an exponent size (8 bits), so their range for normal numbers is roughly $[10^{-38}, 10^{38}]$ (approx $~\!3.4\times10^{38}$ max)&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. FP16’s 5-bit exponent allows max $\sim 6.55\times10^4$ and min normal $\sim 6.10\times10^{-5}$&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. In practice, &#039;&#039;&#039;underflow/overflow&#039;&#039;&#039; can occur in low-precision arithmetic if values are not appropriately scaled. For instance, summing a large number of moderately sized terms in FP16 could overflow where FP32 would not. As an example, summing on the order of $10^4$ values of magnitude ~$10^1$ (say, adding 10,000 terms around 10 each) would produce a total $\sim10^5$, which exceeds FP16’s max finite value ~$6.5\times10^4$ and would overflow to $+\infty$ in half precision&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. Underflow is usually a concern when subtracting nearly equal numbers (catastrophic cancellation yielding a tiny difference that might flush to zero in low precision).&lt;br /&gt;
&lt;br /&gt;
=== 2.3. Accumulation Error in Reductions ===&lt;br /&gt;
&lt;br /&gt;
Floating-point addition is &#039;&#039;&#039;not associative&#039;&#039;&#039;: $(a+b)+c$ can differ from $a+(b+c)$ due to rounding, and thus the order in which we sum a list of numbers can change the result slightly. When summing a large sequence of $N$ numbers $x_1, x_2, ..., x_N$, the &#039;&#039;&#039;accumulated rounding error&#039;&#039;&#039; can be significant. In the &#039;&#039;worst case&#039;&#039; (e.g., adding many positive and negative terms that cause maximal cancellation or always rounding one way), the error can grow on the order of $O(N \varepsilon)$ relative to the exact sum&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;. Simply summing $N$ numbers in sequence (the &#039;&#039;&#039;naive linear summation&#039;&#039;&#039;) has a worst-case error bound proportional to $N\,\varepsilon$&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. This worst-case occurs for adversarial arrangements of values; in more typical scenarios with rounding errors of random signs, one can model the error as a random walk, yielding an expected root-mean-square (RMS) error growth on the order of $O(\sqrt{N}\,\varepsilon)$&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;. Still, with very large $N$, even $\sqrt{N}$ times $\varepsilon$ can become large enough to matter.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Cancellation&#039;&#039;&#039; is a particular problem: if we add two nearly equal numbers of opposite sign, most leading digits cancel and the result loses significant bits of precision. For example, in single precision, subtracting two numbers that agree in the first 6 decimal digits will yield a result with only ~1 digit of accuracy left. Catastrophic cancellation can dramatically increase relative error. Summation is especially sensitive to cancellation if large positive and negative terms are summed naively. The &#039;&#039;&#039;condition number&#039;&#039;&#039; of a summation (the ratio of the sum of absolute values $\sum|x_i|$ to the absolute value of the true sum $|\sum x_i|$) indicates how sensitive the result is to perturbations&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;. If the numbers have mixed signs and nearly cancel out (making the condition number large), even an optimal summation order will have a large relative error. For instance, summing [1.0, $10^{100}$, 1.0, $-10^{100}$] in double precision has an exact sum of 2.0, but naive summation may yield 0.0 due to cancellation; an improved summation algorithm yields the correct 2.0&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
In practical terms, when summing a long list of floating-point numbers (such as accumulating a dot product or reducing a tensor), the &#039;&#039;&#039;accumulation error&#039;&#039;&#039; can become noticeable if no precautions are taken. Techniques to mitigate this error include carefully choosing the summation order or using higher precision for intermediate sums. We will explore these strategies in the next section.&lt;br /&gt;
&lt;br /&gt;
== Accumulators and Reduction Strategies ==&lt;br /&gt;
&lt;br /&gt;
When performing reductions (sums, dot products, etc.), the method of accumulation can greatly affect the accuracy of the result. Below, we discuss various strategies:&lt;br /&gt;
&lt;br /&gt;
=== 3.1. Naive Linear Accumulation ===&lt;br /&gt;
&lt;br /&gt;
The simplest accumulation strategy is to use a single accumulator and add each element sequentially. In C-like pseudocode:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;c&amp;quot;&amp;gt;&lt;br /&gt;
float sum = 0.0f;&lt;br /&gt;
for (int i = 0; i &amp;lt; N; ++i) {&lt;br /&gt;
    sum += array[i];&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This &#039;&#039;&#039;naive linear summation&#039;&#039;&#039; adds values one by one to the running total. While straightforward and fast, it is prone to the rounding error accumulation described earlier. The partial sum &amp;lt;code&amp;gt;sum&amp;lt;/code&amp;gt; continually grows (in absolute value) as terms are added, so new small terms may fall outside its precision. For example, if &amp;lt;code&amp;gt;sum&amp;lt;/code&amp;gt; is large and positive, adding a comparatively tiny positive number may produce no change because the small term&#039;s significant digits vanish in the presence of the large sum (the increment is below the rounding threshold of &amp;lt;code&amp;gt;sum&amp;lt;/code&amp;gt;). Conversely, adding a small number of opposite sign can suffer from cancellation. The order of addition matters: summing from smallest magnitude to largest (ascending order) tends to yield a more accurate result than summing in descending order, because adding small terms first preserves their contributions before the sum becomes very large. The naive loop above implicitly sums in the given index order; if the data is unsorted, this order is essentially arbitrary.&lt;br /&gt;
&lt;br /&gt;
The performance of linear accumulation is excellent (minimal overhead), and for many applications the resulting error is within acceptable limits. However, for &#039;&#039;&#039;very large N&#039;&#039;&#039; or ill-conditioned sums, the inaccuracy can become a problem. In a reduction involving millions of elements, a single-precision sum could lose several digits of accuracy. In deep learning, this could translate to small discrepancies in layer outputs or aggregated gradients if done naively in low precision.&lt;br /&gt;
&lt;br /&gt;
=== 3.2. Pairwise and Tree-Based Reductions ===&lt;br /&gt;
&lt;br /&gt;
A better approach to summation is &#039;&#039;&#039;pairwise summation&#039;&#039;&#039;, which sums numbers in a binary tree structure rather than linearly. The idea is to recursively split the array into halves, sum each half, then add the two partial sums. This can be implemented as:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;!--&lt;br /&gt;
&amp;lt;syntaxhighlight&amp;gt;&lt;br /&gt;
function pairwise_sum(x[1..n]):&lt;br /&gt;
    if n == 0:&lt;br /&gt;
        return 0&lt;br /&gt;
    if n == 1:&lt;br /&gt;
        return x[1]&lt;br /&gt;
    m = floor(n/2)&lt;br /&gt;
    left_sum  = pairwise_sum(x[1..m])&lt;br /&gt;
    right_sum = pairwise_sum(x[m+1..n])&lt;br /&gt;
    return left_sum + right_sum&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
--&amp;gt;&lt;br /&gt;
&lt;br /&gt;
By always adding numbers of similar magnitudes first (small subsets are reduced to subtotals, then those are added, etc.), pairwise summation avoids the situation of a very large running total absorbing small additions until the final steps. The error of pairwise summation grows logarithmically with $N$ in the worst case (roughly $O(\varepsilon \log_2 N)$)&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;, instead of linearly. Intuitively, at each level of the tree, rounding error is constrained, and there are about $\log_2 N$ levels. In fact, the worst-case relative error bound for pairwise summation (base case of 1 element) is:&lt;br /&gt;
&lt;br /&gt;
\[ |E_N| \le \frac{\varepsilon \log_2 N}{1 - \varepsilon \log_2 N} \sum_{i=1}^N |x_i|, \]&lt;br /&gt;
&lt;br /&gt;
which for practical $N$ simplifies to on the order of $\varepsilon \log_2 N$ times the condition number of the sum&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;. In typical random-error scenarios, the error behaves even better (on the order of $\sqrt{\log N}$)&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;. The key is that &#039;&#039;&#039;pairwise (tree) reduction balances the accumulations&#039;&#039;&#039;, preventing one sum from overwhelming the others too early.&lt;br /&gt;
&lt;br /&gt;
From a performance standpoint, pairwise summation has the same $O(N)$ total additions as linear summation, just a different order. It is also highly amenable to parallelization: independent partial sums can be computed in parallel at each level of the tree. Many numerical libraries and BLAS implementations use a form of blocked or pairwise summation in dot products for improved accuracy at negligible performance cost. For example, NumPy and Julia perform partial pairwise summation by default for better numerical behavior&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=== 3.3. Compensated Summation (Kahan, Neumaier) ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Compensated summation&#039;&#039;&#039; is an alternative technique to improve summation accuracy by tracking the small “lost” quantities. The most famous algorithm is the &#039;&#039;&#039;Kahan summation algorithm&#039;&#039;&#039; (also called Kahan–Babuška)&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. Kahan summation uses an extra variable to accumulate the round-off error. In pseudocode:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;c&amp;quot;&amp;gt;&lt;br /&gt;
double sum = 0.0;&lt;br /&gt;
double c = 0.0;    // compensation for lost low-order bits&lt;br /&gt;
for (i = 1; i &amp;lt;= N; ++i) {&lt;br /&gt;
    double y = x[i] - c;    // recover low-order bits by subtracting compensation&lt;br /&gt;
    double t = sum + y;     // perform the addition&lt;br /&gt;
    c = (t - sum) - y;      // compute new compensation (the error in t)&lt;br /&gt;
    sum = t;&lt;br /&gt;
}&lt;br /&gt;
return sum;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Here &amp;lt;code&amp;gt;c&amp;lt;/code&amp;gt; accumulates the tiny leftovers that would otherwise be dropped&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. Each iteration essentially subtracts the last error from the new addend (&amp;lt;code&amp;gt;y = x[i] - c&amp;lt;/code&amp;gt;) so that &amp;lt;code&amp;gt;t = sum + y&amp;lt;/code&amp;gt; includes the small bits from the previous operation. The new error is then stored in &amp;lt;code&amp;gt;c&amp;lt;/code&amp;gt; for the next iteration. The result is that the final &amp;lt;code&amp;gt;sum&amp;lt;/code&amp;gt; is as if computed with higher precision than the machine’s: Kahan’s algorithm can &#039;&#039;&#039;greatly reduce the error&#039;&#039;&#039; in summing a sequence of floats. In fact, with exact arithmetic for the compensation, Kahan summation yields a result accurate to within 1 or 2 ULP of the true sum, regardless of N (assuming the sequence of partial sums doesn’t overflow)&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. More precisely, Kahan summation achieves a &#039;&#039;&#039;worst-case error independent of N&#039;&#039;&#039; (error bound depends only on machine precision, not the number of terms)&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. In practice, using a double accumulator for summing single-precision numbers via Kahan can give near double-precision accuracy.&lt;br /&gt;
&lt;br /&gt;
An important subtlety is that the compensation variable &amp;lt;code&amp;gt;c&amp;lt;/code&amp;gt; should be kept in &#039;&#039;full precision&#039;&#039;. In the implementation above, we used &amp;lt;code&amp;gt;double&amp;lt;/code&amp;gt; for both sum and c. If summing FP32 values, one might use &amp;lt;code&amp;gt;float sum&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;float c&amp;lt;/code&amp;gt; (both 32-bit), which still improves accuracy, but using a higher precision for the accumulator and compensation (i.e. double) yields even better results. Many implementations of Kahan summation use the same type for sum and c, but it&#039;s assumed to be higher precision than the inputs when possible.&lt;br /&gt;
&lt;br /&gt;
Kahan’s algorithm carries a performance cost: each iteration does five arithmetic operations (two adds, one subtract, etc.) and has a data dependency, which can hinder parallelism or pipelining&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. Thus, it runs slower than naive summation (roughly 2–3× slower or more, depending on CPU pipeline). In loops where summation is a bottleneck, this may be significant. Nonetheless, for modest N or where maximum accuracy is required, compensated summation is extremely useful. In fact, Python’s &amp;lt;code&amp;gt;math.fsum&amp;lt;/code&amp;gt; function uses a variant of Kahan’s algorithm (actually a more advanced multi-part compensation) to sum iterables with high precision.&lt;br /&gt;
&lt;br /&gt;
There are variations and improvements on basic Kahan summation. &#039;&#039;&#039;Neumaier’s algorithm&#039;&#039;&#039; (sometimes called &#039;&#039;Kahan-Babuška-Neumaier&#039;&#039;) improves Kahan’s method by also handling cases where the next input is larger in magnitude than the running sum&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. In such cases, the original Kahan formula could suffer error; Neumaier’s version effectively swaps roles when a huge value comes in. Its pseudocode is:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight&amp;gt;&lt;br /&gt;
function NeumaierSum(array):&lt;br /&gt;
    sum = 0.0&lt;br /&gt;
    compensation = 0.0&lt;br /&gt;
    for each x in array:&lt;br /&gt;
        t = sum + x&lt;br /&gt;
        if (fabs(sum) &amp;gt;= fabs(x))&lt;br /&gt;
            compensation += (sum - t) + x;&lt;br /&gt;
        else&lt;br /&gt;
            compensation += (x - t) + sum;&lt;br /&gt;
        sum = t;&lt;br /&gt;
    return sum + compensation;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This algorithm ensures the compensation covers both scenarios: when the sum is the bigger addend or when the new term is bigger&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. In the end, it adds the final &amp;lt;code&amp;gt;compensation&amp;lt;/code&amp;gt; once to the result. Neumaier’s method can produce the correct result in cases where basic Kahan fails. For instance, summing [1.0, $+10^{100}$, 1.0, $-10^{100}$] in double precision: regular Kahan summation might give 0.0 (losing the small terms), whereas Neumaier’s algorithm yields 2.0 exactly&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. There are also higher-order compensation methods (carrying multiple compensation terms, e.g., the &#039;&#039;&#039;Klein summation&#039;&#039;&#039; variant&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt; or using arbitrary precision for intermediate sums) that further reduce error at increased computational cost&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
In summary, compensated summation algorithms trade extra computation for greatly improved numerical accuracy. They are especially valuable when summing large arrays of numbers with varying magnitudes, where naive summation could lose most of the low-order bits. Many scientific computing environments provide compensated sum routines for this reason. In deep learning, however, a different approach to improved precision is often taken: &#039;&#039;&#039;using a wider accumulator type&#039;&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
=== 3.4. Mixed-Precision Accumulation ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Mixed-precision accumulation&#039;&#039;&#039; refers to performing arithmetic in a higher precision than the input data. A common scenario is accumulating sums in 32-bit floats when the operands are 16-bit floats. Rather than implementing a compensation algorithm, one can simply use a wider accumulator so that intermediate rounding error is smaller. This is effectively what modern GPUs do for tensor operations: for example, NVIDIA &#039;&#039;&#039;Tensor Cores operate on FP16 input data with an FP32 accumulator&#039;&#039;&#039; (the 16-bit products are accumulated in 32-bit)&amp;lt;ref name=&amp;quot;ref6&amp;quot;/&amp;gt;. In this way, summing 2048 products in a dot product has far less precision loss than it would with a 16-bit accumulator. Hardware that supports mixed precision can give the best of both worlds: fast low-precision multiplies and high-precision accumulation.&lt;br /&gt;
&lt;br /&gt;
Software can also leverage mixed precision. For instance, one can sum an array of &amp;lt;code&amp;gt;float&amp;lt;/code&amp;gt; values into a &amp;lt;code&amp;gt;double&amp;lt;/code&amp;gt; variable, accumulating the result in double precision and then converting back to float at the end:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;c&amp;quot;&amp;gt;&lt;br /&gt;
double sum = 0.0;&lt;br /&gt;
for (int i=0; i&amp;lt;N; ++i) {&lt;br /&gt;
    sum += (double) arr_float[i];&lt;br /&gt;
}&lt;br /&gt;
float result = (float) sum;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This approach often yields a more accurate &amp;lt;code&amp;gt;result&amp;lt;/code&amp;gt; than summing in single precision directly, because the intermediate sum carries 52-bit precision. The final rounding to float happens only once at the end (minimizing overall rounding error). In fact, the above is one way to implement &amp;lt;code&amp;gt;math.fsum&amp;lt;/code&amp;gt; in Python (it internally does something similar with multiple partial sums in double precision).&lt;br /&gt;
&lt;br /&gt;
In machine learning training and inference, &#039;&#039;&#039;mixed precision&#039;&#039;&#039; is widely used. When using FP16 for speed, it&#039;s common to accumulate with FP32. For example, &#039;&#039;&#039;Google&#039;s TPUs use BFloat16 for multiplication but accumulate in FP32&#039;&#039;&#039; for matrix ops&amp;lt;ref name=&amp;quot;ref6&amp;quot;/&amp;gt;. AWS’s Inferentia (Neuron) hardware similarly supports FP16/BF16 matrix multiplication with FP32 accumulation&amp;lt;ref name=&amp;quot;ref7&amp;quot;/&amp;gt;. The motivation is that summing hundreds or thousands of half-precision values would otherwise introduce intolerable error or risk overflow. With an FP32 accumulator, the reduction error is drastically reduced (by about $2^{13}$ times, since FP32 has 13 more precision bits than FP16). Mixed-precision accumulation thus provides a simple and effective form of compensation: &#039;&#039;&#039;use a larger bucket to collect many small contributions&#039;&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
There is a spectrum of options: summing int8 or int16 integers might use 32-bit integers as accumulators for the same reason (to avoid overflow). Some BLAS libraries even offer &#039;&#039;extended precision accumulators&#039;&#039; for single precision dot products (accumulating in double). The downside of mixed precision is the extra memory and maybe throughput cost of the wider type. But on many platforms, the add latency is dominated by hardware design rather than bit-width, so using a 32-bit adder vs 16-bit has negligible speed impact if supported natively. The key is whether data movement or register pressure increases.&lt;br /&gt;
&lt;br /&gt;
In summary, &#039;&#039;&#039;using a higher precision accumulator is often the easiest way to improve numerical accuracy&#039;&#039;&#039; for reductions. It is a primary strategy in deep learning hardware to maintain stability while exploiting low precision for storage and initial computations.&lt;br /&gt;
&lt;br /&gt;
== Precision Formats in AI and Machine Learning ==&lt;br /&gt;
&lt;br /&gt;
Modern AI systems use a variety of numeric formats to balance speed, memory, and accuracy. We discuss common precisions and their roles:&lt;br /&gt;
&lt;br /&gt;
=== 4.1. FP32 and FP64 in Training and Inference ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;FP32 (single precision)&#039;&#039;&#039; has been the workhorse of deep learning for years. Most training scripts in frameworks like TensorFlow and PyTorch used FP32 by default, as it offers sufficient precision to propagate gradients and accumulate weight updates over millions of iterations without excessive error. With ~7 decimal digits of precision and a wide exponent range, FP32 can handle the dynamic range of activations and gradients in typical neural networks. In inference, FP32 is often more precision than strictly necessary for final predictions, but it has remained common because of its ease and the fact that many GPU operations were optimized for FP32.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;FP64 (double precision)&#039;&#039;&#039; is rarely used in deep learning model training or inference, except in specific scenarios requiring extreme precision. FP64 is common in scientific computing and certain types of numerical simulations. In neural networks, however, the extra precision is usually not needed for model accuracy—using doubles tends to yield negligible improvements in model metrics but at a large performance cost (since on GPUs FP64 throughput can be 1/2, 1/8, or even 1/32 of FP32 throughput depending on hardware). One exception might be when integrating neural nets into larger physics simulations or when solving ill-conditioned problems as part of the model. Some researchers use FP64 to diagnose numerical issues or ensure reproducibility. Generally though, deep learning has favored lower precision for efficiency, and FP64 is considered overkill for most networks&amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
That said, certain classical machine learning algorithms benefit from double precision. For example, computing the inverse of a matrix (as in a Gaussian Process regression or Kalman filter embedded in a model) can incur large rounding errors in single precision, so using FP64 can significantly improve the result&amp;lt;ref name=&amp;quot;ref1&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref1&amp;quot;/&amp;gt;. The ONNX standard supports a float64 tensor type for such needs. In ONNX Runtime, however, emphasis has been on FP32 performance, and some pipelines default to float even if the original model was double&amp;lt;ref name=&amp;quot;ref1&amp;quot;/&amp;gt;. If a model’s prediction computation involves operations that are numerically sensitive (like matrix inversion, Cholesky decomposition, or summing extremely large counts), then FP64 may be required to avoid noticeable discrepancies&amp;lt;ref name=&amp;quot;ref1&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref1&amp;quot;/&amp;gt;. A good practice is to test a model in both float and double if you suspect precision issues; significant differences indicate an ill-conditioned calculation that might need higher precision.&lt;br /&gt;
&lt;br /&gt;
In summary, FP32 remains the &#039;&#039;&#039;default&#039;&#039;&#039; format for most training and inference due to its balance of accuracy and efficiency&amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;, while FP64 is reserved for niche cases or precision-critical subroutines. FP32 typically achieves the same accuracy as FP64 for end-to-end deep learning tasks, because network training can often compensate for small noise, and other sources of error (like generalization error) dominate. Therefore, the community trend has been moving to &#039;&#039;&#039;lower&#039;&#039;&#039; precisions, not higher, to gain speed — provided the models still converge and perform well.&lt;br /&gt;
&lt;br /&gt;
=== 4.2. FP16 and BFloat16 in Deep Learning ===&lt;br /&gt;
&lt;br /&gt;
To further speed up neural network training and inference, &#039;&#039;&#039;half-precision (FP16)&#039;&#039;&#039; was adopted, especially with the advent of GPUs supporting FP16 math at twice the rate of FP32. Using FP16 reduces memory storage and bandwidth by 50% compared to FP32, which is significant for large models. However, FP16’s small exponent range and precision posed challenges: training a large network purely in FP16 can fail due to gradient underflow/overflow or weight updates not registering (because changes are below the 0.001 quantization step). To address this, frameworks introduced &#039;&#039;&#039;mixed-precision training&#039;&#039;&#039;, where compute-intensive parts (matrix multiplies, convolutions) run in FP16, but certain accumulations and model parameters remain in FP32. For instance, NVIDIA’s &#039;&#039;&#039;Tensor Core&#039;&#039;&#039; units require FP16 (or BF16) inputs but accumulate results in FP32, effectively ensuring that reductions are done in higher precision&amp;lt;ref name=&amp;quot;ref6&amp;quot;/&amp;gt;. Research showed that with proper loss scaling and mixed precision, models can train to FP32-level accuracy using mostly FP16 arithmetic, achieving significant speedups.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;BFloat16 (BF16)&#039;&#039;&#039; emerged as an alternative 16-bit format primarily from Google’s TPU training systems. BF16 has a larger exponent (8 bits, same as FP32) but only 7 fraction bits&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. This means BF16 can handle the &#039;&#039;&#039;same dynamic range&#039;&#039;&#039; as FP32 (no overflows where FP32 wouldn’t) but at the cost of even lower precision than FP16 (BF16 has about 2–3 decimal digits of precision vs FP16’s ~3–4). Despite its coarse precision, BF16 turns out to be effective for training deep networks because the algorithms (stochastic gradient descent, etc.) are somewhat robust to that noise, and having the wide range prevents catastrophic loss of signal (no gradient will overflow to Inf unless it was going to in FP32 anyway). Many training frameworks (PyTorch, TensorFlow) support BF16 training on hardware like &#039;&#039;&#039;TPU, NVIDIA A100+, Intel CPUs with AVX-512 BF16, etc&#039;&#039;&#039;. Typically, when using BF16, &#039;&#039;&#039;accumulations are still done in FP32&#039;&#039;&#039; (for example, a TPU v2/v3 multiplies BF16 matrices and accumulates into an FP32 result for the dot product&amp;lt;ref name=&amp;quot;ref6&amp;quot;/&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
In inference, both FP16 and BF16 are used to deploy models for faster speed or lower memory. FP16 is popular on NVIDIA GPUs (which have specialized FP16 units). BF16 is advantageous on TPUs and some CPUs that support it, and increasingly on GPUs (Ampere and newer GPUs also support BF16 arithmetic). The choice between FP16 and BF16 can depend on hardware: BF16’s advantage is easier software conversion (you can often take an FP32-trained model and cast weights to BF16 with minimal loss, since the dynamic range is intact), whereas FP16 might require adjusting the model or ensuring values fit in range (sometimes needing techniques like maintaining a master copy of weights in FP32 during training, or handling out-of-range values).&lt;br /&gt;
&lt;br /&gt;
For ONNX, both Float16 and BFloat16 types are part of the standard. ONNX models can be converted to FP16 (truncating weights and inserting cast nodes for computations) using tools like &amp;lt;code&amp;gt;float16_converter&amp;lt;/code&amp;gt; in ONNX Runtime&amp;lt;ref name=&amp;quot;ref8&amp;quot;/&amp;gt;. ONNX Runtime on GPUs will typically execute those FP16 ops with mixed precision on tensor cores (FP16 input, FP32 accumulate), whereas on CPU it may emulate FP16 or upcast to FP32 (as many CPU implementations do not natively support half-precision math)&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;. In fact, as an example, running an ONNX model in FP16 on a CPU might insert hidden Cast operations to FP32 behind the scenes, because the CPU kernel for that op only exists in FP32&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;. This ensures correctness (the computation is done in higher precision) but can reduce performance. We will revisit such backend-dependent behavior in Section 5.3.&lt;br /&gt;
&lt;br /&gt;
In summary, &#039;&#039;&#039;FP16 and BF16&#039;&#039;&#039; are now established as important low-precision formats in deep learning. FP16 offers more precision bits, which can matter for e.g. small differences, but has a limited range (care needed to avoid overflow). BF16 offers safety in range but less precision (which can slightly affect convergence or final accuracy in some cases). Both typically rely on FP32 for accumulation to achieve acceptable accuracy. When deploying models, one should choose a format supported efficiently by the target hardware and verify that the accuracy is not significantly degraded. Often, CNNs and transformers can run in FP16/BF16 with virtually no drop in score, but certain operations (like softmax, see Section 6.3) might need special handling.&lt;br /&gt;
&lt;br /&gt;
=== 4.3. FP8 Formats (E4M3, E5M2) and Scaling ===&lt;br /&gt;
&lt;br /&gt;
The push for ever lower precision has led to experimental &#039;&#039;&#039;8-bit floating point (FP8)&#039;&#039;&#039; formats for deep learning. In 2022, researchers from NVIDIA, Intel, and Arm proposed an FP8 standard with two variants: &#039;&#039;&#039;E4M3&#039;&#039;&#039; (4 exponent bits, 3 fraction bits) and &#039;&#039;&#039;E5M2&#039;&#039;&#039; (5 exponent bits, 2 fraction bits)&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;. Both have 1 sign bit, for a total of 8 bits. E5M2 has a wider exponent range (5 bits gives bias 15) but one fewer mantissa bit than E4M3. The idea is to use E4M3 for numbers that need more precision (like weights or activations around 1.0) and E5M2 for numbers that need more dynamic range (like gradient updates which can be very small or large). In fact, the study found that using E4M3 for forward activations/weights and E5M2 for backward gradients can allow training of large networks with FP8 to accuracy on par with FP16 training&amp;lt;ref name=&amp;quot;ref11&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
FP8 formats are &#039;&#039;not&#039;&#039; IEEE 754 standard (though IEEE is considering lower precision in its 2019 revision). Notably, the E4M3 format as used by NVIDIA does not allocate any bit-pattern for $+\infty$ or $-\infty$ – it has only NaN for invalid results, using that freed bit pattern to extend the representable range by one exponent value&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;. Essentially, E4M3 can represent numbers up to ~$2^{15}$ (around 3e4) instead of capping at infinity at $2^{16}$, by foregoing infinities&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;. Variations also exist regarding negative zero and how NaNs are handled&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;. For instance, GraphCore’s IPU uses FP8 formats that disallow negative zero and infinities, to maximize useable range&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Using 8-bit floats for neural networks requires additional techniques, chiefly &#039;&#039;&#039;scaling&#039;&#039;&#039;. Because 8-bit has very limited precision and range, one typically applies a power-of-two scale factor to groups of values to normalize them into an FP8-friendly range. This is akin to block floating-point or “microscaling” per layer or per tensor. For example, one might keep a FP32 scale for each channel or tensor, and represent values as $(FP8\_value) \times 2^{scale}$. The ONNX standard has introduced support for FP8 in version 1.15, including types for E4M3 and E5M2 (with variations FNUZ: finite-only and no negative zero)&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;, and even a type called &#039;&#039;&#039;E8M0&#039;&#039;&#039; which is an 8-bit exponent-only number used purely as a scale factor&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;. The E8M0 type has 8 exponent bits, no mantissa, effectively representing powers of two from $2^{-127}$ to $2^{128}$ for scaling purposes&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;. This indicates an approach where an FP8 tensor would be accompanied by an E8M0 scale (or scales) to interpret its value correctly.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Rounding&#039;&#039;&#039; becomes critical when casting values to FP8. The quantization error is much larger relative to FP16 or FP32. Studies have shown that choosing the right rounding mode (e.g., round-to-nearest, stochastic rounding, etc.) and clamping behavior (saturating to max instead of inf) can affect model accuracy&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;. ONNX added a &amp;lt;code&amp;gt;round_mode&amp;lt;/code&amp;gt; attribute to its Cast operator to allow controlling this for FP8&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;. In particular, one paper found that &#039;&#039;&#039;rounding up with saturation&#039;&#039;&#039; (round-to-nearest ties away from 0, and clamp overflows to max finite rather than inf) gave better accuracy in large language model training than other rounding modes&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
At present, FP8 is on the cutting edge. NVIDIA’s H100 GPU supports FP8 tensor cores (for both E4M3 and E5M2), and early results show minimal accuracy loss on some vision and NLP tasks when properly tuned. However, using FP8 requires fine-tuning hyperparameters like scaling factors for each layer and perhaps tweaking training procedures. ONNX’s inclusion of FP8 types means you can represent and transport models using FP8 weights or activations, but actual hardware support is limited and typically paired with calibration or dynamic scaling logic outside of ONNX graph (or using custom ONNX functions for scaling). It remains an &#039;&#039;&#039;open research area&#039;&#039;&#039; how to fully automate FP8 quantization for arbitrary models without accuracy loss.&lt;br /&gt;
&lt;br /&gt;
In summary, FP8 formats promise further compression of neural network computation and data. They are an order of magnitude more error-prone than FP16, but when combined with careful scaling, mixed usage of E4M3/E5M2, and maybe modified training techniques, they have shown surprising success. ONNX is actively evolving to support these ultra-low precision types, signaling their potential importance in future AI inference deployments.&lt;br /&gt;
&lt;br /&gt;
== Accumulation Semantics in ONNX ==&lt;br /&gt;
&lt;br /&gt;
One of the roles of the ONNX specification is to define the mathematical behavior of operators (Conv, MatMul, Sum, etc.) in a framework-agnostic way. However, ONNX mostly defines &#039;&#039;what&#039;&#039; an operator computes in exact arithmetic, not &#039;&#039;how&#039;&#039; the computation is carried out in finite precision. This leaves certain details – like accumulator precision or summation order – up to the implementation (runtime or hardware). We&#039;ll examine how ONNX treats accumulation and what guarantees (or lack thereof) are provided.&lt;br /&gt;
&lt;br /&gt;
=== 5.1. Operator Definitions and Numerical Guarantees ===&lt;br /&gt;
&lt;br /&gt;
Each ONNX operator is described as performing an ideal mathematical function on its inputs. For example, &amp;lt;code&amp;gt;MatMul&amp;lt;/code&amp;gt; is defined as the standard matrix product of two tensors, and &amp;lt;code&amp;gt;ReduceSum&amp;lt;/code&amp;gt; computes a summation along given axes. The ONNX spec, as of current versions, &#039;&#039;&#039;does not mandate the precision of intermediate calculations&#039;&#039;&#039; beyond the types of the inputs and outputs. If an operator’s inputs and outputs are float16, the spec implies the computation is done in float16 in a sense, but it does not forbid an implementation from using higher precision internally – it simply doesn&#039;t mention it. There is &#039;&#039;&#039;no explicit guarantee of identical bit-for-bit results&#039;&#039;&#039; across different ONNX backends for floating-point ops. Instead, results are expected to be numerically close, within tolerances one would normally expect from floating-point differences (due to different hardware or algorithms). The ONNX Backend Test suite typically uses a relative or absolute error tolerance when validating outputs for this reason.&lt;br /&gt;
&lt;br /&gt;
Because of this, important details like the use of extended precision or accumulation in higher precision are left ambiguous. This has led to discussions in the ONNX community about making such behavior more explicit. For example, a proposal was raised to allow specification of accumulation precision for certain ops like MatMul&amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;. The reasoning is that as lower-precision types (float16, bfloat16, float8) become more common, knowing or controlling whether an op internally accumulates in a higher precision is important for both accuracy and reproducibility&amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;. In June 2025, an issue on the ONNX GitHub suggested adding an attribute to MatMul (and similarly Gemm, Softmax, etc.) to specify the accumulator type&amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;. By default it would use input precision (status quo), but an attribute could request (for instance) 32-bit accumulation even if inputs are 16-bit&amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;. As of this writing, however, no such attribute exists in the official spec – it&#039;s an open issue. Thus, the numerical semantics are effectively: &#039;&#039;“compute the result as if in the same type as inputs, but actual implementations may use more precision internally as an optimization.”&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
From a guarantee standpoint, ONNX operators promise the mathematically correct result assuming real-number arithmetic, but they &#039;&#039;&#039;do not guarantee bit-exact results&#039;&#039;&#039; due to floating-point effects. There is an understanding that differences can arise from different summation orders, fused operations (e.g., an implementation might use an FMA (fused multiply-add) which has one rounding instead of two), or use of extended precision registers. ONNX does not formally define rounding modes either – it relies on IEEE 754 defaults, but if a backend used something like deterministic summation or alternative rounding, ONNX has no mechanism to express that.&lt;br /&gt;
&lt;br /&gt;
In summary, ONNX&#039;s current stance is largely &#039;&#039;&#039;implementation-dependent&#039;&#039;&#039; for numerical precision. It provides minimal explicit numerical guarantees beyond type constraints. This is why an ONNX model run on two different hardware or runtime libraries can have slightly different results, especially in reduced precision. The burden is on backends to produce results &amp;quot;close enough&amp;quot; to each other that they don&#039;t affect the model&#039;s decisions in any significant way.&lt;br /&gt;
&lt;br /&gt;
=== 5.2. MatMul, Conv, and Reduction Operators ===&lt;br /&gt;
&lt;br /&gt;
Consider ONNX &amp;lt;code&amp;gt;MatMul&amp;lt;/code&amp;gt;: it takes two tensors (often two matrices) and produces their matrix product. In pure math, $C_{ij} = \sum_k A_{ik} &#039;&#039; B_{kj}$. If A and B are float16, should that sum be computed in float16 or float32? As discussed, ONNX doesn’t specify – it simply says the output is a float16 matrix containing the product. In practice, many backends will &#039;&#039;promote&#039;&#039; the computation: for example, NVIDIA GPUs will do the multiplication and addition in a mixed FP16/FP32 mode by default (because of tensor cores), then convert the final result back to float16. ONNX Runtime on CPU might not support float16 multiplication natively, so it will insert cast ops to float32, perform the MatMul in float32, then cast back to float16&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;. From ONNX’s perspective, as long as the final result is within acceptable error of the “ideal” float16 computation, it’s fine. But note, an “ideal” float16 computation (where every multiply and add is rounded to FP16) might actually be &#039;&#039;less accurate* than what the backend did with FP32. This means that a backend using higher precision can produce a result that is not bit-equal to a pure FP16 implementation – it&#039;s actually closer to the true real number result. This is usually considered a good thing (improved accuracy), but it does highlight a lack of consistency: another backend might do everything in pure FP16 and get a slightly different answer.&lt;br /&gt;
&lt;br /&gt;
The same situation occurs for &#039;&#039;&#039;convolution&#039;&#039;&#039; (&amp;lt;code&amp;gt;Conv&amp;lt;/code&amp;gt; op) which is essentially a batched sum of products (like MatMul in sliding window), and for explicit reduction ops like &amp;lt;code&amp;gt;ReduceSum&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;ReduceMean&amp;lt;/code&amp;gt;, etc. If you have an ONNX &amp;lt;code&amp;gt;ReduceSum&amp;lt;/code&amp;gt; that sums 1000 float16 values, the spec doesn’t say how to sum them. A GPU kernel might sum in a tree using FP32 accumulators; a naive implementation might sum in order with FP16 and possibly overflow or underflow intermediate steps. Both are allowed as long as they output a float16 result that is “reasonable.” In practice, ONNX Runtime and others take care to avoid obviously bad behavior (like overflow) by using higher precision or by splitting the sum. But again, it’s not mandated by the ONNX standard explicitly.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Softmax&#039;&#039;&#039; is an interesting case: Softmax involves an internal sum of exponentials. ONNX defines the Softmax operator as $$\mathrm{Softmax}(x)_i = \frac{\exp(x_i)}{\sum_j \exp(x_j)},$$ typically applied along a specified axis. Directly computing this in float16 can be problematic for large vectors, as discussed in Section 6.3. Indeed, the issue of accumulation precision arises: should the sum $\sum_j \exp(x_j)$ be done in FP16 or can it be in FP32? Many implementations will do it in FP32 even if $x$ is FP16, because the dynamic range of $\exp(x)$ is large. The ONNX Softmax spec doesn’t detail this, but it expects the result to be a proper probability distribution (summing to 1.0 within normal floating error). A pure FP16 softmax on, say, 1000 elements could sum to something like 0.996 or 1.004 due to rounding, whereas FP32 accumulation would be much closer to 1.000. ONNX likely allows both; the small difference is usually inconsequential, but if it were large (say the FP16 sum overflowed to inf), that would be a correctness issue. A robust backend will prevent that (by using the common max-subtraction trick and higher precision accumulation). The ONNX issue 7072 indeed calls out Softmax as another op where explicit control of accumulation precision would be useful&amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
In summary, &#039;&#039;&#039;MatMul, Conv, and reduction ops&#039;&#039;&#039; in ONNX are defined mathematically but with &#039;&#039;implicit&#039;&#039; numerical behavior. Implementations commonly use higher precision for intermediate sums especially when input is low precision, but this is not standardized. Consequently, results can differ slightly between implementations. For critical applications, one must be aware of these differences. A model exported to ONNX with float16 weights might perform slightly differently on one runtime vs another if one does all float16 math and the other uses float32 accumulations. Both conform to ONNX. As a user, if you require a certain precision for accumulation, currently you have to ensure it by the way you export or run the model (for example, insert manual cast nodes in the ONNX graph to force certain ops to FP32). We may see future ONNX versions address this with attributes or metadata as mentioned, but for now these ops are effectively “compute with best effort accuracy”.&lt;br /&gt;
&lt;br /&gt;
=== 5.3. Backend and Hardware-Dependent Behavior ===&lt;br /&gt;
&lt;br /&gt;
Because ONNX is implemented by many backends (ONNX Runtime, TensorRT, CoreML, PyTorch JIT, etc.), and these in turn run on varied hardware (CPU, GPU, accelerators), the numeric behavior can be backend-dependent. Some examples:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;CPU backends:&#039;&#039;&#039; Many CPUs do not have native float16 vector arithmetic (with a few exceptions in recent CPUs). ONNX Runtime’s default CPU execution provider will often upcast float16 to float32, compute, then downcast&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;. This ensures accuracy but means that if you thought you were getting a full float16 pipeline, you’re not – the CPU is essentially ignoring the low precision in compute. On the other hand, x86 CPUs have extended 80-bit precision in x87 registers (though less used now) and 64-bit SIMD registers for doubles; some compiler optimizations might carry intermediate results in higher precision. This could make CPU results slightly different from a GPU’s if, say, a sum is done in 80-bit on CPU versus 32-bit on GPU. Generally, modern compilers avoid such extended precision unless explicitly asked, to improve reproducibility.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;GPU backends:&#039;&#039;&#039; GPUs often perform summations in parallel using tree reductions. The order of operations can change depending on how threads are scheduled. This means even on the same GPU, summing an array in one launch versus another might not give bit-identical results if the internal parallel reduction algorithm differs (though it should be deterministically the same given the same library version and input, but if you change GPU model or BLAS library, it might differ). Also, GPUs have fused multiply-add (FMA) instructions that do $(a*b + c)$ in one step with one rounding, which can produce slightly different results than doing the multiply and add separately (which would round twice). If an ONNX backend uses a fused kernel (like a GEMM library) it might yield different low-order bits than a simple loop.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Vendor libraries:&#039;&#039;&#039; ONNX Runtime can use vendor BLAS or DNN libraries (MKL, cuDNN, TensorRT, etc.). These libraries often implement their own strategies for numerical stability. For example, NVIDIA’s cuDNN might use FP32 accumulation for FP16 inputs by default. TensorRT (NVIDIA’s inference engine) will sometimes automatically promote precisions or insert scaling to maintain stability&amp;lt;ref name=&amp;quot;ref13&amp;quot;/&amp;gt;. On the other hand, if you explicitly request FP16 in TensorRT with no FP32 fallback, it might do the entire op in FP16 and you’d see more error. Some libraries offer knobs: TensorRT allows choosing FP32, FP16, or INT8 precision for each layer, and will use FP32 internally for safety in some cases unless forced otherwise.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Hardware-specific quirks:&#039;&#039;&#039; As another example, NVIDIA’s &#039;&#039;&#039;TensorFloat-32 (TF32)&#039;&#039;&#039; on Ampere GPUs is a precision mode where matrix multiplications use 10-bit mantissa (so about FP16 precision for the multiply) but keep 8-bit exponent (range like FP32). By default, CUDA libraries use TF32 for FP32 matrix multiplies on Ampere to speed them up, unless you disable it. This means if you run an ONNX model with FP32 on an A100 GPU, your matmuls might actually be lower precision than FP32 (though higher than FP16)&amp;lt;ref name=&amp;quot;ref6&amp;quot;/&amp;gt;. It’s an example of hardware choosing an intermediate precision for performance. From ONNX’s perspective, the output is still FP32 and (hopefully) within error tolerances, but a user might be surprised that it’s not a “true” FP32 computation internally. If exactness is needed, one has to turn off TF32.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Non-determinism:&#039;&#039;&#039; Some backends (especially on GPUs) allow non-deterministic execution for performance. For instance, atomic adds in parallel reductions can produce results that vary run-to-run because the order of accumulation is race-condition-dependent. ONNX’s spec doesn’t cover this explicitly, but it’s something to consider. In ONNX Runtime, there are settings for deterministic computing, but by default it may use all available performance optimizations, even if results vary at the 1e-7 level.&lt;br /&gt;
&lt;br /&gt;
In short, the numerical behavior under ONNX can depend on the combination of backend software and hardware capabilities. The lack of a strict spec for accumulation means implementations have freedom to use clever strategies (higher precision, reordering) to improve accuracy or speed. Usually this is positive (e.g. getting better accuracy than a naive approach). But it could also lead to slight inconsistencies. For example, you might find that exporting a PyTorch model to ONNX and running it with one engine yields outputs slightly different from PyTorch’s – not because ONNX is wrong, but because the summation or convolution was done differently (maybe PyTorch summed in FP32 as well, but maybe not in exactly the same way).&lt;br /&gt;
&lt;br /&gt;
The key takeaway is that ONNX &#039;&#039;by design&#039;&#039; abstracts away these details, which is good for portability but means you need to &#039;&#039;&#039;trust the backend&#039;&#039;&#039; or verify its precision behavior for sensitive models. If necessary, you can enforce certain behavior by inserting ops (for example, if you want to ensure FP32 accumulation, you could insert a cast to FP32 for inputs of a MatMul and cast the result back to FP16 – this way the ONNX graph itself mandates FP32 compute in between). This of course sacrifices some performance or device-specific magic.&lt;br /&gt;
&lt;br /&gt;
Looking forward, as mentioned, there are proposals to enrich ONNX’s specification regarding precision. This might include explicit flags for “exact summation” or “accumulate in int32” etc., which would make models more self-descriptive but also potentially less portable (if a backend doesn’t support that, what then?). For now, users should be aware of these backend differences. Most of the time, well-engineered ONNX runtimes yield results close enough that they don&#039;t affect the application (e.g., classification top-1 remains the same even if low-level sums differ by 1e-5). But for high-stakes numerical computations, testing and maybe adjusting the model or backend settings is prudent.&lt;br /&gt;
&lt;br /&gt;
== Numerical Stability in Deep Learning Models ==&lt;br /&gt;
&lt;br /&gt;
Deep learning computations involve many operations that, while theoretically well-defined, can be sensitive to numerical issues when implemented in finite precision. We&#039;ll discuss a few common scenarios in neural networks where numerical stability is a concern, and how precision and accumulation play a role.&lt;br /&gt;
&lt;br /&gt;
=== 6.1. Dot Products and Large-Scale Reductions ===&lt;br /&gt;
&lt;br /&gt;
Neural networks are full of dot products: every fully-connected layer computes $\mathbf{y} = W\mathbf{x}$, which is a series of inner products between weight vectors and the input. Convolutional layers compute weighted sums of local regions. Attention mechanisms compute large matrix products of query/key/value matrices. All these boil down to summing a lot of terms: $\sum_{k=1}^K a_k b_k$. When $K$ is large (it could be in the thousands or more for a single neuron in a fully connected layer of a large model), the potential for floating-point error in that sum increases.&lt;br /&gt;
&lt;br /&gt;
If using FP32, the error may still be negligible in many cases, but in lower precision like FP16, it can become significant. For example, consider a dot product of length 2048 in FP16. Each product is in FP16 (with ~3 decimal digits precision) and if accumulated in FP16, the worst-case error could be on the order of $2048 \times \varepsilon_{FP16} \approx 2048 \times 0.0009766 \approx 2$ (i.e., you could lose a couple units in the third decimal place relative accuracy in worst case). Even in random error accumulation, you might see error on the order of $\sqrt{2048} \times 0.0009766 \approx 0.044$ or 4.4% (though this is a rough estimate). That’s quite significant. Fortunately, as discussed, most hardware will accumulate such sums in a higher precision like FP32, dramatically reducing that error (~7 orders of magnitude smaller $\varepsilon$). But if it didn’t, the network’s outputs could get noisy. A fully-connected layer with many inputs might output slightly different results depending on summation order if done in low precision.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Large-scale reductions&#039;&#039;&#039; also occur in operations like global average pooling (summing all elements of a feature map) or computing the total loss by summing loss over batch samples. If one sums 100,000 values in FP32, the worst-case error could be on the order of 100,000 &#039;&#039; 1e-7 ≈ 1e-2 (1% uncertainty), though typical error would be much less. In FP16, summing 100k values is likely impossible without overflow: as noted, adding ~10^4 moderate numbers can overflow FP16&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. So large reductions &#039;&#039;must* be handled carefully (either in chunks or in higher precision). Frameworks generally do: for example, when performing a sum reduction on GPU, libraries break it into parallel blocks that accumulate in registers (usually 32-bit registers, even if inputs are 16-bit). &lt;br /&gt;
&lt;br /&gt;
Another aspect is that many dot products in neural nets involve values of varying magnitude. If the distribution of terms is such that some are much larger than others, the smaller terms might not contribute much if summed last. Ideally, one might sort by magnitude or use pairwise summation to improve that. Most BLAS libraries effectively do a pairwise strategy due to their parallel structure. So, while worst-case analyses are grim, practical implementations mitigate a lot of it.&lt;br /&gt;
&lt;br /&gt;
However, certain layers can suffer if not handled properly. One famous example is in RNNs or LSTMs: if one uses FP16 naive summation for the gating mechanism, errors could accumulate over many time steps and cause drift or instability. In modern practice, recurrent layers are often run in FP16 with FP32 accumulators to avoid this.&lt;br /&gt;
&lt;br /&gt;
In summary, &#039;&#039;&#039;dot products and large reductions need sufficient precision&#039;&#039;&#039; or robust algorithms. If you are implementing a custom kernel (say a custom ONNX op) and you have to sum a huge array, you should consider using a technique from Section 3 (pairwise summation, Kahan, or at least double accumulator if possible). Many deep learning computations, by virtue of hardware and library design, already do something like this under the hood. But it&#039;s always good to be aware, especially if pushing into lower precision territories.&lt;br /&gt;
&lt;br /&gt;
=== 6.2. Cancellation, Dynamic Range, and Conditioning ===&lt;br /&gt;
&lt;br /&gt;
We introduced &#039;&#039;cancellation&#039;&#039; earlier in the context of summation. In deep learning, cancellation can appear in a few places. One is in the computation of certain norms or variances. For instance, Batch Normalization computes mean and variance of activations. The variance formula $\mathrm{Var}(x) = E[x^2] - (E[x])^2$ is subject to catastrophic cancellation if $E[x]$ and $\sqrt{E[x^2]}$ are close in magnitude (which can happen if the variance is small compared to the mean). If implemented naively in single precision, you might get a negative variance due to numerical error (which would then be clamped to zero or sqrt would yield NaN). Robust implementations compute variance in a single pass or increase precision. Indeed, in many frameworks, batchnorm accumulation is done in FP32 even if the activations are FP16, precisely to avoid numerical issues in computing mean and variance. ONNX itself doesn’t specify this, but an ONNX backend is likely to do batchnorm stats in higher precision for safety. The &#039;&#039;conditioning&#039;&#039; of the variance calculation is poor when variance is tiny relative to mean, so it&#039;s a spot where higher precision is needed.&lt;br /&gt;
&lt;br /&gt;
Another example is in certain loss functions or regularizations. If you subtract two large numbers to get a small difference, you lose precision. Say you have two nearly identical predictions and you subtract them as part of some operation – if done in float16, you might get zero whereas float32 would have a slight difference. This could affect, say, a gradient if that difference was supposed to propagate information. Fortunately, neural nets often avoid directly subtracting large nearly equal numbers; but it can happen. In attention mechanisms, the &#039;&#039;score normalization&#039;&#039; (softmax) subtracts a max to avoid huge exponentials, which is good for range but if multiple scores are close to the max, you subtract numbers that are close. However, those subtractions are not catastrophic in the same way; they&#039;re intentional to reduce range and are typically fine in FP32 or even FP16.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Dynamic range&#039;&#039;&#039; issues are everywhere in deep learning. Activation functions like ReLU and exponential can produce very large or very small values. If subsequent computations don’t account for that, you get infinities or zeros. For example, consider a network output that produces a logit of 100 in FP16. $\exp(100)$ in FP16 overflows to infinity (since FP16 max ~ 6e4 and $e^{100} \sim 3.7e43$). If you then do softmax, that one infinity will dominate the sum and you might get a probability of 1 for that class (or NaNs if not handled). The standard softmax implementation subtracts the max logit, so in this case it would subtract 100 from all logits, making the largest exponent $\exp(0)=1$ and others tiny – this avoids overflow. But what about underflow? If you had a very negative logit, $\exp(-100)$ in FP16 would underflow to 0. In softmax, that just contributes ~0 probability, which is fine (it effectively is zero anyway). Underflow tends to be less dangerous than overflow in inference — underflow usually just means “this probability/neuron is effectively 0,” whereas overflow can poison the results with NaNs or Infs. &lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Conditioning&#039;&#039;&#039; refers to how sensitive a function is to input perturbations. Neural networks themselves as functions can be poorly conditioned (e.g., tiny changes in weights cause big changes in output for some architectures), but typically they are designed to be reasonably well-behaved. Certain internal computations like matrix inversion (if one does a pseudoinverse or something for a layer) can be extremely ill-conditioned if the matrix is near singular. That again would need double precision perhaps. In ML, one common ill-conditioned operation is the &#039;&#039;&#039;softmax denominator&#039;&#039;&#039;: $\sum_j e^{x_j}$ can be very large and also very sensitive to changes in the largest $x_j$. However, the softmax is more stable if computed carefully (max trick). Another is &#039;&#039;&#039;normalization&#039;&#039;&#039;: computing $\frac{x - \mu}{\sigma}$ for layer norm or batch norm – if $\sigma$ is very small, then the division blows up noise. In those cases, a small $\epsilon$ is usually added to $\sigma^2$ to prevent instability (this is a model-level fix, not a numeric one per se, but it acknowledges numeric limits).&lt;br /&gt;
&lt;br /&gt;
In training, &#039;&#039;&#039;optimizers&#039;&#039;&#039; can suffer from precision issues. For example, the Adam optimizer keeps a moving average of squared gradients; in half precision, these small updates might flush to zero or not change at some point. Some research found that certain adaptive optimizers break in FP16 because the small differences are lost (&amp;quot;Why FP16 training breaks NAdam&amp;quot; etc.). The solution is often to keep the optimizer states in FP32 while doing the bulk of compute in FP16. This again is a form of mixed precision.&lt;br /&gt;
&lt;br /&gt;
From an inference perspective, once the model is trained, the main concerns are avoiding overflow in activations and ensuring summations (like in softmax or reductions) are done with enough precision. The model’s weights are fixed, so we don&#039;t have to worry about cumulative error over iterations as in training, just the forward pass.&lt;br /&gt;
&lt;br /&gt;
=== 6.3. Normalization Layers and Softmax ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Normalization layers&#039;&#039;&#039; (BatchNorm, LayerNorm, etc.) and &#039;&#039;&#039;Softmax&#039;&#039;&#039; are specifically worth examining for numerical stability:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Batch Normalization:&#039;&#039;&#039; During inference, batchnorm applies an affine transform using precomputed mean and variance (from training). It’s basically: $y = \frac{x - \mu}{\sqrt{\sigma^2 + \epsilon}} \gamma + \beta$. This is a stable operation as long as $\epsilon$ is provided (to avoid division by zero). Since $\mu, \sigma^2$ are constants in inference, it reduces to linear ops which are fine. The main numerical work (computing mean/variance) happened during training or model calibration, and frameworks usually did that in FP32. So batchnorm at inference time is not a source of instability (it’s just a scale and shift of the data).&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Layer Normalization / Group Normalization:&#039;&#039;&#039; These compute mean/var on the fly for each sample (and possibly channel group). In inference, they still need to compute these statistics for each forward pass. If implemented in low precision, the variance calculation can suffer cancellation as mentioned. However, it&#039;s likely that inference engines compute the mean and variance in FP32 even if inputs are FP16 (especially since these are per-sample operations, not huge batch reductions – it&#039;s feasible to do in higher precision). If someone implemented layer norm purely in FP16, they might see issues when the variance is very small. Usually there’s an $\epsilon$ added which might dominate a tiny variance and thus mitigate catastrophic cancellation (the result might just output something mostly from $\epsilon$). In summary, normalization layers are usually fine as long as the implementation uses enough precision for the summation of squares.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Softmax:&#039;&#039;&#039; Softmax is notorious if done improperly. The safe algorithm: subtract the maximum input from all inputs (to bring the largest exponent to 1.0), exponentiate, sum them up, divide each exponent by the sum. The max subtraction handles overflow in $\exp()$. But what about the sum? As discussed, if there are $N$ elements in the softmax, the sum is at most $N$ (when all inputs equal the max). In classification, $N$ might be the number of classes. For ImageNet, $N=1000$, so worst sum ~1000 (which is fine in FP16, as 1000 &amp;lt;&amp;lt; 65504). For an extreme case like a language model, $N$ could be 50,000 (vocab size). Sum could be 50,000 in worst case (everyone equal probability). 50,000 is still below 65504, so actually an FP16 sum would &#039;&#039;not&#039;&#039; overflow in that scenario, and the rounding error on a sum of 50k identical terms would be maybe a few ULPs (which at 50k magnitude might be ~0.5 difference worst-case). So maybe softmax summation in FP16 is not catastrophic for output probabilities – you’d get essentially 1.0 after normalization instead of 0.99999 in some case. Usually fine.&lt;br /&gt;
&lt;br /&gt;
:The bigger issue is if one or a few terms dominate the softmax. If one term is much larger, the others underflow to 0 after the max subtraction. That’s fine: you get a one-hot distribution essentially. If the model expected a softer distribution, that could be a problem. But if the logits were really that far apart, even FP32 would give nearly one-hot. So not really a precision issue, more of a model property.&lt;br /&gt;
&lt;br /&gt;
:However, the &#039;&#039;&#039;stability during training&#039;&#039;&#039; is a big issue. The GitHub thread we saw (ColossalAI) mentioned training a 130B model where FP16 softmax in attention caused training to occasionally blow up&amp;lt;ref name=&amp;quot;ref14&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref14&amp;quot;/&amp;gt;. They observed attention scores in some heads ranging up to 1e4 or as low as 1e-3&amp;lt;ref name=&amp;quot;ref14&amp;quot;/&amp;gt;. 1e4 exponent in FP16 is definitely Inf (because $\exp(10^4)$ is astronomically huge, but even exp(100) is inf in half). The method CogView introduced (Precision Bottleneck Relaxation) involved subtracting the max &#039;&#039;per head&#039;&#039; to reduce range&amp;lt;ref name=&amp;quot;ref14&amp;quot;/&amp;gt;—which is basically the softmax max trick at a finer granularity or something similar. In the end, they found &#039;&#039;&#039;the simplest fix was to compute softmax in FP32&#039;&#039;&#039; for those layers&amp;lt;ref name=&amp;quot;ref14&amp;quot;/&amp;gt;. They reported that it had negligible speed impact but significantly improved stability&amp;lt;ref name=&amp;quot;ref14&amp;quot;/&amp;gt;. This highlights a general principle: certain operations like softmax (and sometimes matrix multiplications in attention) benefit from higher precision, and using FP32 in a few critical spots can save a mixed-precision training from crashing. In inference, using FP32 for softmax is also a safeguard if one is uncertain. It costs little because softmax is not a heavy compute compared to matrix multiplies.&lt;br /&gt;
&lt;br /&gt;
:ONNX does have a Softmax operator; if the input is float16, a backend might do exactly what they described: internally cast to float32 for the softmax computation, then cast output back to float16. Some inference engines (like DeepSpeed’s inference or others) keep attention softmax in FP32 even if rest is FP16, for robustness&amp;lt;ref name=&amp;quot;ref14&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Apart from softmax, other places where exponentials occur are in loss functions like cross-entropy (which uses log-softmax internally), or in activation functions like Sigmoid and Tanh (which are basically scaled logistic functions). Sigmoid($x)=1/(1+e^{-x})$. If $x$ is moderately large positive, $e^{-x}$ underflows to 0 in FP16 if $x&amp;gt;~11$. That gives a sigmoid output of 1.0 exactly. In FP32, $e^{-x}$ for $x=11$ is about $5e-5$, not underflow, so sigmoid ~0.99995. So FP16 will saturate some outputs to exactly 0 or 1 sooner than FP32 would. This can slightly change behaviors (e.g., a neuron might go to full saturation a bit early). Usually, this isn’t a huge deal, but it could make gradients zero earlier. In inference, we don’t care about gradients, just the output value. Getting exactly 1.0 vs 0.99995 probably has no practical difference for a classification output. But one should be aware that &#039;&#039;&#039;activation functions can saturate faster&#039;&#039;&#039; in lower precision due to limited range or precision. ReLU has no issue (it’s linear or 0), but exponential-based activations do. In practice, again, frameworks often simply compute those in the given precision and accept the slight difference. If it were a problem, one could do those in FP32 as well.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Summary:&#039;&#039;&#039; Numerical stability in inference is maintained by using robust computation methods: subtracting maxima in softmax/LogSumExp, adding epsilon in denominators for normalization, and using higher precision for summations or critical steps. Most of these are built into the algorithms. ONNX models assume these standard practices (they don&#039;t need to explicitly do them in the graph because the backend inherently will for ops like Softmax). It&#039;s wise when building or exporting models to ONNX to test them in the target precision and ensure there are no surprises (like an output becomes NaN or Inf due to precision). Usually if training was done in mixed precision, the model will have been vetted in that environment already.&lt;br /&gt;
&lt;br /&gt;
== Performance vs. Accuracy Trade-offs ==&lt;br /&gt;
&lt;br /&gt;
There is an inherent trade-off between numerical precision and computational performance (speed, memory usage) in AI systems. Choosing lower precision can dramatically increase throughput and reduce memory, but it may introduce arithmetic errors. Engineers must balance these considerations:&lt;br /&gt;
&lt;br /&gt;
=== 7.1. Throughput, Memory Bandwidth, and Precision ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Throughput (speed)&#039;&#039;&#039; is often higher for lower-precision operations. Modern hardware can execute more low-precision ops in parallel than high-precision ops. For example, an NVIDIA V100 GPU can perform 2× more half-precision FLOPs than single-precision per clock, and newer architectures with Tensor Cores can do 8× more (since a Tensor Core warp operation might perform 64 FP16 FMAs per cycle vs 16 FP32). Similarly, int8 operations can be even faster and more energy efficient. Lower precision numbers also use less memory bandwidth – fetching 16-bit data is twice as fast as 32-bit data from memory (assuming the same bus width), which can relieve memory bottlenecks. Many deep learning workloads are memory-bound, so halving the data size can nearly double throughput if compute is not the limiting factor. This is a huge incentive to use lower precision.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Memory and storage&#039;&#039;&#039;: Using FP16 or int8 weights reduces model size, which is important for deploying large models to memory-limited environments (like edge devices or GPUs with limited VRAM). It also means you can fit larger batch sizes in memory, or more models on a chip, etc. There is also a cascade effect: smaller data means better cache utilization, which further improves performance.&lt;br /&gt;
&lt;br /&gt;
However, &#039;&#039;&#039;accuracy&#039;&#039;&#039; suffers if precision is too low. We have discussed how rounding error and limited range can cause deviations in outputs. The question is: do those deviations matter for the application? Often for neural networks, a small amount of noise or error in intermediate calculations does not change the final prediction. Neural nets are somewhat error-tolerant due to their redundant distributed representations. This is why quantization and FP16 techniques can work at all. The final model accuracy (e.g. top-1 accuracy on ImageNet) might drop only 0.5% or less when using FP16 instead of FP32, or int8 instead of FP16, if done properly. For many use-cases, this loss is acceptable given the speed gains.&lt;br /&gt;
&lt;br /&gt;
If one goes too low (like naive FP8 or int4 without special care), accuracy might drop severely – perhaps making the model unusable. Thus the trade-off: as precision decreases, performance increases but at some point accuracy degrades beyond an acceptable threshold. Engineers aim to find the lowest precision that still preserves accuracy.&lt;br /&gt;
&lt;br /&gt;
There is also the consideration of &#039;&#039;&#039;accumulation strategy&#039;&#039;&#039; affecting performance: for instance, Kahan summation (compensated) is more accurate but ~4× slower than naive summation. If you need that accuracy, you pay a performance cost. In many AI inference scenarios, the slight extra accuracy from something like Kahan isn&#039;t needed, so it’s not used. But in a scenario where you sum millions of elements for a final result that is thresholded, maybe you do need it.&lt;br /&gt;
&lt;br /&gt;
Another dimension is &#039;&#039;&#039;determinism vs speed&#039;&#039;&#039;. Enforcing a deterministic reduction order (for reproducibility) might require disabling some parallelism, which could slow things down. If exact repeatability is critical (like certain scientific or financial applications of neural nets), one might sacrifice some performance to ensure the summation happens the same way every time. ONNX allows both modes depending on the runtime.&lt;br /&gt;
&lt;br /&gt;
From a memory perspective, if precision is higher than needed, you waste memory. For example, storing intermediate activations in FP32 when they could be FP16 uses 2× memory, which might mean lower batch size or more memory traffic. So there&#039;s strong motivation to keep things as low-precision as possible &#039;&#039;except&#039;&#039; where accuracy truly demands.&lt;br /&gt;
&lt;br /&gt;
In summary, the trade-off often manifests as &#039;&#039;diminishing returns&#039;&#039;: FP64 vs FP32 – huge slowdown for usually negligible accuracy gain in ML; FP32 vs FP16 – moderate speedup for usually minor accuracy hit; FP16 vs int8 – another big speedup but maybe more accuracy loss unless calibration is done; int8 vs int4 – speed gain but often big accuracy drop that might not be worth it for current models.&lt;br /&gt;
&lt;br /&gt;
Engineers use techniques like &#039;&#039;&#039;mixed precision&#039;&#039;&#039; to target high precision only where needed. For example, run most of the model in FP16, but keep a few sensitive layers in FP32. This yields most of the performance benefits while ensuring critical parts are accurate. We already saw softmax as one example. Another might be final output layer: sometimes using FP32 for the logits and softmax can be safer.&lt;br /&gt;
&lt;br /&gt;
Hardware is also trending to support mixed modes – like FP8 Tensor Cores accumulate in FP32, etc. So hardware tries to give accuracy cheaply.&lt;br /&gt;
&lt;br /&gt;
=== 7.2. Hardware Accelerators and Tensor Cores ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Hardware accelerators&#039;&#039;&#039; (GPUs, TPUs, FPGAs, ASICs like GraphCore IPU, etc.) have specific support for certain precisions. Leveraging these can provide massive speedups:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;NVIDIA Tensor Cores:&#039;&#039;&#039; These started in Volta (V100) with support for 4x4 matrix multiply-adds on FP16 inputs with FP32 accumulation&amp;lt;ref name=&amp;quot;ref6&amp;quot;/&amp;gt;. Using Tensor Cores can give up to 8x throughput compared to using regular FP32 cores. But to use them, the data needs to be FP16 (or now BF16 or FP8 on newer GPUs) and aligned to certain dimensions (multiples of 8 etc. in matrix sizes, which frameworks handle by padding if needed). If an ONNX model is all FP32, by default it would not use Tensor Cores unless the runtime automatically converts some ops to mixed precision (which some do via a flag or environment variable enabling TF32 or FP16). The introduction of &#039;&#039;&#039;TensorFloat-32 (TF32)&#039;&#039;&#039; in Ampere GPUs is essentially a compromise: treat FP32 inputs as two 16-bit significand halves (10-bit mantissa actually) so that it can run on Tensor Cores, giving ~8x speed, while still outputting FP32. TF32 has about the precision of FP16 but range of FP32, and it was chosen because it typically doesn’t hurt convnet accuracy. ONNX Runtime or others on Ampere will use TF32 by default for conv and matmul unless disabled, giving a free speed boost at minimal accuracy cost. If exact FP32 is needed, users have to turn it off (sacrificing speed).&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;TPU (Google):&#039;&#039;&#039; TPUs have native support for BF16 matrix ops with FP32 accumulators. They were designed specifically for the mixed precision approach in training. For inference, TPUs also support int8 and other quantized ops (e.g., via Edge TPU for lower-power). If exporting ONNX to run on a TPU (via some bridge), one might get automatic BF16 usage. BF16 has the advantage of no loss in range, so TPUs can more straightforwardly run models in BF16 without modifications.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;FPGAs and ASICs:&#039;&#039;&#039; Some custom accelerators might use fixed-point or block floating-point arithmetic for efficiency. They often treat the network in quantized terms. The ONNX quantization specification (QuantizeLinear/DequantizeLinear ops) exists for representing quantized models (INT8 etc.), which is another domain beyond floating-point, but it overlaps as a precision issue. In those systems, accumulators for INT8 × INT8 multiplies are usually 32-bit (to avoid overflow for dot products up to certain lengths). The trade-off there is between integer vs float representation, which can give big speedups on CPUs/DSAs via SIMD instructions (many CPUs have vector int8 multiply-add instructions now for inference).&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Memory bandwidth on hardware:&#039;&#039;&#039; Some accelerators include special high-bandwidth memory or cache specifically for lower precision. For example, an accelerator might fetch two FP16 values in one 32-bit memory transaction – effectively doubling memory bandwidth for half precision. On some hardware, using FP16 can even reduce memory &#039;&#039;latency&#039;&#039; if multiple values can be packed.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Parallelism and Vectorization:&#039;&#039;&#039; Many hardware units process data in fixed vector widths (e.g., 256-bit registers). That could be 8 floats at once or 16 half-floats at once in the same 256 bits. If your code is in FP16, you might use the full vector width (16 values) whereas FP32 might only use half (8 values). This means FP16 can make better use of vector units. Similarly, matrix units often have fixed tile sizes, and smaller data types allow larger tiles or more tiles concurrently.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Energy efficiency:&#039;&#039;&#039; Lower precision often means less energy per operation, which on large deployments (data centers or battery-powered devices) is a significant advantage. This isn&#039;t directly performance, but it&#039;s an important trade: you might choose a slightly lower precision to save power even if accuracy drops a tiny bit.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Software overhead:&#039;&#039;&#039; Using mixed precision on hardware involves some overhead of casting and moving data between precisions. For instance, if you have to frequently cast FP16 to FP32 and back, that adds instructions and memory copies. A well-designed accelerator (like GPUs with unified memory for FP16/32 or special hardware casting) minimize this. Still, the runtime might insert extra operations (as we saw, ONNX Runtime CPU inserted many Cast nodes converting FP16 to FP32 and back&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;, which actually &#039;&#039;slowed down&#039;&#039; that FP16 model by 4x vs FP32 on a CPU, because the CPU had no native FP16). So ironically, on some hardware, using lower precision in ONNX might degrade performance if not supported (the CPU case). Always ensure the target hardware actually supports the low precision you plan to use; otherwise, you&#039;ll pay overhead for emulation.&lt;br /&gt;
&lt;br /&gt;
To illustrate trade-off: Suppose you have a model that achieves 99% accuracy in FP32 and drops to 98.5% in FP16. If FP16 runs 2x faster, many would accept the 0.5% accuracy drop for double throughput. If int8 runs 4x faster but accuracy drops to 95%, that might be too much drop for some applications, but acceptable for others that value speed. It&#039;s application-dependent.&lt;br /&gt;
&lt;br /&gt;
In critical applications, one might maintain higher precision accumulators or run certain validation steps in high precision to ensure nothing drifted. In research, sometimes a model is trained in mixed precision but validated in double to ensure any small numeric differences don’t influence the reported result (rare, but for scientific computing integration maybe).&lt;br /&gt;
&lt;br /&gt;
In the ONNX world, performance-vs-accuracy is often managed via &#039;&#039;&#039;Execution Providers&#039;&#039;&#039; (EPs) that target specific hardware. For example, the TensorRT EP in ONNX Runtime can take an ONNX model and run it using NVIDIA&#039;s high-performance inference library, which will use FP16 or INT8 if allowed, to maximize speed. The user can configure the EP with precision flags. If the accuracy drop is too high, one might restrict it to FP32. If it&#039;s fine, allow FP16. Tools exist to measure the accuracy difference (like calibrating int8 quantization).&lt;br /&gt;
&lt;br /&gt;
To conclude, the trade-offs revolve around picking the lowest precision that yields acceptable accuracy, and utilizing hardware features fully. ONNX being a high-level spec abstracts this, but it&#039;s ultimately up to the deployment to decide and sometimes up to the exporter to incorporate quantization. ONNX’s role is to be able to &#039;&#039;represent&#039;&#039; these choices (they have added quantized types, FP8 types, etc., to allow the graph to include those when needed).&lt;br /&gt;
&lt;br /&gt;
== Best Practices and Design Guidelines ==&lt;br /&gt;
&lt;br /&gt;
Given the above insights, here are some best practices and guidelines for dealing with numerical precision and accumulators in AI inference (and training) design:&lt;br /&gt;
&lt;br /&gt;
=== 8.1. Choosing an Accumulator Precision ===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Match precision to data length and range:&#039;&#039;&#039; If an operation accumulates a large number of terms, consider using a higher precision accumulator than the input data. For example, for summing &amp;gt;1000 values of type FP16, an FP32 accumulator is recommended to avoid large rounding error or overflow. If summing &amp;gt; million values of FP32, consider double (FP64) accumulation if high accuracy is needed (though this is rare in DL). &#039;&#039;Rule of thumb:&#039;&#039; if $N \cdot \varepsilon$ is not tiny (say &amp;gt;1e-3), you may benefit from higher precision. For FP16 ($\varepsilon \approx 1e-3$), that threshold N is a few hundred. For FP32 ($\varepsilon \approx 1e-7$), N would be in the millions before 1e-3 error.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Use higher precision in critical layers:&#039;&#039;&#039; Identify if certain layers of the model are more sensitive to precision. For instance, the output layer or a layer that feeds into a very sharp decision boundary might require more precision. If a small change there could flip the prediction, you don&#039;t want excessive numerical error. Using FP32 for logits and softmax is a common practice to maintain classification confidence stability.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Follow hardware defaults, but override if needed:&#039;&#039;&#039; Many hardware accelerators have sensible defaults (like accumulate in FP32 for tensor ops). It&#039;s usually best to use those defaults as they were chosen to balance accuracy and speed. Only override if you have evidence of a problem. For example, if using ONNX Runtime on CPU, you discovered it upcasts to float for you – great. But if using some custom accelerator that does pure FP16, you might manually insert casts in the ONNX graph around certain ops to force higher precision on those ops.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Maintain precision for gradient accumulation (training):&#039;&#039;&#039; This is more training-oriented, but worth noting: when summing gradients over batches or updating weights, use higher precision (FP32 or even 64) for those accumulations. This ensures training doesn&#039;t diverge due to numeric error. Many frameworks keep a &amp;quot;master copy&amp;quot; of weights in FP32 when training in FP16&amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;. For inference, this translates to: any running statistics or counters in the model (like if you had a running average inside the model) should probably be in higher precision to avoid drift.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Be mindful of intermediate cast costs:&#039;&#039;&#039; If you do decide to cast to higher precision for an operation in ONNX, remember it incurs memory copies and compute overhead. Make sure it&#039;s worth it. For example, if you cast a huge tensor to double, that could slow things drastically. Usually FP16-&amp;gt;FP32 is fine (hardware handles it well), but FP32-&amp;gt;FP64 might be expensive on GPUs.&lt;br /&gt;
&lt;br /&gt;
=== 8.2. Safe Reduction Lengths and Heuristics ===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Chunk long reductions:&#039;&#039;&#039; If you must do a very long sum in low precision (due to memory constraints or hardware), chunk the sum into parts, sum each to a higher precision local accumulator, then sum the partial sums. This is essentially pairwise summation. Many libraries use a fixed chunk size heuristic (e.g., sum in blocks of 256 in registers, then add those up). This bounds the error growth. If implementing manually, you can choose a chunk size such that chunk_length &#039;&#039; ε is small (like 256 &#039;&#039; 1e-3 = 0.256 for FP16, hmm maybe choose 100).&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Sort by magnitude if possible:&#039;&#039;&#039; In some cases (not typical in DL, but possible in post-processing), if you have numbers varying by orders of magnitude, summing from smallest to largest improves accuracy. It prevents small ones from being swallowed by big ones. This is more relevant if you output a sum of mixed-scale values (like some algorithm mixing big and small contributions). It’s not always feasible in real-time inference to sort, but keep in mind.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Use compensation for critical sums:&#039;&#039;&#039; If you find a particular sum or difference in your inference computation is crucial (like computing a small residual from two close numbers), consider using Kahan summation or another compensated scheme just for that part. For example, if you had an ONNX custom function that needs to sum a lot of almost cancelling terms, you might implement it with compensation internally.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Heuristics for FP8/low-bit:&#039;&#039;&#039; When using extremely low precision like FP8, adopt the recommended practices: use scaling (don&#039;t feed raw data into FP8, normalize it first), and perhaps only use FP8 on portions of the network that are empirically robust. The industry is still establishing heuristics, but one might be: use FP8 for matrix multiplications, but accumulate in FP16 or FP32; keep normalization in FP16; use FP16 for first and last layer maybe. This is evolving, so keep an eye on literature.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Keep an eye on sum length in softmax and normalization:&#039;&#039;&#039; If you have extremely large class counts or group sizes (e.g., a softmax over 1e6 items, or layer norm over tens of thousands of features), realize this is a giant reduction. You may want to ensure that reduction is done in higher precision. ONNX Softmax by default will likely not handle a million-class case in FP16 well (the sum would overflow since 1e6 &amp;gt; 65504). So either do that on CPU in FP32 or break it down (could do hierarchical softmax, etc.). This is a corner case, but relevant for huge vocab language models. In practice, vocab ~50k is fine as we said.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Test numeric differences:&#039;&#039;&#039; A practical tip: after quantizing or reducing precision, run some test inputs through both the high-precision model and low-precision model. Compare outputs (and maybe some intermediate signals if possible). If differences are consistently small relative to the application tolerance, you&#039;re good. If you see occasional large differences or instabilities (like outputs become NaN or Inf in low precision), that&#039;s a red flag. Use that testing to guide where you might need to increase precision or change algorithms.&lt;br /&gt;
&lt;br /&gt;
=== 8.3. When Higher Precision Is Required ===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Ill-conditioned calculations:&#039;&#039;&#039; If your model or pipeline performs any operation known to be ill-conditioned (matrix inversion, subtraction of nearly equal quantities, computing a tiny difference of large numbers), you likely need higher precision for that part. For example, if your ONNX model includes a custom op to solve a linear system, use double precision in that op if possible (or at least float with iterative refinement). In ML, this might occur in something like a physics-informed neural network or a differentiable simulation within the model.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Strict accuracy requirements:&#039;&#039;&#039; Some applications (medical, aerospace, finance) might have strict tolerances. If an inference model’s output needs to be correct to 5 decimal places, you probably cannot use FP16, which has only 3-4 decimal digits precision. FP32 might be barely enough (~7 digits). FP64 might be warranted to guarantee that level of accuracy in worst case. Most deep learning applications (like image classification) don&#039;t demand that level of numeric precision in outputs – as long as the top class is correct, it doesn&#039;t matter if its probability was 0.901 vs 0.899. But if you&#039;re doing something like summing probabilities to make a risk estimate or doing a cumulative sum over many predictions, be careful to accumulate precisely.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Large sums or products in post-processing:&#039;&#039;&#039; Sometimes the neural net is fine, but the &#039;&#039;post-processing&#039;&#039; of results might need precision. E.g., summing up probabilities of many events, or computing confidence intervals, etc. Do those calculations in double if needed outside the model.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Preventing drift in long sequences:&#039;&#039;&#039; For sequential models (RNNs processing thousands of steps), rounding error can accumulate over steps. If each step has a small error, after 10000 steps it could add up. In such cases, using a higher precision for the state might reduce drift. Some practitioners use FP64 for the hidden state accumulation in very long sequence processing to maintain numerical stability (though this is not common due to speed loss). Alternatively, periodically renormalize or reset small errors.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Simulations or differential equations:&#039;&#039;&#039; If your ONNX model involves simulating something (like a differential equation solver inside a model, or an ODE/RNN hybrid), those often need double precision to be stable for long durations. Recognize those situations and allocate precision accordingly.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Verification and debugging:&#039;&#039;&#039; Use higher precision as a tool to verify correctness. For example, run your model in FP64 (if possible) and compare to FP32 to see if there are differences. If not, FP32 is fine. If yes, you might find a particular calculation losing too much info in FP32, implying maybe you should keep it in FP64 in production as well. This is rare in inference but can be part of rigorous validation.&lt;br /&gt;
&lt;br /&gt;
In ONNX, using higher precision may involve casting to &amp;lt;code&amp;gt;tensor(double)&amp;lt;/code&amp;gt; for certain ops. Not all runtimes support double on GPU though (some do via emulation or just slower path). ONNX CPU does support double for general ops. So be mindful of the target.&lt;br /&gt;
&lt;br /&gt;
Finally, &#039;&#039;&#039;document your precision choices&#039;&#039;&#039;. If you deliver an ONNX model that uses mixed types (float16 inputs, float32 accumulations, int8 weights, etc.), ensure that is clear to whoever maintains or uses it. Precision issues can be sneaky, so clarity helps.&lt;br /&gt;
&lt;br /&gt;
== Limitations and Open Problems ==&lt;br /&gt;
&lt;br /&gt;
Despite advances in handling numerical precision, there remain limitations and open challenges in this domain:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Lack of Precision Specification in ONNX:&#039;&#039;&#039; As noted, ONNX currently has no way to formally specify that an op &#039;&#039;should&#039;&#039; use a higher precision internally. This can lead to nondeterministic differences across platforms&amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;. While proposals exist to add attributes for this&amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;, it is not yet standard. Thus an ONNX model might behave slightly differently on two compliant backends, and there&#039;s no built-in mechanism to ensure identical results. This is a limitation for applications requiring cross-platform consistency. Solving it may involve extending ONNX (e.g., an attribute like &amp;lt;code&amp;gt;accumulatePrecision=&amp;quot;FLOAT32&amp;quot;&amp;lt;/code&amp;gt; for MatMul) or tightening the spec, but those come with backward compatibility and implementation burden concerns.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Reproducibility and Determinism:&#039;&#039;&#039; Even with the same precision, parallel computation can introduce nondeterminism (due to different summation orders). Ensuring bit-identical results across runs (important for some sensitive domains) is non-trivial. Frameworks often provide &amp;quot;deterministic mode&amp;quot; which might use fixed ordering at cost of speed. In ONNX, there&#039;s no global flag for this; it&#039;s up to the runtime. More research could be done on algorithms that produce bit-wise identical results independent of parallel execution order (perhaps using techniques like pairwise summation or consistent partitioning). Stochastic rounding is another area: sometimes using probabilistic rounding can reduce bias in accumulated error, but it introduces randomness – reconciling that with reproducibility is tricky.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Ultra-low Precision (FP8 and below):&#039;&#039;&#039; While FP8 is being explored, going to 4-bit floats or mixed schemes is an open challenge. The OpenAI community is investigating 4-bit (quantization-wise) for LLMs with some success, but a true 4-bit float (like E3M1) would be extremely limited. Perhaps hybrid schemes (like block floating point, log scales, etc.) will come. ONNX already has some support (they mention 4-bit in documentation placeholders&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;), but it&#039;s nascent. Research into training techniques, calibration, and new number formats (like &#039;&#039;&#039;posit numbers&#039;&#039;&#039; or tapering precision floats) is ongoing. If a new format proves useful, ONNX will need to adapt to represent it.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Quantization Noise and Model Robustness:&#039;&#039;&#039; Reducing precision can sometimes expose models to issues like adversarial vulnerability or fragility – e.g., a slight perturbation might flip a decision if the margin was small and precision loss nudged it. Ensuring model robustness under quantization is an open problem. It might require re-training the model with quantization in the loop (quantization-aware training) or designing models that have large &amp;quot;margin&amp;quot; between classes to tolerate quantization noise.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Dynamic range during training vs inference:&#039;&#039;&#039; In training, activations can have wider distributions (especially early in training). Models often learn to constrain their ranges (via activation functions saturating or normalization), which makes inference more stable in lower precision. But if one tries to train in FP8 from scratch, it&#039;s very hard. This gap between what&#039;s possible in a post-trained inference model vs training is an open area – how to train directly in low precision without significant accuracy loss. Solving that could simplify workflows (no need for high-precision training then quantize; just train low-prec end to end). Techniques like loss scaling, gradient clipping, etc., are partial solutions. The development of optimizers that are resilient to low precision is ongoing.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Hardware variability:&#039;&#039;&#039; As new hardware emerges (with new numeric capabilities or quirks), ensuring that ONNX models run consistently is an ongoing task. For instance, one device might implement a fused op that reduces rounding error, another does it separately. Even something like convolution might use a different algorithm (FFT-based vs direct) which has different numeric properties. Open problem: could ONNX allow specifying numeric tolerances per op or preferred algorithms? That may be overkill for most, but for some HPC ML use cases, maybe desired.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Testing and validation of numerics:&#039;&#039;&#039; There isn&#039;t a standardized set of &amp;quot;numeric compliance&amp;quot; tests for ONNX runtimes beyond simple correctness. Perhaps in the future, test suites might include challenging numeric scenarios (large sums, nearly singular matrices) to ensure a runtime handles them gracefully (maybe by switching precision). Right now, it&#039;s mostly on the user to test their model under expected conditions.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Combining quantization with floating point:&#039;&#039;&#039; Many production models use a mix (e.g., int8 for some layers, FP16 for others). ONNX can represent this (with QuantizeLinear ops or MixedPrecision through casting), but tooling to automatically find the optimal mix is still developing. It&#039;s somewhat trial-and-error or using heuristics. An open question is how to automate precision assignment: given a model and a target hardware, how to automatically choose which layers can be int8, which should be FP16, etc., to meet an accuracy target. This crosses into NAS (neural architecture search) territory or automated quantization. &lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Numerical debugging tools:&#039;&#039;&#039; When a model goes wrong due to precision, it&#039;s often hard to pinpoint where. Better tooling to analyze numerical error layer by layer could be useful. For instance, one could run a model in double and float and diff the intermediate results to see where the largest divergence happens. Doing this systematically for big nets is an open tooling challenge. Some frameworks have hooks for this, but not standardized.&lt;br /&gt;
&lt;br /&gt;
In conclusion, while we&#039;ve come far with using lower precision in AI without sacrificing much accuracy, the landscape is always evolving. The key open problem is how to push precision lower (for efficiency gains) without crossing the threshold where accuracy falls off a cliff. Each new generation of hardware and techniques (like FP8, novel quantization schemes) extends that boundary, but also brings new questions on how to manage the numerical errors. ONNX as an interchange will need to keep pace, possibly by adding more expressive power for these numeric considerations.&lt;br /&gt;
&lt;br /&gt;
== See Also ==&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;IEEE 754 Floating-Point&#039;&#039;&#039; – The standard defining binary16, binary32, binary64 formats and rounding behavior.&lt;br /&gt;
* &#039;&#039;&#039;Floating-Point Arithmetic&#039;&#039;&#039; – General topic on how floating-point math works, common issues (overflow, underflow, cancellation).&lt;br /&gt;
* &#039;&#039;&#039;Kahan Summation Algorithm&#039;&#039;&#039; – Algorithm for compensated summation to reduce numerical error&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;.&lt;br /&gt;
* &#039;&#039;&#039;BFloat16&#039;&#039;&#039; – A short-format float with 8-bit exponent, used in TPUs and some GPUs for training networks.&lt;br /&gt;
* &#039;&#039;&#039;Mixed Precision Training&#039;&#039;&#039; – Technique of using FP16 (or BF16) along with FP32 accumulators to accelerate training&amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;.&lt;br /&gt;
* &#039;&#039;&#039;Quantization in Deep Learning&#039;&#039;&#039; – Converting networks to lower bit integer arithmetic for inference (INT8 quantization and calibration techniques).&lt;br /&gt;
* &#039;&#039;&#039;ONNX Operator Specification&#039;&#039;&#039; – Official docs for ONNX ops, which describe their mathematical function but not the required numeric precision (a point of potential extension in future).&lt;br /&gt;
* &#039;&#039;&#039;CUDA Tensor Cores &amp;amp; TF32&#039;&#039;&#039; – NVIDIA documentation on how tensor cores perform mixed precision and the TF32 format for accelerating FP32.&lt;br /&gt;
* &#039;&#039;&#039;Pairwise Summation&#039;&#039;&#039; – Summation algorithm with logN error growth&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;, used in many numerical libraries to improve accuracy of reductions.&lt;br /&gt;
* &#039;&#039;&#039;Softmax Stability&#039;&#039;&#039; – Articles on numerically stable softmax and log-sum-exp implementations to avoid overflow.&lt;br /&gt;
* &#039;&#039;&#039;Performance Tuning for Low Precision&#039;&#039;&#039; – Guides (e.g., NVIDIA or Intel whitepapers) on using low precision and the trade-offs involved in various hardware.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;references&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref1&amp;quot;&amp;gt;Accelerate and simplify Scikit-learn model inference with ONNX Runtime - Microsoft Open Source Blog — opensource.microsoft.com — https://opensource.microsoft.com/blog/2020/12/17/accelerate-simplify-scikit-learn-model-inference-onnx-runtime&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref2&amp;quot;&amp;gt;Floating Point Precision: Understanding FP64, FP32, and FP16 in Large Language Models - DEV Community — dev.to — https://dev.to/lukehinds/floating-point-precision-understanding-fp64-fp32-and-fp16-in-large-language-models-3gk6&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref3&amp;quot;&amp;gt;Floating Point Precision and Its Limitations | by Umair Akbar | Medium — akbu.medium.com — https://akbu.medium.com/floating-point-precision-and-its-limitations-cfb7247d7789&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref4&amp;quot;&amp;gt;Kahan summation algorithm - Wikipedia — en.wikipedia.org — https://en.wikipedia.org/wiki/Kahan_summation_algorithm&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref5&amp;quot;&amp;gt;Pairwise summation - Wikipedia — en.wikipedia.org — https://en.wikipedia.org/wiki/Pairwise_summation&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref6&amp;quot;&amp;gt;Numerical behavior of NVIDIA tensor cores - PMC — pmc.ncbi.nlm.nih.gov — https://pmc.ncbi.nlm.nih.gov/articles/PMC7959640/&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref7&amp;quot;&amp;gt;Mixed precision and performance-accuracy tuning (neuron-cc) — AWS Neuron Documentation — awsdocs-neuron.readthedocs-hosted.com — https://awsdocs-neuron.readthedocs-hosted.com/en/latest/about-neuron/appnotes/neuron-cc/mixed-precision.html&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref8&amp;quot;&amp;gt;Create Float16 and Mixed Precision Models - ONNX Runtime — onnxruntime.ai — https://onnxruntime.ai/docs/performance/model-optimizations/float16.html&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref9&amp;quot;&amp;gt;[Performance] ONNX FP16 model is having performance bottle neck when compared to FP32 variant · Issue #25824 · microsoft/onnxruntime · GitHub — github.com — https://github.com/microsoft/onnxruntime/issues/25824&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref10&amp;quot;&amp;gt;Float stored in 8 bits - ONNX 1.21.0 documentation — onnx.ai — https://onnx.ai/onnx/technical/float8.html&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref11&amp;quot;&amp;gt;[2209.05433] FP8 Formats for Deep Learning — arxiv.org — https://arxiv.org/abs/2209.05433&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref12&amp;quot;&amp;gt;Should MatMul op allow specification of accumulation precision? · Issue #7072 · onnx/onnx · GitHub — github.com — https://github.com/onnx/onnx/issues/7072&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref13&amp;quot;&amp;gt;Quantize ONNX models | onnxruntime — onnxruntime.ai — https://onnxruntime.ai/docs/performance/model-optimizations/quantization.html&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref14&amp;quot;&amp;gt;Keep Attention Softmax FP32 during FP16/ZeRO Training · Issue #1485 · hpcaitech/ColossalAI · GitHub — github.com — https://github.com/hpcaitech/ColossalAI/issues/1485&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;/references&amp;gt;&lt;/div&gt;</summary>
		<author><name>Timo.stripf</name></author>
	</entry>
	<entry>
		<id>https://www.emmtrix.com/w139/index.php?title=Numerical_Precision_in_ONNX_and_AI_Inference&amp;diff=2890</id>
		<title>Numerical Precision in ONNX and AI Inference</title>
		<link rel="alternate" type="text/html" href="https://www.emmtrix.com/w139/index.php?title=Numerical_Precision_in_ONNX_and_AI_Inference&amp;diff=2890"/>
		<updated>2026-01-30T15:18:03Z</updated>

		<summary type="html">&lt;p&gt;Timo.stripf: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&#039;&#039;&#039;Open Neural Network Exchange (ONNX)&#039;&#039;&#039; is an open standard format for representing machine learning models and neural network computations across different frameworks and hardware&amp;lt;ref name=&amp;quot;ref1&amp;quot;/&amp;gt;. As models are exported and deployed via ONNX, the &#039;&#039;&#039;numerical precision&#039;&#039;&#039; of computations becomes critical. Deep learning inference involves a variety of floating-point operations, and small numerical discrepancies can accumulate and affect model accuracy or reproducibility. This article provides an in-depth overview of floating-point precision issues in numerical computing, with a focus on how they manifest in AI inference and the ONNX ecosystem. We cover floating-point formats and rounding behavior, accumulation strategies for summations, the use of low-precision formats (FP16, BFloat16, FP8) in modern AI, and how ONNX defines (or leaves unspecified) the precision of accumulators in operations. Strategies for maintaining numerical stability in deep neural networks are discussed, along with the trade-offs between computational performance and arithmetic accuracy. The goal is to give engineers and researchers a comprehensive, encyclopedic reference on numerical precision in ONNX-based AI inference systems.&lt;br /&gt;
&lt;br /&gt;
== Floating-Point Precision in Numerical Computing ==&lt;br /&gt;
&lt;br /&gt;
=== 2.1. IEEE 754 Floating-Point Formats ===&lt;br /&gt;
&lt;br /&gt;
Modern computers represent real numbers using the IEEE 754 floating-point standard, which defines formats like 32-bit &#039;&#039;single precision&#039;&#039; (FP32) and 64-bit &#039;&#039;double precision&#039;&#039; (FP64). In IEEE 754 binary formats, a floating-point number is composed of three fields: 1 sign bit, several exponent bits (with a bias), and several fraction (mantissa) bits. For example, FP32 consists of 1 sign bit, 8 exponent bits, and 23 fraction bits (24 bits of significand precision including the implicit leading 1)&amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;. This provides roughly 7 decimal digits of precision and an exponent range allowing values on the order of $10^{\pm38}$. FP64 allocates 1 sign bit, 11 exponent bits, and 52 fraction bits (53-bit precision), yielding about 15–16 decimal digits of precision and a vastly larger range (~$10^{\pm308}$)&amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;. These larger formats (especially FP64) are the &amp;quot;gold standard&amp;quot; for numerical accuracy in scientific computing, as they reduce round-off error in complex calculations&amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;. However, the increased precision comes at the cost of doubled storage and typically reduced computational throughput (e.g. many GPUs execute FP64 instructions at a much lower rate than FP32&amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
In addition to single and double precision, the IEEE 754-2008 standard introduced a 16-bit &#039;&#039;half precision&#039;&#039; format (FP16, or binary16). FP16 has 1 sign bit, 5 exponent bits, and 10 fraction bits (11-bit significand including the implicit 1)&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. This format covers a much smaller dynamic range (approximately $6.1\times10^{-5}$ to $6.5\times10^4$ for normalized values&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;) and offers only around 3 decimal digits of precision. Half precision was originally used in graphics, but it has gained popularity in machine learning for its speed and memory advantages (discussed later in Section 4). Another format relevant to AI is &#039;&#039;&#039;BFloat16&#039;&#039;&#039; (Brain Float 16), a 16-bit float with &#039;&#039;&#039;8 exponent bits and 7 fraction bits&#039;&#039;&#039;&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. BFloat16 sacrifices precision (only ~7 fraction bits ≈ 2–3 decimal digits) in order to have the &#039;&#039;&#039;same exponent range as FP32&#039;&#039;&#039;&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. This means BFloat16 can represent very large or very small numbers (∼$10^{\pm38}$, similar range as FP32) but with much less precision between representable values&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. BFloat16 was introduced for deep learning by Google (for TPUs) to allow reduced precision &#039;&#039;training&#039;&#039; without frequent overflow/underflow, relying on the wide range and accepting the coarse precision.&lt;br /&gt;
&lt;br /&gt;
Each floating-point format has a &#039;&#039;&#039;unit roundoff&#039;&#039;&#039; or machine precision, denoted $\epsilon$. This is effectively the spacing between 1.0 and the next representable number. For FP32, $\epsilon \approx 2^{-23} \approx 1.19\times10^{-7}$, corresponding to about 7–8 decimal digits of precision&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. In FP64, $\epsilon = 2^{-52} \approx 2.22\times10^{-16}$ (15–16 decimal digits). Half precision is much less precise: FP16 has $\epsilon = 2^{-10} \approx 9.76\times10^{-4}$&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;, and BFloat16 has $\epsilon = 2^{-7} = 7.8125\times10^{-3}$&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. These values quantify how finely the continuum of real numbers is quantized in each format.&lt;br /&gt;
&lt;br /&gt;
=== 2.2. Machine Epsilon and Rounding Error ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Machine epsilon ($\varepsilon$)&#039;&#039;&#039; is formally defined as the smallest positive number such that $1.0 + \varepsilon &amp;gt; 1.0$ in the floating-point format&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. It effectively measures the &#039;&#039;&#039;precision&#039;&#039;&#039; of the representation. In FP32, $\varepsilon = 2^{-23} \approx 1.19\times10^{-7}$, meaning any result is rounded to about 7 decimal digits. FP16’s $\varepsilon = 2^{-10} \approx 9.77\times10^{-4}$, over 1000× larger (so only ~3 decimal digits of precision), and BFloat16’s $\varepsilon = 2^{-7} \approx 7.8\times10^{-3}$, reflecting its mere ~8-bit precision&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. Rounding in IEEE 754 by default uses &#039;&#039;&#039;round-to-nearest (ties to even)&#039;&#039;&#039;, which ensures the rounding error for any single operation is at most half a unit in the last place (0.5 ULP). We can model a floating-point addition as: &lt;br /&gt;
&lt;br /&gt;
$$\text{fl}(a+b) = (a+b)\,(1 + \delta),$$ &lt;br /&gt;
&lt;br /&gt;
where $|\delta| \le \varepsilon$ for that format&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. Here $\varepsilon/2$ is the maximum &#039;&#039;&#039;relative error&#039;&#039;&#039; in one rounding (also called unit roundoff). For example, when adding two FP32 numbers, the result is accurate to within about $10^{-7}$ of the true sum (relative), whereas in BF16, the result can deviate by up to $\sim7.8\times10^{-3}$ (nearly 0.78% relative error per operation)&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. Thus, &#039;&#039;&#039;each arithmetic operation incurs a small rounding error&#039;&#039;&#039; (on the order of machine epsilon) that can propagate or accumulate through a sequence of calculations.&lt;br /&gt;
&lt;br /&gt;
Because floating-point representations have finite exponent range, we must also consider &#039;&#039;&#039;overflow&#039;&#039;&#039; and &#039;&#039;&#039;underflow&#039;&#039;&#039;. If a result’s magnitude exceeds the maximum representable value, it overflows to infinity; if it falls below the minimum normal (or minimum subnormal) value, it underflows to zero (or a denormalized tiny value). FP32 and BF16 share an exponent size (8 bits), so their range for normal numbers is roughly $[10^{-38}, 10^{38}]$ (approx $~\!3.4\times10^{38}$ max)&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. FP16’s 5-bit exponent allows max $\sim 6.55\times10^4$ and min normal $\sim 6.10\times10^{-5}$&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. In practice, &#039;&#039;&#039;underflow/overflow&#039;&#039;&#039; can occur in low-precision arithmetic if values are not appropriately scaled. For instance, summing a large number of moderately sized terms in FP16 could overflow where FP32 would not. As an example, summing on the order of $10^4$ values of magnitude ~$10^1$ (say, adding 10,000 terms around 10 each) would produce a total $\sim10^5$, which exceeds FP16’s max finite value ~$6.5\times10^4$ and would overflow to $+\infty$ in half precision&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. Underflow is usually a concern when subtracting nearly equal numbers (catastrophic cancellation yielding a tiny difference that might flush to zero in low precision).&lt;br /&gt;
&lt;br /&gt;
=== 2.3. Accumulation Error in Reductions ===&lt;br /&gt;
&lt;br /&gt;
Floating-point addition is &#039;&#039;&#039;not associative&#039;&#039;&#039;: $(a+b)+c$ can differ from $a+(b+c)$ due to rounding, and thus the order in which we sum a list of numbers can change the result slightly. When summing a large sequence of $N$ numbers $x_1, x_2, ..., x_N$, the &#039;&#039;&#039;accumulated rounding error&#039;&#039;&#039; can be significant. In the &#039;&#039;worst case&#039;&#039; (e.g., adding many positive and negative terms that cause maximal cancellation or always rounding one way), the error can grow on the order of $O(N \varepsilon)$ relative to the exact sum&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;. Simply summing $N$ numbers in sequence (the &#039;&#039;&#039;naive linear summation&#039;&#039;&#039;) has a worst-case error bound proportional to $N\,\varepsilon$&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. This worst-case occurs for adversarial arrangements of values; in more typical scenarios with rounding errors of random signs, one can model the error as a random walk, yielding an expected root-mean-square (RMS) error growth on the order of $O(\sqrt{N}\,\varepsilon)$&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;. Still, with very large $N$, even $\sqrt{N}$ times $\varepsilon$ can become large enough to matter.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Cancellation&#039;&#039;&#039; is a particular problem: if we add two nearly equal numbers of opposite sign, most leading digits cancel and the result loses significant bits of precision. For example, in single precision, subtracting two numbers that agree in the first 6 decimal digits will yield a result with only ~1 digit of accuracy left. Catastrophic cancellation can dramatically increase relative error. Summation is especially sensitive to cancellation if large positive and negative terms are summed naively. The &#039;&#039;&#039;condition number&#039;&#039;&#039; of a summation (the ratio of the sum of absolute values $\sum|x_i|$ to the absolute value of the true sum $|\sum x_i|$) indicates how sensitive the result is to perturbations&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;. If the numbers have mixed signs and nearly cancel out (making the condition number large), even an optimal summation order will have a large relative error. For instance, summing [1.0, $10^{100}$, 1.0, $-10^{100}$] in double precision has an exact sum of 2.0, but naive summation may yield 0.0 due to cancellation; an improved summation algorithm yields the correct 2.0&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
In practical terms, when summing a long list of floating-point numbers (such as accumulating a dot product or reducing a tensor), the &#039;&#039;&#039;accumulation error&#039;&#039;&#039; can become noticeable if no precautions are taken. Techniques to mitigate this error include carefully choosing the summation order or using higher precision for intermediate sums. We will explore these strategies in the next section.&lt;br /&gt;
&lt;br /&gt;
== Accumulators and Reduction Strategies ==&lt;br /&gt;
&lt;br /&gt;
When performing reductions (sums, dot products, etc.), the method of accumulation can greatly affect the accuracy of the result. Below, we discuss various strategies:&lt;br /&gt;
&lt;br /&gt;
=== 3.1. Naive Linear Accumulation ===&lt;br /&gt;
&lt;br /&gt;
The simplest accumulation strategy is to use a single accumulator and add each element sequentially. In C-like pseudocode:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;c&amp;quot;&amp;gt;&lt;br /&gt;
float sum = 0.0f;&lt;br /&gt;
for (int i = 0; i &amp;lt; N; ++i) {&lt;br /&gt;
    sum += array[i];&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This &#039;&#039;&#039;naive linear summation&#039;&#039;&#039; adds values one by one to the running total. While straightforward and fast, it is prone to the rounding error accumulation described earlier. The partial sum &amp;lt;code&amp;gt;sum&amp;lt;/code&amp;gt; continually grows (in absolute value) as terms are added, so new small terms may fall outside its precision. For example, if &amp;lt;code&amp;gt;sum&amp;lt;/code&amp;gt; is large and positive, adding a comparatively tiny positive number may produce no change because the small term&#039;s significant digits vanish in the presence of the large sum (the increment is below the rounding threshold of &amp;lt;code&amp;gt;sum&amp;lt;/code&amp;gt;). Conversely, adding a small number of opposite sign can suffer from cancellation. The order of addition matters: summing from smallest magnitude to largest (ascending order) tends to yield a more accurate result than summing in descending order, because adding small terms first preserves their contributions before the sum becomes very large. The naive loop above implicitly sums in the given index order; if the data is unsorted, this order is essentially arbitrary.&lt;br /&gt;
&lt;br /&gt;
The performance of linear accumulation is excellent (minimal overhead), and for many applications the resulting error is within acceptable limits. However, for &#039;&#039;&#039;very large N&#039;&#039;&#039; or ill-conditioned sums, the inaccuracy can become a problem. In a reduction involving millions of elements, a single-precision sum could lose several digits of accuracy. In deep learning, this could translate to small discrepancies in layer outputs or aggregated gradients if done naively in low precision.&lt;br /&gt;
&lt;br /&gt;
=== 3.2. Pairwise and Tree-Based Reductions ===&lt;br /&gt;
&lt;br /&gt;
A better approach to summation is &#039;&#039;&#039;pairwise summation&#039;&#039;&#039;, which sums numbers in a binary tree structure rather than linearly. The idea is to recursively split the array into halves, sum each half, then add the two partial sums. This can be implemented as:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight&amp;gt;&lt;br /&gt;
function pairwise_sum(x[1..n]):&lt;br /&gt;
    if n == 0:&lt;br /&gt;
        return 0&lt;br /&gt;
    if n == 1:&lt;br /&gt;
        return x[1]&lt;br /&gt;
    m = floor(n/2)&lt;br /&gt;
    left_sum  = pairwise_sum(x[1..m])&lt;br /&gt;
    right_sum = pairwise_sum(x[m+1..n])&lt;br /&gt;
    return left_sum + right_sum&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
By always adding numbers of similar magnitudes first (small subsets are reduced to subtotals, then those are added, etc.), pairwise summation avoids the situation of a very large running total absorbing small additions until the final steps. The error of pairwise summation grows logarithmically with $N$ in the worst case (roughly $O(\varepsilon \log_2 N)$)&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;, instead of linearly. Intuitively, at each level of the tree, rounding error is constrained, and there are about $\log_2 N$ levels. In fact, the worst-case relative error bound for pairwise summation (base case of 1 element) is:&lt;br /&gt;
&lt;br /&gt;
\[ |E_N| \le \frac{\varepsilon \log_2 N}{1 - \varepsilon \log_2 N} \sum_{i=1}^N |x_i|, \]&lt;br /&gt;
&lt;br /&gt;
which for practical $N$ simplifies to on the order of $\varepsilon \log_2 N$ times the condition number of the sum&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;. In typical random-error scenarios, the error behaves even better (on the order of $\sqrt{\log N}$)&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;. The key is that &#039;&#039;&#039;pairwise (tree) reduction balances the accumulations&#039;&#039;&#039;, preventing one sum from overwhelming the others too early.&lt;br /&gt;
&lt;br /&gt;
From a performance standpoint, pairwise summation has the same $O(N)$ total additions as linear summation, just a different order. It is also highly amenable to parallelization: independent partial sums can be computed in parallel at each level of the tree. Many numerical libraries and BLAS implementations use a form of blocked or pairwise summation in dot products for improved accuracy at negligible performance cost. For example, NumPy and Julia perform partial pairwise summation by default for better numerical behavior&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=== 3.3. Compensated Summation (Kahan, Neumaier) ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Compensated summation&#039;&#039;&#039; is an alternative technique to improve summation accuracy by tracking the small “lost” quantities. The most famous algorithm is the &#039;&#039;&#039;Kahan summation algorithm&#039;&#039;&#039; (also called Kahan–Babuška)&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. Kahan summation uses an extra variable to accumulate the round-off error. In pseudocode:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;c&amp;quot;&amp;gt;&lt;br /&gt;
double sum = 0.0;&lt;br /&gt;
double c = 0.0;    // compensation for lost low-order bits&lt;br /&gt;
for (i = 1; i &amp;lt;= N; ++i) {&lt;br /&gt;
    double y = x[i] - c;    // recover low-order bits by subtracting compensation&lt;br /&gt;
    double t = sum + y;     // perform the addition&lt;br /&gt;
    c = (t - sum) - y;      // compute new compensation (the error in t)&lt;br /&gt;
    sum = t;&lt;br /&gt;
}&lt;br /&gt;
return sum;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Here &amp;lt;code&amp;gt;c&amp;lt;/code&amp;gt; accumulates the tiny leftovers that would otherwise be dropped&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. Each iteration essentially subtracts the last error from the new addend (&amp;lt;code&amp;gt;y = x[i] - c&amp;lt;/code&amp;gt;) so that &amp;lt;code&amp;gt;t = sum + y&amp;lt;/code&amp;gt; includes the small bits from the previous operation. The new error is then stored in &amp;lt;code&amp;gt;c&amp;lt;/code&amp;gt; for the next iteration. The result is that the final &amp;lt;code&amp;gt;sum&amp;lt;/code&amp;gt; is as if computed with higher precision than the machine’s: Kahan’s algorithm can &#039;&#039;&#039;greatly reduce the error&#039;&#039;&#039; in summing a sequence of floats. In fact, with exact arithmetic for the compensation, Kahan summation yields a result accurate to within 1 or 2 ULP of the true sum, regardless of N (assuming the sequence of partial sums doesn’t overflow)&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. More precisely, Kahan summation achieves a &#039;&#039;&#039;worst-case error independent of N&#039;&#039;&#039; (error bound depends only on machine precision, not the number of terms)&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. In practice, using a double accumulator for summing single-precision numbers via Kahan can give near double-precision accuracy.&lt;br /&gt;
&lt;br /&gt;
An important subtlety is that the compensation variable &amp;lt;code&amp;gt;c&amp;lt;/code&amp;gt; should be kept in &#039;&#039;full precision&#039;&#039;. In the implementation above, we used &amp;lt;code&amp;gt;double&amp;lt;/code&amp;gt; for both sum and c. If summing FP32 values, one might use &amp;lt;code&amp;gt;float sum&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;float c&amp;lt;/code&amp;gt; (both 32-bit), which still improves accuracy, but using a higher precision for the accumulator and compensation (i.e. double) yields even better results. Many implementations of Kahan summation use the same type for sum and c, but it&#039;s assumed to be higher precision than the inputs when possible.&lt;br /&gt;
&lt;br /&gt;
Kahan’s algorithm carries a performance cost: each iteration does five arithmetic operations (two adds, one subtract, etc.) and has a data dependency, which can hinder parallelism or pipelining&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. Thus, it runs slower than naive summation (roughly 2–3× slower or more, depending on CPU pipeline). In loops where summation is a bottleneck, this may be significant. Nonetheless, for modest N or where maximum accuracy is required, compensated summation is extremely useful. In fact, Python’s &amp;lt;code&amp;gt;math.fsum&amp;lt;/code&amp;gt; function uses a variant of Kahan’s algorithm (actually a more advanced multi-part compensation) to sum iterables with high precision.&lt;br /&gt;
&lt;br /&gt;
There are variations and improvements on basic Kahan summation. &#039;&#039;&#039;Neumaier’s algorithm&#039;&#039;&#039; (sometimes called &#039;&#039;Kahan-Babuška-Neumaier&#039;&#039;) improves Kahan’s method by also handling cases where the next input is larger in magnitude than the running sum&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. In such cases, the original Kahan formula could suffer error; Neumaier’s version effectively swaps roles when a huge value comes in. Its pseudocode is:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight&amp;gt;&lt;br /&gt;
function NeumaierSum(array):&lt;br /&gt;
    sum = 0.0&lt;br /&gt;
    compensation = 0.0&lt;br /&gt;
    for each x in array:&lt;br /&gt;
        t = sum + x&lt;br /&gt;
        if (fabs(sum) &amp;gt;= fabs(x))&lt;br /&gt;
            compensation += (sum - t) + x;&lt;br /&gt;
        else&lt;br /&gt;
            compensation += (x - t) + sum;&lt;br /&gt;
        sum = t;&lt;br /&gt;
    return sum + compensation;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This algorithm ensures the compensation covers both scenarios: when the sum is the bigger addend or when the new term is bigger&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. In the end, it adds the final &amp;lt;code&amp;gt;compensation&amp;lt;/code&amp;gt; once to the result. Neumaier’s method can produce the correct result in cases where basic Kahan fails. For instance, summing [1.0, $+10^{100}$, 1.0, $-10^{100}$] in double precision: regular Kahan summation might give 0.0 (losing the small terms), whereas Neumaier’s algorithm yields 2.0 exactly&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;. There are also higher-order compensation methods (carrying multiple compensation terms, e.g., the &#039;&#039;&#039;Klein summation&#039;&#039;&#039; variant&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt; or using arbitrary precision for intermediate sums) that further reduce error at increased computational cost&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
In summary, compensated summation algorithms trade extra computation for greatly improved numerical accuracy. They are especially valuable when summing large arrays of numbers with varying magnitudes, where naive summation could lose most of the low-order bits. Many scientific computing environments provide compensated sum routines for this reason. In deep learning, however, a different approach to improved precision is often taken: &#039;&#039;&#039;using a wider accumulator type&#039;&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
=== 3.4. Mixed-Precision Accumulation ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Mixed-precision accumulation&#039;&#039;&#039; refers to performing arithmetic in a higher precision than the input data. A common scenario is accumulating sums in 32-bit floats when the operands are 16-bit floats. Rather than implementing a compensation algorithm, one can simply use a wider accumulator so that intermediate rounding error is smaller. This is effectively what modern GPUs do for tensor operations: for example, NVIDIA &#039;&#039;&#039;Tensor Cores operate on FP16 input data with an FP32 accumulator&#039;&#039;&#039; (the 16-bit products are accumulated in 32-bit)&amp;lt;ref name=&amp;quot;ref6&amp;quot;/&amp;gt;. In this way, summing 2048 products in a dot product has far less precision loss than it would with a 16-bit accumulator. Hardware that supports mixed precision can give the best of both worlds: fast low-precision multiplies and high-precision accumulation.&lt;br /&gt;
&lt;br /&gt;
Software can also leverage mixed precision. For instance, one can sum an array of &amp;lt;code&amp;gt;float&amp;lt;/code&amp;gt; values into a &amp;lt;code&amp;gt;double&amp;lt;/code&amp;gt; variable, accumulating the result in double precision and then converting back to float at the end:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;c&amp;quot;&amp;gt;&lt;br /&gt;
double sum = 0.0;&lt;br /&gt;
for (int i=0; i&amp;lt;N; ++i) {&lt;br /&gt;
    sum += (double) arr_float[i];&lt;br /&gt;
}&lt;br /&gt;
float result = (float) sum;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This approach often yields a more accurate &amp;lt;code&amp;gt;result&amp;lt;/code&amp;gt; than summing in single precision directly, because the intermediate sum carries 52-bit precision. The final rounding to float happens only once at the end (minimizing overall rounding error). In fact, the above is one way to implement &amp;lt;code&amp;gt;math.fsum&amp;lt;/code&amp;gt; in Python (it internally does something similar with multiple partial sums in double precision).&lt;br /&gt;
&lt;br /&gt;
In machine learning training and inference, &#039;&#039;&#039;mixed precision&#039;&#039;&#039; is widely used. When using FP16 for speed, it&#039;s common to accumulate with FP32. For example, &#039;&#039;&#039;Google&#039;s TPUs use BFloat16 for multiplication but accumulate in FP32&#039;&#039;&#039; for matrix ops&amp;lt;ref name=&amp;quot;ref6&amp;quot;/&amp;gt;. AWS’s Inferentia (Neuron) hardware similarly supports FP16/BF16 matrix multiplication with FP32 accumulation&amp;lt;ref name=&amp;quot;ref7&amp;quot;/&amp;gt;. The motivation is that summing hundreds or thousands of half-precision values would otherwise introduce intolerable error or risk overflow. With an FP32 accumulator, the reduction error is drastically reduced (by about $2^{13}$ times, since FP32 has 13 more precision bits than FP16). Mixed-precision accumulation thus provides a simple and effective form of compensation: &#039;&#039;&#039;use a larger bucket to collect many small contributions&#039;&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
There is a spectrum of options: summing int8 or int16 integers might use 32-bit integers as accumulators for the same reason (to avoid overflow). Some BLAS libraries even offer &#039;&#039;extended precision accumulators&#039;&#039; for single precision dot products (accumulating in double). The downside of mixed precision is the extra memory and maybe throughput cost of the wider type. But on many platforms, the add latency is dominated by hardware design rather than bit-width, so using a 32-bit adder vs 16-bit has negligible speed impact if supported natively. The key is whether data movement or register pressure increases.&lt;br /&gt;
&lt;br /&gt;
In summary, &#039;&#039;&#039;using a higher precision accumulator is often the easiest way to improve numerical accuracy&#039;&#039;&#039; for reductions. It is a primary strategy in deep learning hardware to maintain stability while exploiting low precision for storage and initial computations.&lt;br /&gt;
&lt;br /&gt;
== Precision Formats in AI and Machine Learning ==&lt;br /&gt;
&lt;br /&gt;
Modern AI systems use a variety of numeric formats to balance speed, memory, and accuracy. We discuss common precisions and their roles:&lt;br /&gt;
&lt;br /&gt;
=== 4.1. FP32 and FP64 in Training and Inference ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;FP32 (single precision)&#039;&#039;&#039; has been the workhorse of deep learning for years. Most training scripts in frameworks like TensorFlow and PyTorch used FP32 by default, as it offers sufficient precision to propagate gradients and accumulate weight updates over millions of iterations without excessive error. With ~7 decimal digits of precision and a wide exponent range, FP32 can handle the dynamic range of activations and gradients in typical neural networks. In inference, FP32 is often more precision than strictly necessary for final predictions, but it has remained common because of its ease and the fact that many GPU operations were optimized for FP32.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;FP64 (double precision)&#039;&#039;&#039; is rarely used in deep learning model training or inference, except in specific scenarios requiring extreme precision. FP64 is common in scientific computing and certain types of numerical simulations. In neural networks, however, the extra precision is usually not needed for model accuracy—using doubles tends to yield negligible improvements in model metrics but at a large performance cost (since on GPUs FP64 throughput can be 1/2, 1/8, or even 1/32 of FP32 throughput depending on hardware). One exception might be when integrating neural nets into larger physics simulations or when solving ill-conditioned problems as part of the model. Some researchers use FP64 to diagnose numerical issues or ensure reproducibility. Generally though, deep learning has favored lower precision for efficiency, and FP64 is considered overkill for most networks&amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
That said, certain classical machine learning algorithms benefit from double precision. For example, computing the inverse of a matrix (as in a Gaussian Process regression or Kalman filter embedded in a model) can incur large rounding errors in single precision, so using FP64 can significantly improve the result&amp;lt;ref name=&amp;quot;ref1&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref1&amp;quot;/&amp;gt;. The ONNX standard supports a float64 tensor type for such needs. In ONNX Runtime, however, emphasis has been on FP32 performance, and some pipelines default to float even if the original model was double&amp;lt;ref name=&amp;quot;ref1&amp;quot;/&amp;gt;. If a model’s prediction computation involves operations that are numerically sensitive (like matrix inversion, Cholesky decomposition, or summing extremely large counts), then FP64 may be required to avoid noticeable discrepancies&amp;lt;ref name=&amp;quot;ref1&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref1&amp;quot;/&amp;gt;. A good practice is to test a model in both float and double if you suspect precision issues; significant differences indicate an ill-conditioned calculation that might need higher precision.&lt;br /&gt;
&lt;br /&gt;
In summary, FP32 remains the &#039;&#039;&#039;default&#039;&#039;&#039; format for most training and inference due to its balance of accuracy and efficiency&amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;, while FP64 is reserved for niche cases or precision-critical subroutines. FP32 typically achieves the same accuracy as FP64 for end-to-end deep learning tasks, because network training can often compensate for small noise, and other sources of error (like generalization error) dominate. Therefore, the community trend has been moving to &#039;&#039;&#039;lower&#039;&#039;&#039; precisions, not higher, to gain speed — provided the models still converge and perform well.&lt;br /&gt;
&lt;br /&gt;
=== 4.2. FP16 and BFloat16 in Deep Learning ===&lt;br /&gt;
&lt;br /&gt;
To further speed up neural network training and inference, &#039;&#039;&#039;half-precision (FP16)&#039;&#039;&#039; was adopted, especially with the advent of GPUs supporting FP16 math at twice the rate of FP32. Using FP16 reduces memory storage and bandwidth by 50% compared to FP32, which is significant for large models. However, FP16’s small exponent range and precision posed challenges: training a large network purely in FP16 can fail due to gradient underflow/overflow or weight updates not registering (because changes are below the 0.001 quantization step). To address this, frameworks introduced &#039;&#039;&#039;mixed-precision training&#039;&#039;&#039;, where compute-intensive parts (matrix multiplies, convolutions) run in FP16, but certain accumulations and model parameters remain in FP32. For instance, NVIDIA’s &#039;&#039;&#039;Tensor Core&#039;&#039;&#039; units require FP16 (or BF16) inputs but accumulate results in FP32, effectively ensuring that reductions are done in higher precision&amp;lt;ref name=&amp;quot;ref6&amp;quot;/&amp;gt;. Research showed that with proper loss scaling and mixed precision, models can train to FP32-level accuracy using mostly FP16 arithmetic, achieving significant speedups.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;BFloat16 (BF16)&#039;&#039;&#039; emerged as an alternative 16-bit format primarily from Google’s TPU training systems. BF16 has a larger exponent (8 bits, same as FP32) but only 7 fraction bits&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. This means BF16 can handle the &#039;&#039;&#039;same dynamic range&#039;&#039;&#039; as FP32 (no overflows where FP32 wouldn’t) but at the cost of even lower precision than FP16 (BF16 has about 2–3 decimal digits of precision vs FP16’s ~3–4). Despite its coarse precision, BF16 turns out to be effective for training deep networks because the algorithms (stochastic gradient descent, etc.) are somewhat robust to that noise, and having the wide range prevents catastrophic loss of signal (no gradient will overflow to Inf unless it was going to in FP32 anyway). Many training frameworks (PyTorch, TensorFlow) support BF16 training on hardware like &#039;&#039;&#039;TPU, NVIDIA A100+, Intel CPUs with AVX-512 BF16, etc&#039;&#039;&#039;. Typically, when using BF16, &#039;&#039;&#039;accumulations are still done in FP32&#039;&#039;&#039; (for example, a TPU v2/v3 multiplies BF16 matrices and accumulates into an FP32 result for the dot product&amp;lt;ref name=&amp;quot;ref6&amp;quot;/&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
In inference, both FP16 and BF16 are used to deploy models for faster speed or lower memory. FP16 is popular on NVIDIA GPUs (which have specialized FP16 units). BF16 is advantageous on TPUs and some CPUs that support it, and increasingly on GPUs (Ampere and newer GPUs also support BF16 arithmetic). The choice between FP16 and BF16 can depend on hardware: BF16’s advantage is easier software conversion (you can often take an FP32-trained model and cast weights to BF16 with minimal loss, since the dynamic range is intact), whereas FP16 might require adjusting the model or ensuring values fit in range (sometimes needing techniques like maintaining a master copy of weights in FP32 during training, or handling out-of-range values).&lt;br /&gt;
&lt;br /&gt;
For ONNX, both Float16 and BFloat16 types are part of the standard. ONNX models can be converted to FP16 (truncating weights and inserting cast nodes for computations) using tools like &amp;lt;code&amp;gt;float16_converter&amp;lt;/code&amp;gt; in ONNX Runtime&amp;lt;ref name=&amp;quot;ref8&amp;quot;/&amp;gt;. ONNX Runtime on GPUs will typically execute those FP16 ops with mixed precision on tensor cores (FP16 input, FP32 accumulate), whereas on CPU it may emulate FP16 or upcast to FP32 (as many CPU implementations do not natively support half-precision math)&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;. In fact, as an example, running an ONNX model in FP16 on a CPU might insert hidden Cast operations to FP32 behind the scenes, because the CPU kernel for that op only exists in FP32&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;. This ensures correctness (the computation is done in higher precision) but can reduce performance. We will revisit such backend-dependent behavior in Section 5.3.&lt;br /&gt;
&lt;br /&gt;
In summary, &#039;&#039;&#039;FP16 and BF16&#039;&#039;&#039; are now established as important low-precision formats in deep learning. FP16 offers more precision bits, which can matter for e.g. small differences, but has a limited range (care needed to avoid overflow). BF16 offers safety in range but less precision (which can slightly affect convergence or final accuracy in some cases). Both typically rely on FP32 for accumulation to achieve acceptable accuracy. When deploying models, one should choose a format supported efficiently by the target hardware and verify that the accuracy is not significantly degraded. Often, CNNs and transformers can run in FP16/BF16 with virtually no drop in score, but certain operations (like softmax, see Section 6.3) might need special handling.&lt;br /&gt;
&lt;br /&gt;
=== 4.3. FP8 Formats (E4M3, E5M2) and Scaling ===&lt;br /&gt;
&lt;br /&gt;
The push for ever lower precision has led to experimental &#039;&#039;&#039;8-bit floating point (FP8)&#039;&#039;&#039; formats for deep learning. In 2022, researchers from NVIDIA, Intel, and Arm proposed an FP8 standard with two variants: &#039;&#039;&#039;E4M3&#039;&#039;&#039; (4 exponent bits, 3 fraction bits) and &#039;&#039;&#039;E5M2&#039;&#039;&#039; (5 exponent bits, 2 fraction bits)&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;. Both have 1 sign bit, for a total of 8 bits. E5M2 has a wider exponent range (5 bits gives bias 15) but one fewer mantissa bit than E4M3. The idea is to use E4M3 for numbers that need more precision (like weights or activations around 1.0) and E5M2 for numbers that need more dynamic range (like gradient updates which can be very small or large). In fact, the study found that using E4M3 for forward activations/weights and E5M2 for backward gradients can allow training of large networks with FP8 to accuracy on par with FP16 training&amp;lt;ref name=&amp;quot;ref11&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
FP8 formats are &#039;&#039;not&#039;&#039; IEEE 754 standard (though IEEE is considering lower precision in its 2019 revision). Notably, the E4M3 format as used by NVIDIA does not allocate any bit-pattern for $+\infty$ or $-\infty$ – it has only NaN for invalid results, using that freed bit pattern to extend the representable range by one exponent value&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;. Essentially, E4M3 can represent numbers up to ~$2^{15}$ (around 3e4) instead of capping at infinity at $2^{16}$, by foregoing infinities&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;. Variations also exist regarding negative zero and how NaNs are handled&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;. For instance, GraphCore’s IPU uses FP8 formats that disallow negative zero and infinities, to maximize useable range&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Using 8-bit floats for neural networks requires additional techniques, chiefly &#039;&#039;&#039;scaling&#039;&#039;&#039;. Because 8-bit has very limited precision and range, one typically applies a power-of-two scale factor to groups of values to normalize them into an FP8-friendly range. This is akin to block floating-point or “microscaling” per layer or per tensor. For example, one might keep a FP32 scale for each channel or tensor, and represent values as $(FP8\_value) \times 2^{scale}$. The ONNX standard has introduced support for FP8 in version 1.15, including types for E4M3 and E5M2 (with variations FNUZ: finite-only and no negative zero)&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;, and even a type called &#039;&#039;&#039;E8M0&#039;&#039;&#039; which is an 8-bit exponent-only number used purely as a scale factor&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;. The E8M0 type has 8 exponent bits, no mantissa, effectively representing powers of two from $2^{-127}$ to $2^{128}$ for scaling purposes&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;. This indicates an approach where an FP8 tensor would be accompanied by an E8M0 scale (or scales) to interpret its value correctly.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Rounding&#039;&#039;&#039; becomes critical when casting values to FP8. The quantization error is much larger relative to FP16 or FP32. Studies have shown that choosing the right rounding mode (e.g., round-to-nearest, stochastic rounding, etc.) and clamping behavior (saturating to max instead of inf) can affect model accuracy&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;. ONNX added a &amp;lt;code&amp;gt;round_mode&amp;lt;/code&amp;gt; attribute to its Cast operator to allow controlling this for FP8&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;. In particular, one paper found that &#039;&#039;&#039;rounding up with saturation&#039;&#039;&#039; (round-to-nearest ties away from 0, and clamp overflows to max finite rather than inf) gave better accuracy in large language model training than other rounding modes&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
At present, FP8 is on the cutting edge. NVIDIA’s H100 GPU supports FP8 tensor cores (for both E4M3 and E5M2), and early results show minimal accuracy loss on some vision and NLP tasks when properly tuned. However, using FP8 requires fine-tuning hyperparameters like scaling factors for each layer and perhaps tweaking training procedures. ONNX’s inclusion of FP8 types means you can represent and transport models using FP8 weights or activations, but actual hardware support is limited and typically paired with calibration or dynamic scaling logic outside of ONNX graph (or using custom ONNX functions for scaling). It remains an &#039;&#039;&#039;open research area&#039;&#039;&#039; how to fully automate FP8 quantization for arbitrary models without accuracy loss.&lt;br /&gt;
&lt;br /&gt;
In summary, FP8 formats promise further compression of neural network computation and data. They are an order of magnitude more error-prone than FP16, but when combined with careful scaling, mixed usage of E4M3/E5M2, and maybe modified training techniques, they have shown surprising success. ONNX is actively evolving to support these ultra-low precision types, signaling their potential importance in future AI inference deployments.&lt;br /&gt;
&lt;br /&gt;
== Accumulation Semantics in ONNX ==&lt;br /&gt;
&lt;br /&gt;
One of the roles of the ONNX specification is to define the mathematical behavior of operators (Conv, MatMul, Sum, etc.) in a framework-agnostic way. However, ONNX mostly defines &#039;&#039;what&#039;&#039; an operator computes in exact arithmetic, not &#039;&#039;how&#039;&#039; the computation is carried out in finite precision. This leaves certain details – like accumulator precision or summation order – up to the implementation (runtime or hardware). We&#039;ll examine how ONNX treats accumulation and what guarantees (or lack thereof) are provided.&lt;br /&gt;
&lt;br /&gt;
=== 5.1. Operator Definitions and Numerical Guarantees ===&lt;br /&gt;
&lt;br /&gt;
Each ONNX operator is described as performing an ideal mathematical function on its inputs. For example, &amp;lt;code&amp;gt;MatMul&amp;lt;/code&amp;gt; is defined as the standard matrix product of two tensors, and &amp;lt;code&amp;gt;ReduceSum&amp;lt;/code&amp;gt; computes a summation along given axes. The ONNX spec, as of current versions, &#039;&#039;&#039;does not mandate the precision of intermediate calculations&#039;&#039;&#039; beyond the types of the inputs and outputs. If an operator’s inputs and outputs are float16, the spec implies the computation is done in float16 in a sense, but it does not forbid an implementation from using higher precision internally – it simply doesn&#039;t mention it. There is &#039;&#039;&#039;no explicit guarantee of identical bit-for-bit results&#039;&#039;&#039; across different ONNX backends for floating-point ops. Instead, results are expected to be numerically close, within tolerances one would normally expect from floating-point differences (due to different hardware or algorithms). The ONNX Backend Test suite typically uses a relative or absolute error tolerance when validating outputs for this reason.&lt;br /&gt;
&lt;br /&gt;
Because of this, important details like the use of extended precision or accumulation in higher precision are left ambiguous. This has led to discussions in the ONNX community about making such behavior more explicit. For example, a proposal was raised to allow specification of accumulation precision for certain ops like MatMul&amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;. The reasoning is that as lower-precision types (float16, bfloat16, float8) become more common, knowing or controlling whether an op internally accumulates in a higher precision is important for both accuracy and reproducibility&amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;. In June 2025, an issue on the ONNX GitHub suggested adding an attribute to MatMul (and similarly Gemm, Softmax, etc.) to specify the accumulator type&amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;. By default it would use input precision (status quo), but an attribute could request (for instance) 32-bit accumulation even if inputs are 16-bit&amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;. As of this writing, however, no such attribute exists in the official spec – it&#039;s an open issue. Thus, the numerical semantics are effectively: &#039;&#039;“compute the result as if in the same type as inputs, but actual implementations may use more precision internally as an optimization.”&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
From a guarantee standpoint, ONNX operators promise the mathematically correct result assuming real-number arithmetic, but they &#039;&#039;&#039;do not guarantee bit-exact results&#039;&#039;&#039; due to floating-point effects. There is an understanding that differences can arise from different summation orders, fused operations (e.g., an implementation might use an FMA (fused multiply-add) which has one rounding instead of two), or use of extended precision registers. ONNX does not formally define rounding modes either – it relies on IEEE 754 defaults, but if a backend used something like deterministic summation or alternative rounding, ONNX has no mechanism to express that.&lt;br /&gt;
&lt;br /&gt;
In summary, ONNX&#039;s current stance is largely &#039;&#039;&#039;implementation-dependent&#039;&#039;&#039; for numerical precision. It provides minimal explicit numerical guarantees beyond type constraints. This is why an ONNX model run on two different hardware or runtime libraries can have slightly different results, especially in reduced precision. The burden is on backends to produce results &amp;quot;close enough&amp;quot; to each other that they don&#039;t affect the model&#039;s decisions in any significant way.&lt;br /&gt;
&lt;br /&gt;
=== 5.2. MatMul, Conv, and Reduction Operators ===&lt;br /&gt;
&lt;br /&gt;
Consider ONNX &amp;lt;code&amp;gt;MatMul&amp;lt;/code&amp;gt;: it takes two tensors (often two matrices) and produces their matrix product. In pure math, $C_{ij} = \sum_k A_{ik} &#039;&#039; B_{kj}$. If A and B are float16, should that sum be computed in float16 or float32? As discussed, ONNX doesn’t specify – it simply says the output is a float16 matrix containing the product. In practice, many backends will &#039;&#039;promote&#039;&#039; the computation: for example, NVIDIA GPUs will do the multiplication and addition in a mixed FP16/FP32 mode by default (because of tensor cores), then convert the final result back to float16. ONNX Runtime on CPU might not support float16 multiplication natively, so it will insert cast ops to float32, perform the MatMul in float32, then cast back to float16&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;. From ONNX’s perspective, as long as the final result is within acceptable error of the “ideal” float16 computation, it’s fine. But note, an “ideal” float16 computation (where every multiply and add is rounded to FP16) might actually be &#039;&#039;less accurate* than what the backend did with FP32. This means that a backend using higher precision can produce a result that is not bit-equal to a pure FP16 implementation – it&#039;s actually closer to the true real number result. This is usually considered a good thing (improved accuracy), but it does highlight a lack of consistency: another backend might do everything in pure FP16 and get a slightly different answer.&lt;br /&gt;
&lt;br /&gt;
The same situation occurs for &#039;&#039;&#039;convolution&#039;&#039;&#039; (&amp;lt;code&amp;gt;Conv&amp;lt;/code&amp;gt; op) which is essentially a batched sum of products (like MatMul in sliding window), and for explicit reduction ops like &amp;lt;code&amp;gt;ReduceSum&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;ReduceMean&amp;lt;/code&amp;gt;, etc. If you have an ONNX &amp;lt;code&amp;gt;ReduceSum&amp;lt;/code&amp;gt; that sums 1000 float16 values, the spec doesn’t say how to sum them. A GPU kernel might sum in a tree using FP32 accumulators; a naive implementation might sum in order with FP16 and possibly overflow or underflow intermediate steps. Both are allowed as long as they output a float16 result that is “reasonable.” In practice, ONNX Runtime and others take care to avoid obviously bad behavior (like overflow) by using higher precision or by splitting the sum. But again, it’s not mandated by the ONNX standard explicitly.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Softmax&#039;&#039;&#039; is an interesting case: Softmax involves an internal sum of exponentials. ONNX defines the Softmax operator as $$\mathrm{Softmax}(x)_i = \frac{\exp(x_i)}{\sum_j \exp(x_j)},$$ typically applied along a specified axis. Directly computing this in float16 can be problematic for large vectors, as discussed in Section 6.3. Indeed, the issue of accumulation precision arises: should the sum $\sum_j \exp(x_j)$ be done in FP16 or can it be in FP32? Many implementations will do it in FP32 even if $x$ is FP16, because the dynamic range of $\exp(x)$ is large. The ONNX Softmax spec doesn’t detail this, but it expects the result to be a proper probability distribution (summing to 1.0 within normal floating error). A pure FP16 softmax on, say, 1000 elements could sum to something like 0.996 or 1.004 due to rounding, whereas FP32 accumulation would be much closer to 1.000. ONNX likely allows both; the small difference is usually inconsequential, but if it were large (say the FP16 sum overflowed to inf), that would be a correctness issue. A robust backend will prevent that (by using the common max-subtraction trick and higher precision accumulation). The ONNX issue 7072 indeed calls out Softmax as another op where explicit control of accumulation precision would be useful&amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
In summary, &#039;&#039;&#039;MatMul, Conv, and reduction ops&#039;&#039;&#039; in ONNX are defined mathematically but with &#039;&#039;implicit&#039;&#039; numerical behavior. Implementations commonly use higher precision for intermediate sums especially when input is low precision, but this is not standardized. Consequently, results can differ slightly between implementations. For critical applications, one must be aware of these differences. A model exported to ONNX with float16 weights might perform slightly differently on one runtime vs another if one does all float16 math and the other uses float32 accumulations. Both conform to ONNX. As a user, if you require a certain precision for accumulation, currently you have to ensure it by the way you export or run the model (for example, insert manual cast nodes in the ONNX graph to force certain ops to FP32). We may see future ONNX versions address this with attributes or metadata as mentioned, but for now these ops are effectively “compute with best effort accuracy”.&lt;br /&gt;
&lt;br /&gt;
=== 5.3. Backend and Hardware-Dependent Behavior ===&lt;br /&gt;
&lt;br /&gt;
Because ONNX is implemented by many backends (ONNX Runtime, TensorRT, CoreML, PyTorch JIT, etc.), and these in turn run on varied hardware (CPU, GPU, accelerators), the numeric behavior can be backend-dependent. Some examples:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;CPU backends:&#039;&#039;&#039; Many CPUs do not have native float16 vector arithmetic (with a few exceptions in recent CPUs). ONNX Runtime’s default CPU execution provider will often upcast float16 to float32, compute, then downcast&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;. This ensures accuracy but means that if you thought you were getting a full float16 pipeline, you’re not – the CPU is essentially ignoring the low precision in compute. On the other hand, x86 CPUs have extended 80-bit precision in x87 registers (though less used now) and 64-bit SIMD registers for doubles; some compiler optimizations might carry intermediate results in higher precision. This could make CPU results slightly different from a GPU’s if, say, a sum is done in 80-bit on CPU versus 32-bit on GPU. Generally, modern compilers avoid such extended precision unless explicitly asked, to improve reproducibility.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;GPU backends:&#039;&#039;&#039; GPUs often perform summations in parallel using tree reductions. The order of operations can change depending on how threads are scheduled. This means even on the same GPU, summing an array in one launch versus another might not give bit-identical results if the internal parallel reduction algorithm differs (though it should be deterministically the same given the same library version and input, but if you change GPU model or BLAS library, it might differ). Also, GPUs have fused multiply-add (FMA) instructions that do $(a*b + c)$ in one step with one rounding, which can produce slightly different results than doing the multiply and add separately (which would round twice). If an ONNX backend uses a fused kernel (like a GEMM library) it might yield different low-order bits than a simple loop.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Vendor libraries:&#039;&#039;&#039; ONNX Runtime can use vendor BLAS or DNN libraries (MKL, cuDNN, TensorRT, etc.). These libraries often implement their own strategies for numerical stability. For example, NVIDIA’s cuDNN might use FP32 accumulation for FP16 inputs by default. TensorRT (NVIDIA’s inference engine) will sometimes automatically promote precisions or insert scaling to maintain stability&amp;lt;ref name=&amp;quot;ref13&amp;quot;/&amp;gt;. On the other hand, if you explicitly request FP16 in TensorRT with no FP32 fallback, it might do the entire op in FP16 and you’d see more error. Some libraries offer knobs: TensorRT allows choosing FP32, FP16, or INT8 precision for each layer, and will use FP32 internally for safety in some cases unless forced otherwise.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Hardware-specific quirks:&#039;&#039;&#039; As another example, NVIDIA’s &#039;&#039;&#039;TensorFloat-32 (TF32)&#039;&#039;&#039; on Ampere GPUs is a precision mode where matrix multiplications use 10-bit mantissa (so about FP16 precision for the multiply) but keep 8-bit exponent (range like FP32). By default, CUDA libraries use TF32 for FP32 matrix multiplies on Ampere to speed them up, unless you disable it. This means if you run an ONNX model with FP32 on an A100 GPU, your matmuls might actually be lower precision than FP32 (though higher than FP16)&amp;lt;ref name=&amp;quot;ref6&amp;quot;/&amp;gt;. It’s an example of hardware choosing an intermediate precision for performance. From ONNX’s perspective, the output is still FP32 and (hopefully) within error tolerances, but a user might be surprised that it’s not a “true” FP32 computation internally. If exactness is needed, one has to turn off TF32.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Non-determinism:&#039;&#039;&#039; Some backends (especially on GPUs) allow non-deterministic execution for performance. For instance, atomic adds in parallel reductions can produce results that vary run-to-run because the order of accumulation is race-condition-dependent. ONNX’s spec doesn’t cover this explicitly, but it’s something to consider. In ONNX Runtime, there are settings for deterministic computing, but by default it may use all available performance optimizations, even if results vary at the 1e-7 level.&lt;br /&gt;
&lt;br /&gt;
In short, the numerical behavior under ONNX can depend on the combination of backend software and hardware capabilities. The lack of a strict spec for accumulation means implementations have freedom to use clever strategies (higher precision, reordering) to improve accuracy or speed. Usually this is positive (e.g. getting better accuracy than a naive approach). But it could also lead to slight inconsistencies. For example, you might find that exporting a PyTorch model to ONNX and running it with one engine yields outputs slightly different from PyTorch’s – not because ONNX is wrong, but because the summation or convolution was done differently (maybe PyTorch summed in FP32 as well, but maybe not in exactly the same way).&lt;br /&gt;
&lt;br /&gt;
The key takeaway is that ONNX &#039;&#039;by design&#039;&#039; abstracts away these details, which is good for portability but means you need to &#039;&#039;&#039;trust the backend&#039;&#039;&#039; or verify its precision behavior for sensitive models. If necessary, you can enforce certain behavior by inserting ops (for example, if you want to ensure FP32 accumulation, you could insert a cast to FP32 for inputs of a MatMul and cast the result back to FP16 – this way the ONNX graph itself mandates FP32 compute in between). This of course sacrifices some performance or device-specific magic.&lt;br /&gt;
&lt;br /&gt;
Looking forward, as mentioned, there are proposals to enrich ONNX’s specification regarding precision. This might include explicit flags for “exact summation” or “accumulate in int32” etc., which would make models more self-descriptive but also potentially less portable (if a backend doesn’t support that, what then?). For now, users should be aware of these backend differences. Most of the time, well-engineered ONNX runtimes yield results close enough that they don&#039;t affect the application (e.g., classification top-1 remains the same even if low-level sums differ by 1e-5). But for high-stakes numerical computations, testing and maybe adjusting the model or backend settings is prudent.&lt;br /&gt;
&lt;br /&gt;
== Numerical Stability in Deep Learning Models ==&lt;br /&gt;
&lt;br /&gt;
Deep learning computations involve many operations that, while theoretically well-defined, can be sensitive to numerical issues when implemented in finite precision. We&#039;ll discuss a few common scenarios in neural networks where numerical stability is a concern, and how precision and accumulation play a role.&lt;br /&gt;
&lt;br /&gt;
=== 6.1. Dot Products and Large-Scale Reductions ===&lt;br /&gt;
&lt;br /&gt;
Neural networks are full of dot products: every fully-connected layer computes $\mathbf{y} = W\mathbf{x}$, which is a series of inner products between weight vectors and the input. Convolutional layers compute weighted sums of local regions. Attention mechanisms compute large matrix products of query/key/value matrices. All these boil down to summing a lot of terms: $\sum_{k=1}^K a_k b_k$. When $K$ is large (it could be in the thousands or more for a single neuron in a fully connected layer of a large model), the potential for floating-point error in that sum increases.&lt;br /&gt;
&lt;br /&gt;
If using FP32, the error may still be negligible in many cases, but in lower precision like FP16, it can become significant. For example, consider a dot product of length 2048 in FP16. Each product is in FP16 (with ~3 decimal digits precision) and if accumulated in FP16, the worst-case error could be on the order of $2048 \times \varepsilon_{FP16} \approx 2048 \times 0.0009766 \approx 2$ (i.e., you could lose a couple units in the third decimal place relative accuracy in worst case). Even in random error accumulation, you might see error on the order of $\sqrt{2048} \times 0.0009766 \approx 0.044$ or 4.4% (though this is a rough estimate). That’s quite significant. Fortunately, as discussed, most hardware will accumulate such sums in a higher precision like FP32, dramatically reducing that error (~7 orders of magnitude smaller $\varepsilon$). But if it didn’t, the network’s outputs could get noisy. A fully-connected layer with many inputs might output slightly different results depending on summation order if done in low precision.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Large-scale reductions&#039;&#039;&#039; also occur in operations like global average pooling (summing all elements of a feature map) or computing the total loss by summing loss over batch samples. If one sums 100,000 values in FP32, the worst-case error could be on the order of 100,000 &#039;&#039; 1e-7 ≈ 1e-2 (1% uncertainty), though typical error would be much less. In FP16, summing 100k values is likely impossible without overflow: as noted, adding ~10^4 moderate numbers can overflow FP16&amp;lt;ref name=&amp;quot;ref3&amp;quot;/&amp;gt;. So large reductions &#039;&#039;must* be handled carefully (either in chunks or in higher precision). Frameworks generally do: for example, when performing a sum reduction on GPU, libraries break it into parallel blocks that accumulate in registers (usually 32-bit registers, even if inputs are 16-bit). &lt;br /&gt;
&lt;br /&gt;
Another aspect is that many dot products in neural nets involve values of varying magnitude. If the distribution of terms is such that some are much larger than others, the smaller terms might not contribute much if summed last. Ideally, one might sort by magnitude or use pairwise summation to improve that. Most BLAS libraries effectively do a pairwise strategy due to their parallel structure. So, while worst-case analyses are grim, practical implementations mitigate a lot of it.&lt;br /&gt;
&lt;br /&gt;
However, certain layers can suffer if not handled properly. One famous example is in RNNs or LSTMs: if one uses FP16 naive summation for the gating mechanism, errors could accumulate over many time steps and cause drift or instability. In modern practice, recurrent layers are often run in FP16 with FP32 accumulators to avoid this.&lt;br /&gt;
&lt;br /&gt;
In summary, &#039;&#039;&#039;dot products and large reductions need sufficient precision&#039;&#039;&#039; or robust algorithms. If you are implementing a custom kernel (say a custom ONNX op) and you have to sum a huge array, you should consider using a technique from Section 3 (pairwise summation, Kahan, or at least double accumulator if possible). Many deep learning computations, by virtue of hardware and library design, already do something like this under the hood. But it&#039;s always good to be aware, especially if pushing into lower precision territories.&lt;br /&gt;
&lt;br /&gt;
=== 6.2. Cancellation, Dynamic Range, and Conditioning ===&lt;br /&gt;
&lt;br /&gt;
We introduced &#039;&#039;cancellation&#039;&#039; earlier in the context of summation. In deep learning, cancellation can appear in a few places. One is in the computation of certain norms or variances. For instance, Batch Normalization computes mean and variance of activations. The variance formula $\mathrm{Var}(x) = E[x^2] - (E[x])^2$ is subject to catastrophic cancellation if $E[x]$ and $\sqrt{E[x^2]}$ are close in magnitude (which can happen if the variance is small compared to the mean). If implemented naively in single precision, you might get a negative variance due to numerical error (which would then be clamped to zero or sqrt would yield NaN). Robust implementations compute variance in a single pass or increase precision. Indeed, in many frameworks, batchnorm accumulation is done in FP32 even if the activations are FP16, precisely to avoid numerical issues in computing mean and variance. ONNX itself doesn’t specify this, but an ONNX backend is likely to do batchnorm stats in higher precision for safety. The &#039;&#039;conditioning&#039;&#039; of the variance calculation is poor when variance is tiny relative to mean, so it&#039;s a spot where higher precision is needed.&lt;br /&gt;
&lt;br /&gt;
Another example is in certain loss functions or regularizations. If you subtract two large numbers to get a small difference, you lose precision. Say you have two nearly identical predictions and you subtract them as part of some operation – if done in float16, you might get zero whereas float32 would have a slight difference. This could affect, say, a gradient if that difference was supposed to propagate information. Fortunately, neural nets often avoid directly subtracting large nearly equal numbers; but it can happen. In attention mechanisms, the &#039;&#039;score normalization&#039;&#039; (softmax) subtracts a max to avoid huge exponentials, which is good for range but if multiple scores are close to the max, you subtract numbers that are close. However, those subtractions are not catastrophic in the same way; they&#039;re intentional to reduce range and are typically fine in FP32 or even FP16.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Dynamic range&#039;&#039;&#039; issues are everywhere in deep learning. Activation functions like ReLU and exponential can produce very large or very small values. If subsequent computations don’t account for that, you get infinities or zeros. For example, consider a network output that produces a logit of 100 in FP16. $\exp(100)$ in FP16 overflows to infinity (since FP16 max ~ 6e4 and $e^{100} \sim 3.7e43$). If you then do softmax, that one infinity will dominate the sum and you might get a probability of 1 for that class (or NaNs if not handled). The standard softmax implementation subtracts the max logit, so in this case it would subtract 100 from all logits, making the largest exponent $\exp(0)=1$ and others tiny – this avoids overflow. But what about underflow? If you had a very negative logit, $\exp(-100)$ in FP16 would underflow to 0. In softmax, that just contributes ~0 probability, which is fine (it effectively is zero anyway). Underflow tends to be less dangerous than overflow in inference — underflow usually just means “this probability/neuron is effectively 0,” whereas overflow can poison the results with NaNs or Infs. &lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Conditioning&#039;&#039;&#039; refers to how sensitive a function is to input perturbations. Neural networks themselves as functions can be poorly conditioned (e.g., tiny changes in weights cause big changes in output for some architectures), but typically they are designed to be reasonably well-behaved. Certain internal computations like matrix inversion (if one does a pseudoinverse or something for a layer) can be extremely ill-conditioned if the matrix is near singular. That again would need double precision perhaps. In ML, one common ill-conditioned operation is the &#039;&#039;&#039;softmax denominator&#039;&#039;&#039;: $\sum_j e^{x_j}$ can be very large and also very sensitive to changes in the largest $x_j$. However, the softmax is more stable if computed carefully (max trick). Another is &#039;&#039;&#039;normalization&#039;&#039;&#039;: computing $\frac{x - \mu}{\sigma}$ for layer norm or batch norm – if $\sigma$ is very small, then the division blows up noise. In those cases, a small $\epsilon$ is usually added to $\sigma^2$ to prevent instability (this is a model-level fix, not a numeric one per se, but it acknowledges numeric limits).&lt;br /&gt;
&lt;br /&gt;
In training, &#039;&#039;&#039;optimizers&#039;&#039;&#039; can suffer from precision issues. For example, the Adam optimizer keeps a moving average of squared gradients; in half precision, these small updates might flush to zero or not change at some point. Some research found that certain adaptive optimizers break in FP16 because the small differences are lost (&amp;quot;Why FP16 training breaks NAdam&amp;quot; etc.). The solution is often to keep the optimizer states in FP32 while doing the bulk of compute in FP16. This again is a form of mixed precision.&lt;br /&gt;
&lt;br /&gt;
From an inference perspective, once the model is trained, the main concerns are avoiding overflow in activations and ensuring summations (like in softmax or reductions) are done with enough precision. The model’s weights are fixed, so we don&#039;t have to worry about cumulative error over iterations as in training, just the forward pass.&lt;br /&gt;
&lt;br /&gt;
=== 6.3. Normalization Layers and Softmax ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Normalization layers&#039;&#039;&#039; (BatchNorm, LayerNorm, etc.) and &#039;&#039;&#039;Softmax&#039;&#039;&#039; are specifically worth examining for numerical stability:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Batch Normalization:&#039;&#039;&#039; During inference, batchnorm applies an affine transform using precomputed mean and variance (from training). It’s basically: $y = \frac{x - \mu}{\sqrt{\sigma^2 + \epsilon}} \gamma + \beta$. This is a stable operation as long as $\epsilon$ is provided (to avoid division by zero). Since $\mu, \sigma^2$ are constants in inference, it reduces to linear ops which are fine. The main numerical work (computing mean/variance) happened during training or model calibration, and frameworks usually did that in FP32. So batchnorm at inference time is not a source of instability (it’s just a scale and shift of the data).&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Layer Normalization / Group Normalization:&#039;&#039;&#039; These compute mean/var on the fly for each sample (and possibly channel group). In inference, they still need to compute these statistics for each forward pass. If implemented in low precision, the variance calculation can suffer cancellation as mentioned. However, it&#039;s likely that inference engines compute the mean and variance in FP32 even if inputs are FP16 (especially since these are per-sample operations, not huge batch reductions – it&#039;s feasible to do in higher precision). If someone implemented layer norm purely in FP16, they might see issues when the variance is very small. Usually there’s an $\epsilon$ added which might dominate a tiny variance and thus mitigate catastrophic cancellation (the result might just output something mostly from $\epsilon$). In summary, normalization layers are usually fine as long as the implementation uses enough precision for the summation of squares.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Softmax:&#039;&#039;&#039; Softmax is notorious if done improperly. The safe algorithm: subtract the maximum input from all inputs (to bring the largest exponent to 1.0), exponentiate, sum them up, divide each exponent by the sum. The max subtraction handles overflow in $\exp()$. But what about the sum? As discussed, if there are $N$ elements in the softmax, the sum is at most $N$ (when all inputs equal the max). In classification, $N$ might be the number of classes. For ImageNet, $N=1000$, so worst sum ~1000 (which is fine in FP16, as 1000 &amp;lt;&amp;lt; 65504). For an extreme case like a language model, $N$ could be 50,000 (vocab size). Sum could be 50,000 in worst case (everyone equal probability). 50,000 is still below 65504, so actually an FP16 sum would &#039;&#039;not&#039;&#039; overflow in that scenario, and the rounding error on a sum of 50k identical terms would be maybe a few ULPs (which at 50k magnitude might be ~0.5 difference worst-case). So maybe softmax summation in FP16 is not catastrophic for output probabilities – you’d get essentially 1.0 after normalization instead of 0.99999 in some case. Usually fine.&lt;br /&gt;
&lt;br /&gt;
:The bigger issue is if one or a few terms dominate the softmax. If one term is much larger, the others underflow to 0 after the max subtraction. That’s fine: you get a one-hot distribution essentially. If the model expected a softer distribution, that could be a problem. But if the logits were really that far apart, even FP32 would give nearly one-hot. So not really a precision issue, more of a model property.&lt;br /&gt;
&lt;br /&gt;
:However, the &#039;&#039;&#039;stability during training&#039;&#039;&#039; is a big issue. The GitHub thread we saw (ColossalAI) mentioned training a 130B model where FP16 softmax in attention caused training to occasionally blow up&amp;lt;ref name=&amp;quot;ref14&amp;quot;/&amp;gt;&amp;lt;ref name=&amp;quot;ref14&amp;quot;/&amp;gt;. They observed attention scores in some heads ranging up to 1e4 or as low as 1e-3&amp;lt;ref name=&amp;quot;ref14&amp;quot;/&amp;gt;. 1e4 exponent in FP16 is definitely Inf (because $\exp(10^4)$ is astronomically huge, but even exp(100) is inf in half). The method CogView introduced (Precision Bottleneck Relaxation) involved subtracting the max &#039;&#039;per head&#039;&#039; to reduce range&amp;lt;ref name=&amp;quot;ref14&amp;quot;/&amp;gt;—which is basically the softmax max trick at a finer granularity or something similar. In the end, they found &#039;&#039;&#039;the simplest fix was to compute softmax in FP32&#039;&#039;&#039; for those layers&amp;lt;ref name=&amp;quot;ref14&amp;quot;/&amp;gt;. They reported that it had negligible speed impact but significantly improved stability&amp;lt;ref name=&amp;quot;ref14&amp;quot;/&amp;gt;. This highlights a general principle: certain operations like softmax (and sometimes matrix multiplications in attention) benefit from higher precision, and using FP32 in a few critical spots can save a mixed-precision training from crashing. In inference, using FP32 for softmax is also a safeguard if one is uncertain. It costs little because softmax is not a heavy compute compared to matrix multiplies.&lt;br /&gt;
&lt;br /&gt;
:ONNX does have a Softmax operator; if the input is float16, a backend might do exactly what they described: internally cast to float32 for the softmax computation, then cast output back to float16. Some inference engines (like DeepSpeed’s inference or others) keep attention softmax in FP32 even if rest is FP16, for robustness&amp;lt;ref name=&amp;quot;ref14&amp;quot;/&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Apart from softmax, other places where exponentials occur are in loss functions like cross-entropy (which uses log-softmax internally), or in activation functions like Sigmoid and Tanh (which are basically scaled logistic functions). Sigmoid($x)=1/(1+e^{-x})$. If $x$ is moderately large positive, $e^{-x}$ underflows to 0 in FP16 if $x&amp;gt;~11$. That gives a sigmoid output of 1.0 exactly. In FP32, $e^{-x}$ for $x=11$ is about $5e-5$, not underflow, so sigmoid ~0.99995. So FP16 will saturate some outputs to exactly 0 or 1 sooner than FP32 would. This can slightly change behaviors (e.g., a neuron might go to full saturation a bit early). Usually, this isn’t a huge deal, but it could make gradients zero earlier. In inference, we don’t care about gradients, just the output value. Getting exactly 1.0 vs 0.99995 probably has no practical difference for a classification output. But one should be aware that &#039;&#039;&#039;activation functions can saturate faster&#039;&#039;&#039; in lower precision due to limited range or precision. ReLU has no issue (it’s linear or 0), but exponential-based activations do. In practice, again, frameworks often simply compute those in the given precision and accept the slight difference. If it were a problem, one could do those in FP32 as well.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Summary:&#039;&#039;&#039; Numerical stability in inference is maintained by using robust computation methods: subtracting maxima in softmax/LogSumExp, adding epsilon in denominators for normalization, and using higher precision for summations or critical steps. Most of these are built into the algorithms. ONNX models assume these standard practices (they don&#039;t need to explicitly do them in the graph because the backend inherently will for ops like Softmax). It&#039;s wise when building or exporting models to ONNX to test them in the target precision and ensure there are no surprises (like an output becomes NaN or Inf due to precision). Usually if training was done in mixed precision, the model will have been vetted in that environment already.&lt;br /&gt;
&lt;br /&gt;
== Performance vs. Accuracy Trade-offs ==&lt;br /&gt;
&lt;br /&gt;
There is an inherent trade-off between numerical precision and computational performance (speed, memory usage) in AI systems. Choosing lower precision can dramatically increase throughput and reduce memory, but it may introduce arithmetic errors. Engineers must balance these considerations:&lt;br /&gt;
&lt;br /&gt;
=== 7.1. Throughput, Memory Bandwidth, and Precision ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Throughput (speed)&#039;&#039;&#039; is often higher for lower-precision operations. Modern hardware can execute more low-precision ops in parallel than high-precision ops. For example, an NVIDIA V100 GPU can perform 2× more half-precision FLOPs than single-precision per clock, and newer architectures with Tensor Cores can do 8× more (since a Tensor Core warp operation might perform 64 FP16 FMAs per cycle vs 16 FP32). Similarly, int8 operations can be even faster and more energy efficient. Lower precision numbers also use less memory bandwidth – fetching 16-bit data is twice as fast as 32-bit data from memory (assuming the same bus width), which can relieve memory bottlenecks. Many deep learning workloads are memory-bound, so halving the data size can nearly double throughput if compute is not the limiting factor. This is a huge incentive to use lower precision.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Memory and storage&#039;&#039;&#039;: Using FP16 or int8 weights reduces model size, which is important for deploying large models to memory-limited environments (like edge devices or GPUs with limited VRAM). It also means you can fit larger batch sizes in memory, or more models on a chip, etc. There is also a cascade effect: smaller data means better cache utilization, which further improves performance.&lt;br /&gt;
&lt;br /&gt;
However, &#039;&#039;&#039;accuracy&#039;&#039;&#039; suffers if precision is too low. We have discussed how rounding error and limited range can cause deviations in outputs. The question is: do those deviations matter for the application? Often for neural networks, a small amount of noise or error in intermediate calculations does not change the final prediction. Neural nets are somewhat error-tolerant due to their redundant distributed representations. This is why quantization and FP16 techniques can work at all. The final model accuracy (e.g. top-1 accuracy on ImageNet) might drop only 0.5% or less when using FP16 instead of FP32, or int8 instead of FP16, if done properly. For many use-cases, this loss is acceptable given the speed gains.&lt;br /&gt;
&lt;br /&gt;
If one goes too low (like naive FP8 or int4 without special care), accuracy might drop severely – perhaps making the model unusable. Thus the trade-off: as precision decreases, performance increases but at some point accuracy degrades beyond an acceptable threshold. Engineers aim to find the lowest precision that still preserves accuracy.&lt;br /&gt;
&lt;br /&gt;
There is also the consideration of &#039;&#039;&#039;accumulation strategy&#039;&#039;&#039; affecting performance: for instance, Kahan summation (compensated) is more accurate but ~4× slower than naive summation. If you need that accuracy, you pay a performance cost. In many AI inference scenarios, the slight extra accuracy from something like Kahan isn&#039;t needed, so it’s not used. But in a scenario where you sum millions of elements for a final result that is thresholded, maybe you do need it.&lt;br /&gt;
&lt;br /&gt;
Another dimension is &#039;&#039;&#039;determinism vs speed&#039;&#039;&#039;. Enforcing a deterministic reduction order (for reproducibility) might require disabling some parallelism, which could slow things down. If exact repeatability is critical (like certain scientific or financial applications of neural nets), one might sacrifice some performance to ensure the summation happens the same way every time. ONNX allows both modes depending on the runtime.&lt;br /&gt;
&lt;br /&gt;
From a memory perspective, if precision is higher than needed, you waste memory. For example, storing intermediate activations in FP32 when they could be FP16 uses 2× memory, which might mean lower batch size or more memory traffic. So there&#039;s strong motivation to keep things as low-precision as possible &#039;&#039;except&#039;&#039; where accuracy truly demands.&lt;br /&gt;
&lt;br /&gt;
In summary, the trade-off often manifests as &#039;&#039;diminishing returns&#039;&#039;: FP64 vs FP32 – huge slowdown for usually negligible accuracy gain in ML; FP32 vs FP16 – moderate speedup for usually minor accuracy hit; FP16 vs int8 – another big speedup but maybe more accuracy loss unless calibration is done; int8 vs int4 – speed gain but often big accuracy drop that might not be worth it for current models.&lt;br /&gt;
&lt;br /&gt;
Engineers use techniques like &#039;&#039;&#039;mixed precision&#039;&#039;&#039; to target high precision only where needed. For example, run most of the model in FP16, but keep a few sensitive layers in FP32. This yields most of the performance benefits while ensuring critical parts are accurate. We already saw softmax as one example. Another might be final output layer: sometimes using FP32 for the logits and softmax can be safer.&lt;br /&gt;
&lt;br /&gt;
Hardware is also trending to support mixed modes – like FP8 Tensor Cores accumulate in FP32, etc. So hardware tries to give accuracy cheaply.&lt;br /&gt;
&lt;br /&gt;
=== 7.2. Hardware Accelerators and Tensor Cores ===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Hardware accelerators&#039;&#039;&#039; (GPUs, TPUs, FPGAs, ASICs like GraphCore IPU, etc.) have specific support for certain precisions. Leveraging these can provide massive speedups:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;NVIDIA Tensor Cores:&#039;&#039;&#039; These started in Volta (V100) with support for 4x4 matrix multiply-adds on FP16 inputs with FP32 accumulation&amp;lt;ref name=&amp;quot;ref6&amp;quot;/&amp;gt;. Using Tensor Cores can give up to 8x throughput compared to using regular FP32 cores. But to use them, the data needs to be FP16 (or now BF16 or FP8 on newer GPUs) and aligned to certain dimensions (multiples of 8 etc. in matrix sizes, which frameworks handle by padding if needed). If an ONNX model is all FP32, by default it would not use Tensor Cores unless the runtime automatically converts some ops to mixed precision (which some do via a flag or environment variable enabling TF32 or FP16). The introduction of &#039;&#039;&#039;TensorFloat-32 (TF32)&#039;&#039;&#039; in Ampere GPUs is essentially a compromise: treat FP32 inputs as two 16-bit significand halves (10-bit mantissa actually) so that it can run on Tensor Cores, giving ~8x speed, while still outputting FP32. TF32 has about the precision of FP16 but range of FP32, and it was chosen because it typically doesn’t hurt convnet accuracy. ONNX Runtime or others on Ampere will use TF32 by default for conv and matmul unless disabled, giving a free speed boost at minimal accuracy cost. If exact FP32 is needed, users have to turn it off (sacrificing speed).&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;TPU (Google):&#039;&#039;&#039; TPUs have native support for BF16 matrix ops with FP32 accumulators. They were designed specifically for the mixed precision approach in training. For inference, TPUs also support int8 and other quantized ops (e.g., via Edge TPU for lower-power). If exporting ONNX to run on a TPU (via some bridge), one might get automatic BF16 usage. BF16 has the advantage of no loss in range, so TPUs can more straightforwardly run models in BF16 without modifications.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;FPGAs and ASICs:&#039;&#039;&#039; Some custom accelerators might use fixed-point or block floating-point arithmetic for efficiency. They often treat the network in quantized terms. The ONNX quantization specification (QuantizeLinear/DequantizeLinear ops) exists for representing quantized models (INT8 etc.), which is another domain beyond floating-point, but it overlaps as a precision issue. In those systems, accumulators for INT8 × INT8 multiplies are usually 32-bit (to avoid overflow for dot products up to certain lengths). The trade-off there is between integer vs float representation, which can give big speedups on CPUs/DSAs via SIMD instructions (many CPUs have vector int8 multiply-add instructions now for inference).&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Memory bandwidth on hardware:&#039;&#039;&#039; Some accelerators include special high-bandwidth memory or cache specifically for lower precision. For example, an accelerator might fetch two FP16 values in one 32-bit memory transaction – effectively doubling memory bandwidth for half precision. On some hardware, using FP16 can even reduce memory &#039;&#039;latency&#039;&#039; if multiple values can be packed.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Parallelism and Vectorization:&#039;&#039;&#039; Many hardware units process data in fixed vector widths (e.g., 256-bit registers). That could be 8 floats at once or 16 half-floats at once in the same 256 bits. If your code is in FP16, you might use the full vector width (16 values) whereas FP32 might only use half (8 values). This means FP16 can make better use of vector units. Similarly, matrix units often have fixed tile sizes, and smaller data types allow larger tiles or more tiles concurrently.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Energy efficiency:&#039;&#039;&#039; Lower precision often means less energy per operation, which on large deployments (data centers or battery-powered devices) is a significant advantage. This isn&#039;t directly performance, but it&#039;s an important trade: you might choose a slightly lower precision to save power even if accuracy drops a tiny bit.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Software overhead:&#039;&#039;&#039; Using mixed precision on hardware involves some overhead of casting and moving data between precisions. For instance, if you have to frequently cast FP16 to FP32 and back, that adds instructions and memory copies. A well-designed accelerator (like GPUs with unified memory for FP16/32 or special hardware casting) minimize this. Still, the runtime might insert extra operations (as we saw, ONNX Runtime CPU inserted many Cast nodes converting FP16 to FP32 and back&amp;lt;ref name=&amp;quot;ref9&amp;quot;/&amp;gt;, which actually &#039;&#039;slowed down&#039;&#039; that FP16 model by 4x vs FP32 on a CPU, because the CPU had no native FP16). So ironically, on some hardware, using lower precision in ONNX might degrade performance if not supported (the CPU case). Always ensure the target hardware actually supports the low precision you plan to use; otherwise, you&#039;ll pay overhead for emulation.&lt;br /&gt;
&lt;br /&gt;
To illustrate trade-off: Suppose you have a model that achieves 99% accuracy in FP32 and drops to 98.5% in FP16. If FP16 runs 2x faster, many would accept the 0.5% accuracy drop for double throughput. If int8 runs 4x faster but accuracy drops to 95%, that might be too much drop for some applications, but acceptable for others that value speed. It&#039;s application-dependent.&lt;br /&gt;
&lt;br /&gt;
In critical applications, one might maintain higher precision accumulators or run certain validation steps in high precision to ensure nothing drifted. In research, sometimes a model is trained in mixed precision but validated in double to ensure any small numeric differences don’t influence the reported result (rare, but for scientific computing integration maybe).&lt;br /&gt;
&lt;br /&gt;
In the ONNX world, performance-vs-accuracy is often managed via &#039;&#039;&#039;Execution Providers&#039;&#039;&#039; (EPs) that target specific hardware. For example, the TensorRT EP in ONNX Runtime can take an ONNX model and run it using NVIDIA&#039;s high-performance inference library, which will use FP16 or INT8 if allowed, to maximize speed. The user can configure the EP with precision flags. If the accuracy drop is too high, one might restrict it to FP32. If it&#039;s fine, allow FP16. Tools exist to measure the accuracy difference (like calibrating int8 quantization).&lt;br /&gt;
&lt;br /&gt;
To conclude, the trade-offs revolve around picking the lowest precision that yields acceptable accuracy, and utilizing hardware features fully. ONNX being a high-level spec abstracts this, but it&#039;s ultimately up to the deployment to decide and sometimes up to the exporter to incorporate quantization. ONNX’s role is to be able to &#039;&#039;represent&#039;&#039; these choices (they have added quantized types, FP8 types, etc., to allow the graph to include those when needed).&lt;br /&gt;
&lt;br /&gt;
== Best Practices and Design Guidelines ==&lt;br /&gt;
&lt;br /&gt;
Given the above insights, here are some best practices and guidelines for dealing with numerical precision and accumulators in AI inference (and training) design:&lt;br /&gt;
&lt;br /&gt;
=== 8.1. Choosing an Accumulator Precision ===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Match precision to data length and range:&#039;&#039;&#039; If an operation accumulates a large number of terms, consider using a higher precision accumulator than the input data. For example, for summing &amp;gt;1000 values of type FP16, an FP32 accumulator is recommended to avoid large rounding error or overflow. If summing &amp;gt; million values of FP32, consider double (FP64) accumulation if high accuracy is needed (though this is rare in DL). &#039;&#039;Rule of thumb:&#039;&#039; if $N \cdot \varepsilon$ is not tiny (say &amp;gt;1e-3), you may benefit from higher precision. For FP16 ($\varepsilon \approx 1e-3$), that threshold N is a few hundred. For FP32 ($\varepsilon \approx 1e-7$), N would be in the millions before 1e-3 error.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Use higher precision in critical layers:&#039;&#039;&#039; Identify if certain layers of the model are more sensitive to precision. For instance, the output layer or a layer that feeds into a very sharp decision boundary might require more precision. If a small change there could flip the prediction, you don&#039;t want excessive numerical error. Using FP32 for logits and softmax is a common practice to maintain classification confidence stability.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Follow hardware defaults, but override if needed:&#039;&#039;&#039; Many hardware accelerators have sensible defaults (like accumulate in FP32 for tensor ops). It&#039;s usually best to use those defaults as they were chosen to balance accuracy and speed. Only override if you have evidence of a problem. For example, if using ONNX Runtime on CPU, you discovered it upcasts to float for you – great. But if using some custom accelerator that does pure FP16, you might manually insert casts in the ONNX graph around certain ops to force higher precision on those ops.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Maintain precision for gradient accumulation (training):&#039;&#039;&#039; This is more training-oriented, but worth noting: when summing gradients over batches or updating weights, use higher precision (FP32 or even 64) for those accumulations. This ensures training doesn&#039;t diverge due to numeric error. Many frameworks keep a &amp;quot;master copy&amp;quot; of weights in FP32 when training in FP16&amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;. For inference, this translates to: any running statistics or counters in the model (like if you had a running average inside the model) should probably be in higher precision to avoid drift.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Be mindful of intermediate cast costs:&#039;&#039;&#039; If you do decide to cast to higher precision for an operation in ONNX, remember it incurs memory copies and compute overhead. Make sure it&#039;s worth it. For example, if you cast a huge tensor to double, that could slow things drastically. Usually FP16-&amp;gt;FP32 is fine (hardware handles it well), but FP32-&amp;gt;FP64 might be expensive on GPUs.&lt;br /&gt;
&lt;br /&gt;
=== 8.2. Safe Reduction Lengths and Heuristics ===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Chunk long reductions:&#039;&#039;&#039; If you must do a very long sum in low precision (due to memory constraints or hardware), chunk the sum into parts, sum each to a higher precision local accumulator, then sum the partial sums. This is essentially pairwise summation. Many libraries use a fixed chunk size heuristic (e.g., sum in blocks of 256 in registers, then add those up). This bounds the error growth. If implementing manually, you can choose a chunk size such that chunk_length &#039;&#039; ε is small (like 256 &#039;&#039; 1e-3 = 0.256 for FP16, hmm maybe choose 100).&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Sort by magnitude if possible:&#039;&#039;&#039; In some cases (not typical in DL, but possible in post-processing), if you have numbers varying by orders of magnitude, summing from smallest to largest improves accuracy. It prevents small ones from being swallowed by big ones. This is more relevant if you output a sum of mixed-scale values (like some algorithm mixing big and small contributions). It’s not always feasible in real-time inference to sort, but keep in mind.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Use compensation for critical sums:&#039;&#039;&#039; If you find a particular sum or difference in your inference computation is crucial (like computing a small residual from two close numbers), consider using Kahan summation or another compensated scheme just for that part. For example, if you had an ONNX custom function that needs to sum a lot of almost cancelling terms, you might implement it with compensation internally.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Heuristics for FP8/low-bit:&#039;&#039;&#039; When using extremely low precision like FP8, adopt the recommended practices: use scaling (don&#039;t feed raw data into FP8, normalize it first), and perhaps only use FP8 on portions of the network that are empirically robust. The industry is still establishing heuristics, but one might be: use FP8 for matrix multiplications, but accumulate in FP16 or FP32; keep normalization in FP16; use FP16 for first and last layer maybe. This is evolving, so keep an eye on literature.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Keep an eye on sum length in softmax and normalization:&#039;&#039;&#039; If you have extremely large class counts or group sizes (e.g., a softmax over 1e6 items, or layer norm over tens of thousands of features), realize this is a giant reduction. You may want to ensure that reduction is done in higher precision. ONNX Softmax by default will likely not handle a million-class case in FP16 well (the sum would overflow since 1e6 &amp;gt; 65504). So either do that on CPU in FP32 or break it down (could do hierarchical softmax, etc.). This is a corner case, but relevant for huge vocab language models. In practice, vocab ~50k is fine as we said.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Test numeric differences:&#039;&#039;&#039; A practical tip: after quantizing or reducing precision, run some test inputs through both the high-precision model and low-precision model. Compare outputs (and maybe some intermediate signals if possible). If differences are consistently small relative to the application tolerance, you&#039;re good. If you see occasional large differences or instabilities (like outputs become NaN or Inf in low precision), that&#039;s a red flag. Use that testing to guide where you might need to increase precision or change algorithms.&lt;br /&gt;
&lt;br /&gt;
=== 8.3. When Higher Precision Is Required ===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Ill-conditioned calculations:&#039;&#039;&#039; If your model or pipeline performs any operation known to be ill-conditioned (matrix inversion, subtraction of nearly equal quantities, computing a tiny difference of large numbers), you likely need higher precision for that part. For example, if your ONNX model includes a custom op to solve a linear system, use double precision in that op if possible (or at least float with iterative refinement). In ML, this might occur in something like a physics-informed neural network or a differentiable simulation within the model.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Strict accuracy requirements:&#039;&#039;&#039; Some applications (medical, aerospace, finance) might have strict tolerances. If an inference model’s output needs to be correct to 5 decimal places, you probably cannot use FP16, which has only 3-4 decimal digits precision. FP32 might be barely enough (~7 digits). FP64 might be warranted to guarantee that level of accuracy in worst case. Most deep learning applications (like image classification) don&#039;t demand that level of numeric precision in outputs – as long as the top class is correct, it doesn&#039;t matter if its probability was 0.901 vs 0.899. But if you&#039;re doing something like summing probabilities to make a risk estimate or doing a cumulative sum over many predictions, be careful to accumulate precisely.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Large sums or products in post-processing:&#039;&#039;&#039; Sometimes the neural net is fine, but the &#039;&#039;post-processing&#039;&#039; of results might need precision. E.g., summing up probabilities of many events, or computing confidence intervals, etc. Do those calculations in double if needed outside the model.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Preventing drift in long sequences:&#039;&#039;&#039; For sequential models (RNNs processing thousands of steps), rounding error can accumulate over steps. If each step has a small error, after 10000 steps it could add up. In such cases, using a higher precision for the state might reduce drift. Some practitioners use FP64 for the hidden state accumulation in very long sequence processing to maintain numerical stability (though this is not common due to speed loss). Alternatively, periodically renormalize or reset small errors.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Simulations or differential equations:&#039;&#039;&#039; If your ONNX model involves simulating something (like a differential equation solver inside a model, or an ODE/RNN hybrid), those often need double precision to be stable for long durations. Recognize those situations and allocate precision accordingly.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Verification and debugging:&#039;&#039;&#039; Use higher precision as a tool to verify correctness. For example, run your model in FP64 (if possible) and compare to FP32 to see if there are differences. If not, FP32 is fine. If yes, you might find a particular calculation losing too much info in FP32, implying maybe you should keep it in FP64 in production as well. This is rare in inference but can be part of rigorous validation.&lt;br /&gt;
&lt;br /&gt;
In ONNX, using higher precision may involve casting to &amp;lt;code&amp;gt;tensor(double)&amp;lt;/code&amp;gt; for certain ops. Not all runtimes support double on GPU though (some do via emulation or just slower path). ONNX CPU does support double for general ops. So be mindful of the target.&lt;br /&gt;
&lt;br /&gt;
Finally, &#039;&#039;&#039;document your precision choices&#039;&#039;&#039;. If you deliver an ONNX model that uses mixed types (float16 inputs, float32 accumulations, int8 weights, etc.), ensure that is clear to whoever maintains or uses it. Precision issues can be sneaky, so clarity helps.&lt;br /&gt;
&lt;br /&gt;
== Limitations and Open Problems ==&lt;br /&gt;
&lt;br /&gt;
Despite advances in handling numerical precision, there remain limitations and open challenges in this domain:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Lack of Precision Specification in ONNX:&#039;&#039;&#039; As noted, ONNX currently has no way to formally specify that an op &#039;&#039;should&#039;&#039; use a higher precision internally. This can lead to nondeterministic differences across platforms&amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;. While proposals exist to add attributes for this&amp;lt;ref name=&amp;quot;ref12&amp;quot;/&amp;gt;, it is not yet standard. Thus an ONNX model might behave slightly differently on two compliant backends, and there&#039;s no built-in mechanism to ensure identical results. This is a limitation for applications requiring cross-platform consistency. Solving it may involve extending ONNX (e.g., an attribute like &amp;lt;code&amp;gt;accumulatePrecision=&amp;quot;FLOAT32&amp;quot;&amp;lt;/code&amp;gt; for MatMul) or tightening the spec, but those come with backward compatibility and implementation burden concerns.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Reproducibility and Determinism:&#039;&#039;&#039; Even with the same precision, parallel computation can introduce nondeterminism (due to different summation orders). Ensuring bit-identical results across runs (important for some sensitive domains) is non-trivial. Frameworks often provide &amp;quot;deterministic mode&amp;quot; which might use fixed ordering at cost of speed. In ONNX, there&#039;s no global flag for this; it&#039;s up to the runtime. More research could be done on algorithms that produce bit-wise identical results independent of parallel execution order (perhaps using techniques like pairwise summation or consistent partitioning). Stochastic rounding is another area: sometimes using probabilistic rounding can reduce bias in accumulated error, but it introduces randomness – reconciling that with reproducibility is tricky.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Ultra-low Precision (FP8 and below):&#039;&#039;&#039; While FP8 is being explored, going to 4-bit floats or mixed schemes is an open challenge. The OpenAI community is investigating 4-bit (quantization-wise) for LLMs with some success, but a true 4-bit float (like E3M1) would be extremely limited. Perhaps hybrid schemes (like block floating point, log scales, etc.) will come. ONNX already has some support (they mention 4-bit in documentation placeholders&amp;lt;ref name=&amp;quot;ref10&amp;quot;/&amp;gt;), but it&#039;s nascent. Research into training techniques, calibration, and new number formats (like &#039;&#039;&#039;posit numbers&#039;&#039;&#039; or tapering precision floats) is ongoing. If a new format proves useful, ONNX will need to adapt to represent it.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Quantization Noise and Model Robustness:&#039;&#039;&#039; Reducing precision can sometimes expose models to issues like adversarial vulnerability or fragility – e.g., a slight perturbation might flip a decision if the margin was small and precision loss nudged it. Ensuring model robustness under quantization is an open problem. It might require re-training the model with quantization in the loop (quantization-aware training) or designing models that have large &amp;quot;margin&amp;quot; between classes to tolerate quantization noise.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Dynamic range during training vs inference:&#039;&#039;&#039; In training, activations can have wider distributions (especially early in training). Models often learn to constrain their ranges (via activation functions saturating or normalization), which makes inference more stable in lower precision. But if one tries to train in FP8 from scratch, it&#039;s very hard. This gap between what&#039;s possible in a post-trained inference model vs training is an open area – how to train directly in low precision without significant accuracy loss. Solving that could simplify workflows (no need for high-precision training then quantize; just train low-prec end to end). Techniques like loss scaling, gradient clipping, etc., are partial solutions. The development of optimizers that are resilient to low precision is ongoing.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Hardware variability:&#039;&#039;&#039; As new hardware emerges (with new numeric capabilities or quirks), ensuring that ONNX models run consistently is an ongoing task. For instance, one device might implement a fused op that reduces rounding error, another does it separately. Even something like convolution might use a different algorithm (FFT-based vs direct) which has different numeric properties. Open problem: could ONNX allow specifying numeric tolerances per op or preferred algorithms? That may be overkill for most, but for some HPC ML use cases, maybe desired.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Testing and validation of numerics:&#039;&#039;&#039; There isn&#039;t a standardized set of &amp;quot;numeric compliance&amp;quot; tests for ONNX runtimes beyond simple correctness. Perhaps in the future, test suites might include challenging numeric scenarios (large sums, nearly singular matrices) to ensure a runtime handles them gracefully (maybe by switching precision). Right now, it&#039;s mostly on the user to test their model under expected conditions.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Combining quantization with floating point:&#039;&#039;&#039; Many production models use a mix (e.g., int8 for some layers, FP16 for others). ONNX can represent this (with QuantizeLinear ops or MixedPrecision through casting), but tooling to automatically find the optimal mix is still developing. It&#039;s somewhat trial-and-error or using heuristics. An open question is how to automate precision assignment: given a model and a target hardware, how to automatically choose which layers can be int8, which should be FP16, etc., to meet an accuracy target. This crosses into NAS (neural architecture search) territory or automated quantization. &lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Numerical debugging tools:&#039;&#039;&#039; When a model goes wrong due to precision, it&#039;s often hard to pinpoint where. Better tooling to analyze numerical error layer by layer could be useful. For instance, one could run a model in double and float and diff the intermediate results to see where the largest divergence happens. Doing this systematically for big nets is an open tooling challenge. Some frameworks have hooks for this, but not standardized.&lt;br /&gt;
&lt;br /&gt;
In conclusion, while we&#039;ve come far with using lower precision in AI without sacrificing much accuracy, the landscape is always evolving. The key open problem is how to push precision lower (for efficiency gains) without crossing the threshold where accuracy falls off a cliff. Each new generation of hardware and techniques (like FP8, novel quantization schemes) extends that boundary, but also brings new questions on how to manage the numerical errors. ONNX as an interchange will need to keep pace, possibly by adding more expressive power for these numeric considerations.&lt;br /&gt;
&lt;br /&gt;
== See Also ==&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;IEEE 754 Floating-Point&#039;&#039;&#039; – The standard defining binary16, binary32, binary64 formats and rounding behavior.&lt;br /&gt;
* &#039;&#039;&#039;Floating-Point Arithmetic&#039;&#039;&#039; – General topic on how floating-point math works, common issues (overflow, underflow, cancellation).&lt;br /&gt;
* &#039;&#039;&#039;Kahan Summation Algorithm&#039;&#039;&#039; – Algorithm for compensated summation to reduce numerical error&amp;lt;ref name=&amp;quot;ref4&amp;quot;/&amp;gt;.&lt;br /&gt;
* &#039;&#039;&#039;BFloat16&#039;&#039;&#039; – A short-format float with 8-bit exponent, used in TPUs and some GPUs for training networks.&lt;br /&gt;
* &#039;&#039;&#039;Mixed Precision Training&#039;&#039;&#039; – Technique of using FP16 (or BF16) along with FP32 accumulators to accelerate training&amp;lt;ref name=&amp;quot;ref2&amp;quot;/&amp;gt;.&lt;br /&gt;
* &#039;&#039;&#039;Quantization in Deep Learning&#039;&#039;&#039; – Converting networks to lower bit integer arithmetic for inference (INT8 quantization and calibration techniques).&lt;br /&gt;
* &#039;&#039;&#039;ONNX Operator Specification&#039;&#039;&#039; – Official docs for ONNX ops, which describe their mathematical function but not the required numeric precision (a point of potential extension in future).&lt;br /&gt;
* &#039;&#039;&#039;CUDA Tensor Cores &amp;amp; TF32&#039;&#039;&#039; – NVIDIA documentation on how tensor cores perform mixed precision and the TF32 format for accelerating FP32.&lt;br /&gt;
* &#039;&#039;&#039;Pairwise Summation&#039;&#039;&#039; – Summation algorithm with logN error growth&amp;lt;ref name=&amp;quot;ref5&amp;quot;/&amp;gt;, used in many numerical libraries to improve accuracy of reductions.&lt;br /&gt;
* &#039;&#039;&#039;Softmax Stability&#039;&#039;&#039; – Articles on numerically stable softmax and log-sum-exp implementations to avoid overflow.&lt;br /&gt;
* &#039;&#039;&#039;Performance Tuning for Low Precision&#039;&#039;&#039; – Guides (e.g., NVIDIA or Intel whitepapers) on using low precision and the trade-offs involved in various hardware.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;references&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref1&amp;quot;&amp;gt;Accelerate and simplify Scikit-learn model inference with ONNX Runtime - Microsoft Open Source Blog — opensource.microsoft.com — https://opensource.microsoft.com/blog/2020/12/17/accelerate-simplify-scikit-learn-model-inference-onnx-runtime&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref2&amp;quot;&amp;gt;Floating Point Precision: Understanding FP64, FP32, and FP16 in Large Language Models - DEV Community — dev.to — https://dev.to/lukehinds/floating-point-precision-understanding-fp64-fp32-and-fp16-in-large-language-models-3gk6&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref3&amp;quot;&amp;gt;Floating Point Precision and Its Limitations | by Umair Akbar | Medium — akbu.medium.com — https://akbu.medium.com/floating-point-precision-and-its-limitations-cfb7247d7789&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref4&amp;quot;&amp;gt;Kahan summation algorithm - Wikipedia — en.wikipedia.org — https://en.wikipedia.org/wiki/Kahan_summation_algorithm&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref5&amp;quot;&amp;gt;Pairwise summation - Wikipedia — en.wikipedia.org — https://en.wikipedia.org/wiki/Pairwise_summation&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref6&amp;quot;&amp;gt;Numerical behavior of NVIDIA tensor cores - PMC — pmc.ncbi.nlm.nih.gov — https://pmc.ncbi.nlm.nih.gov/articles/PMC7959640/&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref7&amp;quot;&amp;gt;Mixed precision and performance-accuracy tuning (neuron-cc) — AWS Neuron Documentation — awsdocs-neuron.readthedocs-hosted.com — https://awsdocs-neuron.readthedocs-hosted.com/en/latest/about-neuron/appnotes/neuron-cc/mixed-precision.html&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref8&amp;quot;&amp;gt;Create Float16 and Mixed Precision Models - ONNX Runtime — onnxruntime.ai — https://onnxruntime.ai/docs/performance/model-optimizations/float16.html&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref9&amp;quot;&amp;gt;[Performance] ONNX FP16 model is having performance bottle neck when compared to FP32 variant · Issue #25824 · microsoft/onnxruntime · GitHub — github.com — https://github.com/microsoft/onnxruntime/issues/25824&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref10&amp;quot;&amp;gt;Float stored in 8 bits - ONNX 1.21.0 documentation — onnx.ai — https://onnx.ai/onnx/technical/float8.html&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref11&amp;quot;&amp;gt;[2209.05433] FP8 Formats for Deep Learning — arxiv.org — https://arxiv.org/abs/2209.05433&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref12&amp;quot;&amp;gt;Should MatMul op allow specification of accumulation precision? · Issue #7072 · onnx/onnx · GitHub — github.com — https://github.com/onnx/onnx/issues/7072&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref13&amp;quot;&amp;gt;Quantize ONNX models | onnxruntime — onnxruntime.ai — https://onnxruntime.ai/docs/performance/model-optimizations/quantization.html&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;ref name=&amp;quot;ref14&amp;quot;&amp;gt;Keep Attention Softmax FP32 during FP16/ZeRO Training · Issue #1485 · hpcaitech/ColossalAI · GitHub — github.com — https://github.com/hpcaitech/ColossalAI/issues/1485&amp;lt;/ref&amp;gt;&lt;br /&gt;
&amp;lt;/references&amp;gt;&lt;/div&gt;</summary>
		<author><name>Timo.stripf</name></author>
	</entry>
</feed>