There are only four billion floats, so test them all (2014)
randomascii.wordpress.com
randomascii.wordpress.com
So I just wrote code to mint nonsense URIs for our system based on a small integer seed (like https://foo.example/123456789), then span up a system with a bunch of RAM (maybe 8-12GB) to iterate through each seed, storing the hash and seed it was generated from, until it hit a collision after a few gigabytes of RAM, then it spat out the two URIs from the colliding seeds. Bingo, now we had data for unit testing to show that stuff works even in the unlikely case that two distinct URIs have the same hash.
Probably a day's work to build that, and then I'd guess several hours every month saved debugging weird collision bugs because our unit tests would now tell us if we'd screwed up and collisions were slipping past.
Take over a project with thousands of integration tests.
I trust none of them. Why? Because anything hard to test has been mocked to oblivion.
I have api endpoints that have massive complicated integration tests. I eventually just got source for every project in company and checked to see what they actually did with API.
If the website changes its URI hash algorithm, the "honest" test that just supplies two URIs that happened to collide under the earlier algorithm will suddenly become worthless. The mocked test will continue doing exactly what you always wanted it to do -- exercise the "what if the hashes collide?" case in your code.
(I can sympathize with both approaches and don’t think either one is better.)
Tests take an hour to run, are extremely fragile and flakey. Getting your local dev machine set up to run even a subset of the tests is a half day ordeal, and it can change periodically.
If you have external dependencies that don't version their APIs well it may be inevitable that you need to run tests against them. But for developer productivity you should have a fast/non-flakey path to run local unit tests with no network dependencies. It's also great for working on a plane.
Agree. Not an integration test. If it has mocks in it, it is not testing the INTEGRATION of/between components.
Mocks are for Unit tests. Where you want to isolate testing one single UNIT from all the other crap it integrates with (which have their own unit tests) After all these unit tests run, you test the whole shebang together with integration tests.
So since I could generate collisions for the real hash, that's what I did. If it had been infeasible, I would of course have found a different way to test.
This particular software was written mostly in C (today I would not write it in C) which makes it even more likely that introducing never-used-in-production behaviour unexpectedly perturbs the system. This same system for example ran into an obscure (and since fixed) libc allocator bug, and a defect in the Linux kernel, because we were really kicking the shit out of the memory mapped file I/O with what we were doing.
> which makes it even more likely that introducing never-used-in-production behaviour unexpectedly perturbs the system.
Isn’t that a good thing? Isn’t the goal to deal with the unexpected in the first case?
Defense in depth is regarding layering different types of security, not targeted testing.
Personally, I see tests as not just a way to prevent bugs, but as a way to learn about the system being tested—both how it behaves now and, continually, how it behaves in future iterations. In this specific case, understanding what happens if two URIs hash to the same value even if they don't "really" do that is an important bit of information: it tells us not only what would happen with a real hash collision but also how our system would behave if there were a bug either in the hashing algorithm itself or in the code that connects the hashing algorithm to the component that uses the hashes.
What happens if we introduce a caching layer between where the hash is generated and where it's used, and there's a cache invalidation bug? I don't know how realistic that is in this specific hypothetical, but I'm sure there are other performance optimizations with the same bug potential that I'm not even thinking of. Changes like that can lead to absolute debugging nightmares! Testing even "impossible" edge cases helps catch this in a systematic way, without needing full knowledge of which conditions might matter.
Of course, none of this talks to the trade-off you pointed out: test code does have a real cost. That is absolutely something to consider. I just don't think the answer is to draw a hard line at "don't test situations that can't come up in production"—and, perhaps naively, I think the "real" solution is less about deciding what is and isn't worth testing and more about reducing the costs of testing more. Write application code in a way that's easy to test and treat your test code as code—keep it clean, include comments, and invest time in setup and tooling to make your tests as easy to write and maintain as possible.
That’s understating it. It’s incompetence because it’s an excuse to not test a huge surface area of edge cases.
I've heard this before, but never actually had that problem with fakes in practise. Where can I read more about it?
Of course, sometimes getting verisimilar test cases is difficult enough that you judge it not worth the effort over mocking it up and dealing with any issues with the mockup, foreseen and unforeseen.
A test handing a hash collision is not a test of the hashing algorithm so you should be able to switch out the algorithm to something super predictable. This isn’t a discussion about mocking frameworks - you should be able to do with by just plugging in a different hash implementation when you construct the test.
Also I can employ a $75k - $100k/year engineer and say follow this protocol for testing. If I want someone who can optimize test case 100% of the time with zero mistakes ever then I might have to pay $150k - $200k / year.
If we wanted to change how the hash is calculated all extant instances of the system need to dump to backups, then be painstakingly restored after updating. Possible, but certainly not something you'd be doing for giggles.
Anyone who demanded the real hashing algorithm in this case doesn’t understand how hash collisions work and what problems you need to solve when using hashing. Your “100% accurate test case” serves very little value.
It’s the same as requiring a real person perform the checkout workflow in your shopping cart integration tests. It’s “100% accurate” but it’s a massive waste of resources and provides terrible coverage of the many other edge cases.
You could even measure the performance hit from that degraded condition and create a heuristic to warn you if something is wrong with the fast path.
If each seed is a 32-bit int, and each hash is 60 bits, then you'll have to store 92 bits for each (seed, hash) pair.
With 2^32 (~4.3 billion) (seed, hash) pairs, you'd need 2^32 * 92 bits ~= 50 gigabytes.
But you mention only needing 8-12GB. Is my calculation wrong, or have I missed something?
But, instead of using a hash table to store (seed, hash) pairs, we can just use a list of only hashes. To generate the next hash we use current hash. i.e. next_hash = hash("example.com/" + string(int64(cur_hash))). So now, we just store 60-bit hash values which takes up 2^30 * 60 bits = 8gigs.
The only other problem is detecting if a hash already exists, which I think can be done using a space efficient probabilistic hashset like a bloom filter (and when the bloom filter says the hash already exists, we do a linear scan to find where it exists and we have a possible collision).
Oechslin improved on Hellman's idea to make "Rainbow Tables" by sightly perturbing the reduction function with the depth, so that collisions are fleeting unless they also coincidentally happen in the same position on a hash chain, this makes his Rainbow Table far more efficient/ reliable for the same pre-computation effort and it produces the "rainbow" picture in his slide deck.
For my purpose Oechslin's improvement was irrelevant of course because I actually wanted collisions.
But memory fades, it is possible either that I got lucky, that I actually had 16GB of RAM, or that I made some minor efficiency improvements that were enough to make it work.
There's some kind of sparsity/entropy function involved with optimizing these two but I'm too clueless to sort it out. Without the vocabulary to make my case I just got weirded out by a bunch of smart people staring at me quizzically. Sooo..
Sometimes it helps to write a doc afterwards to get my thoughts in order and decide if it's worth sharing.
Much more memory efficient to use a Bloom filter. Build n different ways of hashing the hash (this can be as simple as having some standard hash function H() making the ith different way H(h, i)). Then when you add an entry you write a bit at each of these locations.
Instead of writing right away, you check to see if all the bits were already set. If they were, the entry being added is a candidate for a possible collision with some earlier entry, so stream it out to a file or something.
Once you have the (much smaller) list of a few million collision candidates, you do another iteration over the whole data set, checking each one against only the collision candidates.
The most memory-intensive part is the Bloom filter. You could probably get away with a Bloom filter size of 10 bits per entry or less. (If you want specific numbers, there are lots of online calculators to help you calibrate the size of a Bloom filter.)
But given this is a one-off search, and you had a machine with enough RAM to find a collision, an ordinary hash-based set implementation is good enough and saves the most precious resource of all, developer time :)
We actually had most of the bloom filter code you'd need because we used a lot of Bloom Filters in that project. One of the very common queries that system had was "Hey do we know about random thing X?" for which the answer would often (but not always) be "No". So Bloom Filters enabled the system to be able to say definitively "Nope, I know nothing about X" a good fraction of the time without any further work. This actually hurt us in some third party benchmarks we ran for boasting, but helped in our real use case.
* Target: 64-bit space (2^64).
* GPUs are more flexible and faster. A Vega64 (a few years old now) has 4096 shaders ("CudaThreads") at 1.6 GHz. It can search the entire 32-bit floating point (or integer) space in less than a second. 1.6 GHz x 4096 shaders == 2^42.5 bitspace per second.
* People exhaustively searching a space are relatively patient: and willing to wait months for the results. 60 seconds x 60 minutes x 24 hours x 30 days == 2^21.3
1.6 GHz x 4096 shaders x 60 seconds x 60 minutes x 24 hours x 30 days is roughly equal to 2^63.8. We've done it: a month of GPU effort is all you need to exhaustively search the 64-bit space
-------
We've entered a world (~3 years ago), where it is reasonable to brute force the 64-bit space, so long as you're willing to wait about a month for the results.
If you're not willing to wait a month, then use 2 GPUs, or 4 GPUs, or a cluster of GPUs like the DGX A100 (or multiple clusters).
But that also assumes that the operation you're testing is 1-clock tick, which is only true for the most trivial of calculations. Still, 216kWs per day per "test-case clock ticks" shows us that the 2^64 exhaustive bitspace is within a reasonable organization's grasp.
I am also not sure about how fast you are if you need to transfer data out of the GPU, or to add conditional branching to the shader code (it used to be that branches were super slow in GPUs, not sure where they are now).
I could see it working for some other problems though.
GPUs implement the ceil and round functions.
Heck, GPUs implement popcount, bit-reverse, and a whole slew of instructions that CPUs don't. (Mainly bitreverse and bpermute. Intel x86 only has permute (aka pshufb), but not the backwards version. BSwap can't emulate bitreverse)
> (it used to be that branches were super slow in GPUs, not sure where they are now).
Branches are fine. Its divergent branches (in both CPUs and GPUs) that are slow. A uniform branch will be branch-predicted on a CPU, while a uniform branch on a GPU will have all 64-threads (AMD GCN) or 32-threads (NVidia or RDNA) follow the instruction pointer.
There's no slowdown with uniform branches, either on CPUs or GPUs. Both CPUs and GPUs do poorly on divergent branches, but GPUs do MUCH worse than CPUs.
Given the advances in computers over the past few years: exhausting the 64-bit (or 2x 32-bit) space is "reasonable" for the year 2020. At least, reasonable for people who are willing to wait a month (on one GPU), or willing to buy a cluster of GPUs.
There may be some GPU code that is reasonable to test on all 2^64 values, but it doesn't help us verify the implementation of SSE (Intel architecture) ceil/round/etc against reference implementations at all, because trying all double precision values would not complete in a practical amount of time and offloading it to a GPU means we are not testing the SSE implementation that we're trying to verify anymore.
Actually we're getting close-ish: AVX512(=8x64) at 4GHz (or two dependency chains at 2GHz) times 64 CPU cores (which TFA's author has mentioned having) is 2 teraops. Double-precision space of 2^64 / 2Tops is 3 months/instruction on a single computer. So you could brute-force verify a simple function in a year or two, and at least the SIMD width and number of cores is likely to continue increasing.
Also, AVX512 throttles down, so you're not going to running it on all cores full tilt at 4GHz today.
I think it's much more reasonable for 64 bit space to have a reasonable set of human-chosen test cases + a large randomized test sequence.
No, the post is about exhaustive testing, and that it's completely reasonable on 32 bit values because it only takes a few minutes.
> Given the advances in computers over the past few years: exhausting the 64-bit (or 2x 32-bit) space is "reasonable" for the year 2020.
That's not actually reasonable, it's feasible for very specific contexts.
> At least, reasonable for people who are willing to wait a month (on one GPU), or willing to buy a cluster of GPUs.
And whose code specifically can run on GPU. TFA is about CPU-optimised code, the entire point was to exhaustively test an SSE3 implementations. How do you do that on a GPU?
I was inventing a new hash function for myself, just for giggles a few weeks ago.
I needed a constant: I started by choosing an arbitrary constant (I started with 0x31415926...), but then I realized that the 32-bit space, and even 50+ bit spaces were feasible on my GPU.
I ended up rewriting the random-number generator on the GPU, and searched for the number which caused the largest number of "avalanche" bit-flips across the entire 32-bit seed space of the RNG.
Where an "avalanche" bit flip is 16 bits flipped, and 16-bits remaining the same, after the RNG was applied to the seed.
For example: seed = 1 * K, where K == 0xAAAAAAAA will cause 16-bit flips, and 16-bits to remain the same.
Repeat for all 32-bit values of seed, searching for the optimal K. 0xAAAAAAAA wasn't the best number (aka: 1010101010101010 binary), but you can see where my thought process was.
My RNG was more complicated than just a single multiply, but I think you can see where the general thought process was with my above example.
--------
There was a time when constants were arbitrarily chosen for these kinds of functions. But today, we can literally test ALL numbers and just pick the best.
If K is constrained by some other requirements (in my case: it must be odd, and a few other tidbits to work with my RNG), then you shrink the search space from 64-bits down to something that can be accomplished in just a few days of search.
-------------
As far as I'm concerned, the blog post is talking about how modern computers have conquered the 32-bit space.
My current post is how the 64-bit space could be feasibly explored. It wasn't even that long ago when 64-bit space was considered cryptographic-secure (see DES crypto algorithm or WEP).
I think there are a lot of computing algorithms you might run on a GPU where you might want to verify the correctness over the range of 32-bit or 64-bit numbers (integers or floating point).
This is another in the "Now that we have computers with gazillions of bits of RAM and many cores and gazillions of cycles, what conventional wisdom is no longer wise?" series :-).
I suggested to a client that they use Sqlite3 for their company database rather than a disk based database. And they looked at me shocked, as if I had spoken heresy. I pointed out how many records they could store on a 64GB machine and that is a small data center class machine and it sort of boggled their mind. Add read-only copies, shared from a NAS device gave them perfect scalability in terms of queries per second (their sales folks had their own database that they were updating). It was fun to watch their expression go from horror to wonder to excitement when they finally implemented it.
I work for another shop that scolds me for even suggesting using something else than k8s. How can a persistent file system possibly scale? Lol. Ironically they're grossing -$3 million/year.
The IBM 53-bit quantum compute result from a ton of hard drives + compute power was also intriguing (https://www.ibm.com/blogs/research/2019/10/on-quantum-suprem...).
128PiB of disk space needed for a 54-qubit computation on their system (hard drives). Fun to see where the supercomputers are still needed, but smaller qubits can be emulated on normal computers. 40 qubit calculations probably can be emulated on a 16TB team of hard drives in RAID0.
Ninja edit: or a UPS and a shutdown procedure triggered by it that writes the database to disk. You can write 100GB in a couple minutes over a 10Gbps network. Faster to a local SSD.
There are just certain classes of data (e.g. any kind of business transaction) where you can’t afford this type of best effort durability.
Sqlite is a disk based database...
The main problem I was uneasy about was, I would simply use the equivalent of rsync -Pa /ramdisk /disk in a loop, which seemed to ensure safety in the event of a poweroff. It never broke, but I wouldn’t say it was a good idea, necessarily.
Sure speeds up most things though. The benefits are pretty crazy for I/O bound programs.
Randomly inject bit flips into a target process memory space, so you can see/test how your software handles it (if at all).
That's probably a bad idea. rsync isn't going to copy over writes in the same order they were written by the DB. Most DBs assume that when there's some crash that at least the disk would have been in a logically consistent state with respect to the order of certain writes separated by e.g. fsync and barriers.
https://news.ycombinator.com/item?id=15252426
Can't believe this was already 3 years ago. Interesting read!
Previous submission 2014: https://news.ycombinator.com/item?id=7135261
32 bits (and now 64 as @dragontamer calculated) is a huge input space. You could verify 4 int_8 or 2 char[2] functions.
Basic math ends at division. Heck if you learn how math works on computers, it feels like a number trick. Those are reliable.
The issue comes when you need to track never ending decimal points. Even if every transistor was dedicated to your problem, you'd still need to round. So even if you have 32 bits of decimals, there will be rounding sometimes.
The human has to decide what to do. Instead of converting the binary to decimal and calling it done, it's more important to be consistent and predictable.
The issues described in the article arent 5/2= 2 billion. The article talks about rounding giving you always even numbers, which you can imagine leads to issues if you are using both float and using logic that involves even and odd numbers.
Edit- can someone correct me or add details, I'm very interested but I'm not taking ENG 240 again... :p
So, "decimal places" or "rounding to a decimal place" has nothing to do with base 10 (which is what I think you are implying): decimal and "decimal places" exist in all bases, including base 2, and the meaning of each "decimal place" depends on the numeric base (base 10, base 2, etc.).
In ML precision doesn't count so much. It's often possible that we train on 32bits and deploy the model on 16bits. In fact stochasticity is useful and we add it in by dropout or other things. You can drop any connection (or millions of them) in a neural net and get almost the same result. There have been papers that showed that reducing the number of bits used to represent weights might even improve the network because it has a regularising effect. The real brain is also stochastic.
I recently helped move/port one fairly complex numeric model from a language and environment to another language and environment and there was an assumption made by upper management that this was simple and exact deterministic results would be easily had.
I warned that this could be an involved process because of simple variations, lack of equivalent mappings between environments and languages, some of which pointed out in this article.
Management were quite surprised with the level of effort needed to dig down layers of abstraction and identify all these sort of variations in numeric libraries from consistent high level abstractions they were familiar with, and why those high level abstractions simply didn't exist in the other language and environment. They were additionally surprised inthe level of effort needed to get satisfactory error tolerance ranges between the two models when having to recreate high level abstractions identically from the source language and environment to the target language and environment that simply didn't exist prior to the work.
They didn't realize the variation until we performed a detailed analysis on results and found very small but statistically distinguishable results from the two versions. We ended up identifying a few dozen small assumptions they made about their high level abstractions from the source environment that were false and caused the variation.
So "wrong" as used in the article simply means "not the same as the IEEE behavior", not "wrong" as in "wildly inaccurate". It matters if you want 100% exact emulation to get identical results. It doesn't if you don't and just want speed.
Round-to-even (also known as banker's rounding, because it's used in finance too) isn't weird at all. It minimizes rounding error, especially on repeated calculations. The rounding rule that everyone learns in elementary school is bad and results in systematically biased computations. We can and should do better than that flawed approach. This post explains it in more depth: https://mathematica.stackexchange.com/a/2120
Most bugs are corner cases of one sort or another. At the application level you can decide how important the corners are. When you're creating a math library that many others are going to use, and there is a standard, I think the expectations should be higher.
Which is to say, that minor errors often don't matter, but if you are offering up a general purpose solution then those minor errors probably matter to _somebody_.
To know whose bug it was in your example, you have to make a value judgment about whether the changes were minor and justified, not just a technical evaluation.
This bug contains both, I think. The correct result for round(5.5) is arbitrary. The reason that 6.0 is the correct answer is "merely" because that is what the IEEE standard says. That standard could have been different (round-to-nearest odd), but there is value in not lightly ignoring an existing standard.
Other cases are more clear cut. ceil(1e-10) should return 1. This seems non-controversial since ceil is defined as rounding up. Any number greater than zero and less than or equal to one should return one. And yet, some of the algorithms would return zero. That was probably the most serious of the errors.
How serious the errors are depends on how the functions are used, but I think they were all promoted as general-purpose-replacements so they could have ended up being used anywhere.
1. That can’t happen.
2. That doesn’t happen on my machine.
3. That shouldn’t happen.
4. Why does that happen?
5. Oh, I see.
6. How did that ever work?
It's not DNS.
There's no way it's DNS.
It was DNS.
https://www.cyberciti.biz/humour/a-haiku-about-dns/Found a lot of bugs this way.
There will be orchestration overhead and transmission latency, but I guess one could get all results in one place in a day, if one had the ability to do the “run it on every smartphone” thing.
I figured plotting a few bytes if output (256x256) would do nicely. I plotted all possible two byte inputs just to make sure it was working, and that ran instantly, so I tried 3 bytes and that was still pretty fast, so I did 4 while I was at lunch or in a meeting.
Sure enough, big black splotches from dropping some of the seed data on the floor. Please to be fixing.
Old farts know a lot of things have been tried before and how those worked out. But we also think doing a complex operation 4 billion times is going to be irretrievably slow. Always test your assumptions.
I thought Float Epsilon was the smallest possible number which could be represented by a float? This statement doesn’t seem to make sense...
The utility of this metric is that it tells you the best possible relative error, as you can never guarantee a result will be more correct to the true result (in the domain of mathematical real numbers as opposed to machine floating-point numbers) than half of machine epsilon.
This concept is often extended into discussions of "units in the last place" (ULPs), which is the magnitude of the value of last bit of the mantissa. So ulp(1.0) = ulp(1.5) = machine epsilon, ulp(2.0) = ulp(3.9) = 2 * machine epsilon, etc. A good floating-point library will document its accuracy in terms of ulps, so that we might say that the maximum error of the exp function is 1 ULP and that of pow might be 6 ULPs.
- If you add 1.0f to FLT_EPSILON then you get an exact result - If you add 1.0f to FLT_EPSILON * 0.5 then you get a number that cannot be represented and it will either round to 1.0 or 1.0+FLT_EPSILON - Similarly, if you add 0.5f to FLT_EPSILON * 0.25 then you get a number that cannot be represented and it will either round to 0.5 or to 0.5 + FLT_EPSILON * 0.5
With the default rounding mode that last calculation rounds to 0.5. There are a _lot_ of floats below FLT_EPSILON.
https://en.wikipedia.org/wiki/Barrett_reduction
Specifically one that uses 80-bit long double for calculating `(__int128_t)a * b % M` from this github:
https://github.com/kth-competitive-programming/kactl/blob/ma...
But how do you test the correctness of such code? You can't just stress test all possible product of 64 bit numbers!
In the end you still gotta go back to good old fashion proofs: https://github.com/kth-competitive-programming/kactl/blob/ma...
I think the point is that exhaustive testing is feasible in more situations than you might at first think, and than floating-point arithmetic is a particularly fertile source of edge conditions and corner cases, so it's a good place to go for exhaustive testing where you can.
I should learn to read
E.g. for a function that takes two floats (x,y) test every value of x input with y={0,-0,1,nan,inf,-1,x,-x,2x,x/2,FLT_EPSILON,FLT_MIN,FLT_MAX,random,random,random...}, and then repeat with x,y swapped.
A little knowledge of the domain can help pick better special values, but there is a trade-off in introducing the same blind-spots that permitted bugs in the first place. Still: testing just one argument on all values because that's all you can afford is still a lot better than only testing a couple special values for both arguments.
Exhaustive testing, like randomized testing catches errors you weren't even thinking about-- but unlike random testing exhaustion can reliably catch bugs which are extremely rare. "Partial exhaustion" can have some of the same benefits.
SSE4.1 introduced roundps instruction that's both fast and correct. Unlike AVX2 or FMA3, SSE4.1 support is almost universal by now. In most of my current projects, I check support on startup and refuse to run if SSE4.1 ain't supported. Just don't forget to set the corresponding macros for DirectXMath, Eigen, etc.
A version of the Pentium was released that couldn't divide by 10. And even back then, testing all the mantissas on a simulator would have taken only hours. Instead, they masked, marketed and released a tragically flawed processor.
Just test the input space exhaustively, folks! It's a computer; it feels no pain. Work it hard until it fails or works perfectly.
But... would randomness help? Take a representative sample of like 1/64th of the double space and you might run into most of the common problems.
400 trillion hashes is not a big computation in the blockchain era.
Does anyone know the reason behind this? why the nearest even and not a hard cut like "always round up with >= 0.5"?
Certain applications will choose fixed point to avoid what appears to be the imprecisions of floating point.
2. "Conventional wisdom Nazis" Maybe the author was just bored of writing at this point, but some justification for why bitwise comparing floats was a correct decision would have been nice. Is it because there's no math involving unrepresentable intermediates? Because there's no integration happening, so no way for errors to accumulate?
Anytime there is a correct answer, and it can be determined exactly (+, -, *, /, sqrt) you should expect nothing less than perfection. ceil, round, and floor fall into this category fairly self-evidently, I think.
I hope people test like that for really critical stuff, but not for your everyday library for your personal startup or e-business. Not even the big players need this.
What sort of clown would make that mistake and yet think they are capable of writing a floating point library?
ceil(5) should be 5
So, if ceil(x) is implemented as round(x+0.5) then: ceil(4) = round(4.5) = 4 ceil(5) = round(5.5) = 6
The first of those is correct. The second is incorrect.
Such effects and similar should be well known to anyone taking on the responsibility to develop a floating point library.