-
Notifications
You must be signed in to change notification settings - Fork 360
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Apply random float error #4156
Apply random float error #4156
Conversation
Please also add a test (as a new function in |
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
Almost done I think. I have 1 TODO left, inside of the non-determinism test, it is about this:
Should we do an |
a64db7e
to
b82c735
Compare
@rustbot ready |
I think the easiest way is something like this; you can do that with a closure returning any |
@rustbot ready |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks a lot, this looks great! All comments are minor.
@rustbot author
Please post @rustbot ready
when the PR is ready for the next round of review.
tests/pass/float.rs
Outdated
ulp diff: {ulp_difference} > {allowed_ulp} | ||
", | ||
*a, | ||
*b |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This will print complete nonsense when the exponent is different, won't it?
I'm not convinced going via the hex/binary representation is the best option here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
f16
and f128
currently don't implement Display
, so using debug was the only thing that worked (that i know of).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't understand your reply.
If the exponent is different, subtracting the two i128
representations will not be the difference in ULP, it will be complete junk. The entire logic falls apart.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So here's an alternative way to approximate the distance in ULP, but it may fail spectacularly due to rounding:
let ulp = (expected.next_up() - expected.next_down()) / 2;
let ulp_diff = ((real - expected) / ulp).round();
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have to be honest, I don't see why we would use this way of calculating ULP. Isn't it unlikely to get approximately equal values with different exponents in our tests?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's not a question of probability -- if we happen to write a test where the result is near the boundary between values represented with different exponents, we'll suddenly get completely nonsensical results. That's fragile and hard to debug. We shouldn't have surprising footguns in our code that can go off any day in the future that someone adds a new test.
tests/pass/float.rs
Outdated
frem_algebraic, frem_fast, fsub_algebraic, fsub_fast, | ||
}; | ||
use std::{f32, f64}; | ||
// FIXME(powi_powf): add when supported |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what is that about?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
you mentioned in the zulip chat to not include the pow
intrinsics in this pr. I should explain this better inside the comment.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah I see. The better place for the FIXME would be in the shim implementation.
tests/pass/float.rs
Outdated
/// Another form of checking if 2 floating point numbers are almost equal to eachother | ||
/// Using `a` and `b` as floating point numbers: | ||
/// | ||
/// Instead of doing a simple EPSILON check (which we did at first): | ||
/// Absolute difference of `a` and `b` can't be greater than some E (10^-6, ...). | ||
/// | ||
/// We look at ULP: `Units in the Last Place` or `Units of Least Precision` | ||
/// It is the absolute difference of the *unsigned integer* representation of `a` and `b`. | ||
/// For example for f32, which in IEEE format looks like this: | ||
/// | ||
/// s: sign bit | ||
/// e: exponent bits | ||
/// m: significand bits | ||
/// f32: s | eeee eeee | mmm mmmm mmmm mmmm mmmm mmmm | ||
/// u32: seee eeee emmm mmmm mmmm mmmm mmmm mmmm | ||
/// | ||
/// We see that checking `a` and `b` with different signs has no meaning, but we should not forget | ||
/// -0.0 and +0.0. | ||
/// Same with exponents but no zero checking. | ||
/// | ||
/// So when Sign and Exponent are the same, we can get a reasonable ULP value | ||
/// by doing the operation explained above. And if this is less than or equal to our chosen upper bound | ||
/// we can say that `a` and `b` are approximately equal. | ||
/// | ||
/// Basically ULP can be seen as a distance metric of floating point numbers, but having | ||
/// the same amount of "spacing" between consecutive numbers. So eventhough 2 very large floating point numbers | ||
/// have a large value difference, their ULP can still be 1, so they are still "approximatly equal", | ||
/// but the EPSILON check would have failed. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I only learned about this concept last week, but I tried my best. Let me know if i missed something or if something is explained badly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh wow that's a lot more detailed than I expected, thanks!
Fixed these CI checks in the last commit but CI never ran on that one... |
@rustbot blocked I don't know whats going on |
CI tests your branch merged with the target branch. The errors in CI look like you have a semantic merge conflict (GitHub can only automatically detect textual merge conflicts). You should rebase onto the upstream target branch. The most foolproof way to do this is a rebase pull: |
312b028
to
25ecf26
Compare
If you still have a clean local build, run |
@rustbot ready |
Sorry for the delay, Uni started this week. I extended the explanation of the Approx Check, let me know if this is correct and not too long? @rustbot ready |
tests/pass/float.rs
Outdated
fn approx_eq_check<F: Float>( | ||
actual: F, | ||
expected: F, | ||
allowed_ulp: F::Int, | ||
) -> Result<(), NotApproxEq<F>> | ||
where | ||
F::Int: PartialOrd, | ||
{ | ||
let actual_signum = actual.signum(); | ||
let expected_signum = expected.signum(); | ||
|
||
if actual_signum != expected_signum { | ||
// Floats with different signs must both be 0. | ||
if actual != expected { | ||
return Err(NotApproxEq::SignsDiffer); | ||
} | ||
} else { | ||
let ulp = (expected.next_up() - expected.next_down()).halve(); | ||
let ulp_diff = ((actual - expected) / ulp).round().as_int(); | ||
|
||
if ulp_diff > allowed_ulp { | ||
return Err(NotApproxEq::UlpFail(ulp_diff)); | ||
} | ||
} | ||
Ok(()) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Now that this is just regular float arithmetic, why do we still need this trait and all the rest?
Also, most of the explanation is not relevant any more since we are not working on the bit representation any more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Trait is still needed for other helper functions I didn't write, they were also used in macros and such so i followed that approach. It's still a lot of code and the macro will thus generate a lot of code (not sure if this matters), and there is no way to make a float generic function other than using the trait.
I'll update the explanation, let me know what you think about the rest.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Extra: Also the weird extra functions (halve, as_int, round) i had to add to the Trait were necessary to make the generic function work. Just putting in as F::int
or / 2.0
did not work in any clean way other than this. It's no problem if i have to remove it if needed.
10ece47
to
e095b40
Compare
1e977f7
to
5c09010
Compare
I found it easier to just do the remaining changes than to try to explain them, so I have updated the PR accordingly. I also rebased over the latest rustup which adds one more function that had to be tested for non-determinism, and squashed the commits. (If you want to see what I changed compared to your version, look at the 2nd commit.) Turns out a macro-based solution still works just fine. Maybe a method is cleaner, but switching to that should not be done in the same PR as adding new functionality. Thanks for your work! |
5c09010
to
269a7f5
Compare
269a7f5
to
5c9eaa3
Compare
Alright that's good. Thanks, learned a lot! |
This partially tries to fix #3555 (does not include the
pow
operations)this just does
apply_random_float_error
on the results of the intrinsics.Things changed from original:
sqrtf*
intrinsics to make the implementation cleaner, since applying the error was not requiredulp_err_scale
, which does-(F::PRECISION - 1 - n
for a2^n
ULP error.test_non_determinism
which tests a lot of non-deterministic operations with the same input to check if they are almost but not entirely equal.