Research Software Engineering at Warwick
https://blogs.warwick.ac.uk/researchsoftware
We are Research Software Engineering at the University of Warwick. Doing our daily job brings us up against odd stuff to do with computers and programming. We document them here so that we can say that we leave them a bit better than we found them.en-GB(C) 2024 Christopher Bradyhttps://blogs.law.harvard.edu/tech/rssChristopher BradyChristopher BradyWarwick Blogs, University of Warwick, https://blogs.warwick.ac.uk120Now with Less NaN! by Heather Ratcliffe
https://blogs.warwick.ac.uk/researchsoftware/entry/now_with_less/
<p>This week I'm at <a href="https://sc18.supercomputing.org/program/proceedings/">Supercomputing 2018</a> in Dallas, enjoying a wide variety of talks! Just a short post on a couple of the "Aha" moments from <a href="https://sc18.supercomputing.org/proceedings/workshops/workshop_pages/pec339.html">this talk</a>on correctness and reproducibility of Floating point code by <span class="var-all_people_presenting_names_affs">James Demmel (University of California, Berkeley). I had never noticed the perils of NaN and Inf propagation he mentioned and I think they're really worth knowing. I said a bit about <a href="https://en.wikipedia.org/wiki/IEEE_754" style="font-family: Nunito, sans-serif;">IEEE 754</a>which dictates what NaN and Inf are and how they behave in <a href="./snippet_of_the_/" style="font-family: Nunito, sans-serif;">this post</a>. This entry is mostly about unexpected ways the standard can be broken by accident, mainly due to optimisations that rely on multiplying by zero. Per the standard, anything <strong>except NaN, </strong>times by zero is zero, but <strong>any operation involving a NaN</strong>must give NaN. </span></p>
<h2>Max Value the Obvious Way</h2>
<p>Imagine you're asked to write a routine to find the Max or Min value of an array. It's obvious. Start by assuming it's the first value, and then compare every other value to see if it's higher/lower, until you reach the end. Max/Min MUST look at every value in the array to get their answer (I am sure there's a mathematical word for that, but I don't know it), so this is the most efficient option there is in terms of comparisons. However, it has a major flaw once we account for IEEE behaviour: NaN compares as false to <em>everything</em>, even itself. </p>
<p>So, imagine running this algorithm on the arrays [NaN, 2, 1] and [2, NaN, 1]. We get NaN and 2 respectively, as we've made the first element "sticky"! Step-by-step:</p>
<pre>Max of [NaN, 2, 1] :
Take first element : NaN
Compare to second element: 2 > NaN -> False : Max = NaN
Compare to third element: 1 > NaN -> False : Max = NaN</pre>
<pre>RESULT: NaN</pre>
<pre>Max of [2, NaN, 1] :
Take first element: 2
Compare to second element: NaN > 2 -> False: Max = 2
Compare to third element: 1 > 2 -> False: Max = 2
</pre>
<pre>RESULT: 2</pre>
<p>This was an example given in the talk, and it's obvious when you look at it, but easy to not think of! There's two serious problems here: firstly the loss of the NaN value in the second case, which violates the standard and secondly the change of answer on array permutation, which is just generally bad for something like Max which should be invariant. </p>
<h2>3x3 Determinant</h2>
<p>Now imagine taking the determinant of a 3x3 Matrix. Generally, this is easy. You take each element of the first row, and multiply by the determinant of the 2x2 sub-matrix omitting the column at hand, with appropriate signs. Suppose a first row element is zero: obviously that term contributes 0 regardless of the 2x2 submatrix! So you can speed things up by testing for zero and omitting any such terms, right? Not once NaN gets involved. Now consider this matrix:</p>
<pre>1 0 0
NaN 1 1
0 0 1 </pre>
<p>We <strong>should</strong> get: 1 * 1 + 0 * NaN + 0 * NaN = NaN. But with our clever optimisation, we get 1 * 1 + 0 + 0 = 1. This is a pretty big problem! We're hiding away that NaN, by accident!</p>
<p>There's another problem too! Even if we checked for NaN in our array, what if we have an Inf instead? Inf * 0 should give us NaN. Effectively, Inf is "a number, but bigger than we can represent". Zero is "anything smaller than the smallest number we can represent". The product is then kind-of "any number, we don't know which". It might be zero, it might be Inf, it might be anything. So, the standard dictates it be NaN. </p>
<p>So, if we use the optimisation, we're not just hiding a NaN, we're stopping one ever being created! This is potentially an much more serious problem, because we might be happy using the Compiler flag to give us errors on NaN, but not willing to make under or overflows fatal. Then we'd completely miss this problem!</p>
<h2>Optimisations in BLAS</h2>
<p>The talk (at least it's first half) focused on the Linear Algebra library BLAS, which is heavily optimised, but as it turns out does have several bugs like this. For instance, a slightly complicated multiply op, alpha * A * B, might check for alpha = 0 in order to save the work of the matrix multiply when the end result should be zero anyway. Unless, of course, either A or B contains any NaN's when we must get back NaN. </p>
<h2>But it's all gone wrong anyway...</h2>
<p>One comment on the talk, which is worth a mention was, to paraphrase "If you're getting NaN's things have already gone wrong, so why should the Linear Algebra library bother to check? You should be checking." This is an interesting point and in some circumstances holds true. I tend to think of NaN or Inf as bugs and would prefer to write code so they can't arise, but I am not working with long linear algebra workflows where you want to run a series of operations in something like BLAS and don't examine the intermediate steps. </p>
<h2>Reproducible BLAS</h2>
<p>The problem above was largely the topic of the first half of the talk, which focused on how BLAS could manage the problem via errors/exceptions, and being careful with optimisations. For instance, use the optimisations to do the calculations, but also return an error code detailling any issues with the arguments. </p>
<p>Another large part of this talk was about reproducibility, in particular in parallel codes. Here, depending on the processor decomposition, you can get subtly different results for a sum over numbers of varying size. This is a really tricky problem, which is being addressed by things like <a href="https://sinews.siam.org/Details-Page/reproducible-blas-make-addition-associative-again">ReproBLAS</a>. </p>
<h2>Punchline</h2>
<p>Dealing with Floating point numbers is hard. It gets harder once you demand reproducibility at a bitwise level. Both the Maxval issue above, and a simple sum with values of varying size may not give the same answers when terms/entries are reordered. In particular, parallel codes always effectively reorder terms whenever you chance the decomposition (e.g. run on more or less cores). </p>
<p>Hopefully, you never need to really worry about NaN (or the less tricky, but still tricky Inf), but do be careful! The problems I've discussed are mainly due to treating zero incorrectly per the IEEE standard, the problems with NaN or Inf following from this. <strong>Be careful with zero!</strong> It might not be as small as you think ;)</p>BugsIeeeSupercomputingWed, 14 Nov 2018 18:47:23 GMTHeather Ratcliffehttps://blogs.warwick.ac.uk/researchsoftware/entry/now_with_less/#comments8a1785826541eaee0167129afba804550