There Are Only Four Billion Floats–So Test Them All (2014)
randomascii.wordpress.com
randomascii.wordpress.com
I did a project in college for a VLSI minor where we implemented a 16bit multiplier - after we got the chips back we were supposed to do scan testing, but I just wrote up a little program in an fpga, wired up my chip to test, and swept all the possible inputs verifying the output. I think I could only run this at 10MHz, and you had to shift in the operands one bit at a time, but it still only took on the order of an hour or two to complete.
I also verified this setup by capturing some cycles on a logic analyzer - then pulled together a little awk script to turn a dump of that into the actual numbers I could check. Fun project.
scan test: it involves scanning test patterns into internal circuits within the device under test (DUT). The design’s flip-flops are modified to allow them to function as stimulus and observation points, or “scan cells” during test, while performing their intended functional role during normal operation.
Fortunately, I had some device vendor sample code in C (of varying quality and provenance), which I believed was probably correct, but I wasn't 100% certain which (if any) of numerous CRC standard variants it was actually implementing (so I couldn't just use an off-the-shelf Rust library).
So, the fastest/best way to get a perfectly compatible Rust implementation seemed to be:
1. Write idiomatic Rust code to try to mimic the behavior of the C code.
2. Write some C code to generate a text file of exhaustive results from the known-good C implementation, for all possible inputs up to length N.
3. Exhaustively check the Rust implementation against that text file of inputs and outputs.
[0] https://reveng.sourceforge.io/
https://users.ece.cmu.edu/~koopman/crc/notes.html
I don't know what algorithm they're using, but the idea that you can make a hamming distance calculation over all possible input sets these days is pretty spectacular.
Test oracles are great for porting software.
https://github.com/ClickHouse/ClickHouse/blob/master/src/IO/...
- we create a table, insert all four billion floats there, and check our functions with SQL queries.
Currently, the state-of-the-art library is https://github.com/lemire/fast_double_parser, but we use a slightly worse (but more performant) routine.
It is also interesting how to format floats in string in an efficient and user-friendly way.
For this problem, we switched iteratively: strtod -> Google's Double Conversion -> Ryu (patched) -> Dragonbox. Currently, we use Dragonbox.
If it (dragonbox?) is more performant, why is it also worse?
The truth of todays computers is that the 32-bit space is a sub-second problem, possibly faster than human reaction speeds.
Test the whole 32 bit space. It's cheap today.
-------
Extrapolating a bit: the 2^48 space is 27 seconds per op. Maybe 100 ops are needed for your test code, so the 48-bit space (aka: 3x 16-bit input space) can be exhausted and tested in roughly 50 minutes.
The real issue is that the verification code is much, much, slower.
Per GPU. A consumer GPU at that (A100 is faster).
4x GPUs (the AMD 6900 XT) per node and maybe 10x nodes in a rack (4U per node, 42U cabinet) can do 64-bit sweep in half a day.
> The real issue is that the verification code is much, much, slower.
Isn't verification of 64b multipliers actually a 128-bit problem? (x * y == z, so you have 2x 64-bit numbers to work with).
Also, I think they use BDDs, which although multiplication is exponential, its still small enough exponent that 128-bit BDDs fit on today's computers. So its not a brute force thing like discussed in this topic, but actually a very elegant data-structure (though with tons of complications, as is the nature of NP-complete and expspace problems).
1. Superscalar (multiple instructions in one clock tick)
2. SIMD (not as good as GPU, but still substantial)
Ryzen 9 7950X (Zen4 cores, 16-cores) has AVX512. Its only 256-bit wide, but there are 4 pipelines (!!). (Actually, 8 pipelines, but only 4 of them are SIMD. Its complicated, lets just assume 4)
So each pipeline can handle 8x32 bit integers per clock, and up to 4 pipelines can be active at once. I do believe Zen4 allows for 4-pipelines to all be performing the classic "MAD / Multiply-and-accumulate" instruction (very important for padding those matrix multiplication TFLOPS numbers), though how much you want to count this is... rather difficult because the 4x pipelines don't all have the same capabilities.
-----------
I can't say I've tested this out myself. But the Ryzen 9 7950X has a theoretical throughput of 16 cores x 8 ops per pipeline x 4 pipelines == 512 x 32-bit ops per clock tick, and is therefore 4x faster than your "128-core" estimate.
Running this code in practice is difficult... but similar to GPU programming. Intel ispc is a good tool for generating optimized AVX512 in the "GPU programming style". I know that OpenMP has a SIMD-statement, and the autovectorizers on GCC/CLang/LLVM are getting better.
A lot of options, all difficult but "valid"... at least valid for a simple brute-force embarrassingly parallel problem as discussed here. Things get rather complicated rather quickly in this space when we talk about practical applications...
------------
Note: to achieve this in practice requires some very careful coding and magic. You'll need to fit inside of the micro-op cache, you'll need to keep the pipeline perfectly open, etc. etc.
I spotted a bug in a code review today, which was formatting dates with ordinal numbers, like "9th Feb". The ordinal logic only has to work for inputs 1 to 31, so I wrote an exhaustive test, which it failed by outputting "11st", "12nd" and "13rd" (due to only looking at the last digit)
https://news.ycombinator.com/item?id=24745563 175 comments
https://news.ycombinator.com/item?id=15252426 50 comments
https://news.ycombinator.com/item?id=7135261 118 comments
At the time I was thinking, their test group could have tested every single mantissa over several operations exhaustively. Why didn't they? What excuse could they have had? It was a computer; they feel no pain when asked to do laborious repetitive tasks.
A+B+C does not equal B+C+A in floating point.
Reading the aside
In fact IEEE floating-point math is designed to, whenever practical, give the best possible answer (correctly rounded)
Made me realize - there are not "just" four billion floats, there are four billion floats multiplied by however many levels of rounding you want to test against.
The "ninety seconds" doesn't qualify if the test harness was multithreaded. If it wasn't, a modern >30 core box (maybe buried in CI somewhere) should be able to manage quite a few rounding levels without difficulty thankfully.
But this is even more appropriate for 16-bit floats - you can realistically do all the adds/multiplies/etc, but maybe not the 3op multiply-accumulates
If you're cycling through all possible numbers and seeing where the output might blow up or go pathological, you probably instead (to be efficient about it) test the far limits of the input number range, and then sample the internal space with an adaptive spacing to see where things are changing and whether it's about to go crazy somewhere. Like divide by zero, subtract 2 small numbers etc.? Or analyze the function to see where subtraction (or whatever operation is applicable here) might misbehave? Rather than spend all your compute equally sampling every possible number.
I'm no expert but it seems to be the same type of problem.
The takeaway might be more: spend time to optimise testing everything, rather than spending time optimising the selection of the subset to test.
Massive caveat of course is that some search spaces are just big, but 32bit space shouldn’t be considered big any more.
You can use random testing for exhaustive testing. On an input space of size N, you will cover that space completely in O(NlogN) tests on average, and you will include any single input in O(NlogN) trials with high probability.
Further, writing clever shotgun tests may actually take you longer than writing the simple loop. In which case it's wasting developer time too.
Be very sure you need to do anything but exhaustive testing, before expending the time money and effort on it. That's my advice.
When you get into double-precision floats, well, there's a lot more.
Is that not correct? Isn't it expected that very small numbers round to zero?
If you have a very small but non-zero positive number, it gets rounded to 0.5 after the addition, and then the second rounding incorrectly rounds toward 0 instead of 1.
The result is that you have a value x such that x > ceil(x), which shouldn't ever happen.
Oh duh. I somehow forgot that over the span of a few sentences. Thanks.
But how often is anyone doing that? If you design microprocessors, or programming languages (maybe), or numerical libraries. But hopefully if you do any of those, you already know to test every value against a reference algorithm.
Is there anything in this post that is more generally applicable to the other 99.99% of developers? I'm trying to think of anything I've ever written where it would even be possible to test billions of input values, but there'd never be a reference to test it against.
(And if you know what you are doing, you can also compare floats exactly in some circumstances.)
But the article already mentions that.
In the process my coworker and found something we believed to be a bug with complex nans in some recent GCC versions, but where not smart enough to understand whether it is actually a bug or undefined behaviour in the standart.
Anyway, we where so exhausted from the refactor, we didn't follow it up again.
I knew what I was doing and that is why using Epsilon was the most reliable thing in my data processing algos.
Obviously, you need to account for tolerances, but that accounting does not need to be via an epsilon in your count, it can also take the form of careful numerical analysis. For example, in some numerical algorithms, it is right and proper to run them until an equality test (without epsilon) succeeds. And using any positive epsilon would give a worse result.