Higher quality random floats
corsix.org
corsix.org
I've written such an implementation with explanations [0], but it doesn't handle subnormals correctly. I've yet to find the time to revisit the problem.
[0] https://github.com/camel-cdr/cauldron/blob/main/cauldron/ran...
IIRC I also explained it here: https://m.youtube.com/watch?v=VHJUlRiRDCY&t=2774s
I experienced this where a colleague had tried to reduce the number of divisions by rewriting (a/x)*(b/y)*(c/z) to (a*b*c)/(x*y*z).
The problem was that in certain circumstances the terms were all small, but of comparable magnitude. Thus the latter lead to subnormal divided by subnormal. The fix was to undo the optimization and do the divisions first, which lead to the multiplication of three numbers of order 1.
Anyway, I wonder if the optimization even helps at all. Either way the critical path is a division and two multiplications. That seems like the sort of thing that, between SIMD an OoO execution, a modern CPU ought to be able to work out, haha.
No promises but I’d want to benchmark it before caring.
This should not be true in a conformant implementation of IEEE 754-2008. You can get infinity by dividing by a subnormal (due to overflow), but that should only be possible with sufficiently large numerators.
> The problem was that in certain circumstances the terms were all small, but of comparable magnitude. Thus the latter lead to subnormal divided by subnormal. The fix was to undo the optimization and do the divisions first, which lead to the multiplication of three numbers of order 1.
My guess is that this problem was actually the numerator being subnormal and the denominator underflowing all the way to zero and therefore the result going to infinity.
> My guess is that this problem was actually the numerator being subnormal and the denominator underflowing all the way to zero and therefore the result going to infinity.
Yes thinking about you're probably correct.
The probability that your "perfect" code would ever get triggered is such that humankind has not built enough compute to expect to get that unlucky yet.
(you may ask: why then care about that possibility if it's dead code anyway? Because of security vulns in context of flawed RNGs. Bad underlying RNG leading to bad distributions is expected; bad underlying RNG leading to RCE is not OK. So don't ever output zero or subnormals!)
For bfloat16 the same holds; for ieee float16, well, I have no clue what we want.
You are aware that the current age of the universe is < 10^27 nanoseconds, just for comparison?
I was just thinking about the (0,1) case, under the mistaken assumption that one could map it to (a,b) via multiplication/addition, but you're right -- if you want (a,b) perfectly () then it's not obvious to me.
() up to inaccuracies of cosmologically negligible scale
It's been awhile since I've thought about this problem, so I'll reserve the right to be off by a small constant additive or multiplicative factor. You need around 24+256 bits for 32-bit IEEE-754 floats (less by roughly half if you're sticking in a range like 0 to 1), and 53+2048 for 64-bit. The average number of bits required is something closer to 25 and 54, respectively.
Uniform as in all representable numbers have equal probability? Or uniform as in approximating a uniform distribution over real numbers?
Another sample implementation (maybe easier to read?) is in https://discourse.julialang.org/t/output-distribution-of-ran...
As far as I remember, the main reasons for not rocking the boat on that in julia were: Everybody generates rand floats wrong, so doing it right breaks expectations and compat; and there is a perf price of maybe 0.5-1.5 cycles/number to pay; and this is nontrivial to SIMD (basically because we don't have simd TZCNT / LZCNT / access to exponential distribution)
I'd be very happy if this can of worms could be reopened, but am currently not active enough in julia dev to champion it.
Also somebody would need figure out something very clever for AVX2 / NEON. (AVX512 has the required instructions)
Also I can't imagine the mess with GPU -- if rand statistics differ widely between CPU and GPU that's a no-go, and I don't know what works well on which GPUs.
Ugh, I’m sorry they had to deal with this and am sorry with the (understandable) choice they made.
Typical example where the badness of the floats bites you is if you do something like log(rand()), or 1/x, or more generally you map your uniform (0,1] interval via a cdf to generate a different distribution, and your mapping has a singularity at zero (this is like super standard -- e.g. how you generate exponentially distributed numbers. Or if you generate random vectors in an N-d ball, you're using a singular cdf to compute the magnitude your vector, multiplied with a normalized normal distributed N-d variable to generate the angular component).
Then the bad random floats (i.e. the non-smooth distribution close to zero) introduce real and visible bias after transformation. Afaiu the problem is well-known and serious people correct for it somewhere. If you fix the underlying problem without revisiting the now-obsolete fixes, then your results are biased again.
I'm not arguing for keeping the bad random float generation everywhere. I think it should be fixed, not just in julia but everywhere. I'm just saying that it's not a no-brainer and there is discussion and compat and communication and code audit involved in doing such a momentuous change.
Also, I'm not really up to putting in the work to champion that at the moment (arguing on the internet doesn't count ;)).
Julia currently uses the same trick as Lua (generate uniform Float64 bit pattern between 1 and 2 by masking a random UInt64 and reinterpreting, then subtract 1).
I wonder what the speed is.
Having the “other” half open unit interval in addition to the traditional one might come in handy for several applications.
Generate two independent doubles a and b the "usual way", meaning from [0, 1) with equal spacings of 2^-53. Then calculate 2^-53*b + a. Assuming for simplicity that the rounding mode is towards zero, this gives a new random double in [0, 1). I've only done the math in my head so I'm not 100% sure on all the numbers, but I believe it scores pretty well at the criteria in the blog post; tentative results are
Probability of zero: 2^-106
Smallest nonzero: 2^-106
Largest non-seeable: approx. 2^-54
Distinct values: 2^58.7
The usual rounding mode (round to nearest, ties to even) will generate numbers in [0, 1] with very slight biases that need closer analysis. One could also go further and do e.g. 2^-106*c + 2^-53*b + a, but something like that should only be needed for for single-precision floats.
The obvious downside is that you always need two random integers and not just in the slow path. However, SIMD becomes easier because you don't need AVX512 for lzcnt.
from random import Random
from math import ldexp
class FullRandom(Random):
def random(self):
mantissa = 0x10_0000_0000_0000 | self.getrandbits(52)
exponent = -53
x = 0
while not x:
x = self.getrandbits(32)
exponent += x.bit_length() - 32
return ldexp(mantissa, exponent)
[0] https://docs.python.org/3/library/random.html#recipes[1] Start with the first `rand_between_zero_and_one` snippet. `x = ((x + 1) >> 1) + (e << 52)` can be rewritten as `d = (1.0 + ((x + 1) >> 1) * 2^-52) * 2^(e - 1023)` (since it always generates a normal number). `E[(x + 1) >> 1]` exactly equals to 2^-51, and `E[2^e] = 2^1022 * 2^-1 + 2^(1022-1) * 2^-2 + ... + 2^(1022-74) * 2^-75 + 2^(1022-75) * 2^-75 = 2^1023 (1/4^1 + 1/4^2 + ... + 1/4^75 + 0.5/4^75) = 2^1023 (1/3 + 1/(6*4^75))`. So `E[d] = (1 + 2^-51 * 2^52) * (1/3 + 1/(6*4^75)) = 1/2 + 1/4^76`.
eg if x = 1 read 53 more mantissa bits and stop, if x = 0 decrement the exponent and repeat. There's no bias in such a process.