-
Notifications
You must be signed in to change notification settings - Fork 13.2k
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
Enable Non-determinism of float operations in Miri and change std tests #138062
base: master
Are you sure you want to change the base?
Enable Non-determinism of float operations in Miri and change std tests #138062
Conversation
…754 and C Standards
The Miri subtree was changed cc @rust-lang/miri |
The library changes lgtm, for the rest r? @RalfJung |
Co-authored-by: Dante Broggi <34220985+Dante-Broggi@users.noreply.github.com>
I should have clarified: the doc tests fail when running |
src/tools/miri/src/intrinsics/mod.rs
Outdated
2, // log2(4) | ||
); | ||
|
||
// Clamp values to the output range defined in IEEE 754 9.1. |
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.
We said we're going to follow the C standard, which is more permissive than the IEEE spec. Is there a reference for this in the C standard?
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.
Yes and no...
For some operations it specifies it like this:
The atan functions return arctan x in the interval [−π/2, +π/2] radians
But for sin
and cos
like this:
The sin functions return
sin x
If I read "returns sin x", I understand it as "the output is [-1, +1]", but maybe that's just me.
src/tools/miri/src/intrinsics/mod.rs
Outdated
let fixed_res = match (f1.category(), f2.category()) { | ||
// 1^y = 1 for any y even a NaN. | ||
// TODO: C Standard says any NaN, IEEE says not a Signaling NaN | ||
(Category::Normal, _) if f1 == 1.0f32.to_soft() => Some(1.0f32.to_soft()), |
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.
Can you avoid using soft floats 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.
In the miri issue you said this:
except that the logic for fixed_res and clamp has to be done with soft-floats.
Did I misunderstand what you meant?
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.
Argh sorry, I meant "avoid using hard floats" :)
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.
alright!
src/tools/miri/src/intrinsics/mod.rs
Outdated
let fixed_res = match (f1.category(), f2.category()) { | ||
// 1^y = 1 for any y even a NaN. | ||
// TODO: C says any NaN, IEEE says no a Sign NaN | ||
(Category::Normal, _) if f1 == 1.0f64.to_soft() => Some(1.0f64.to_soft()), |
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.
Please do not duplicate the same logic 4 times. We were a bit lazy here so far as the boilerplate for making the code generic was bigger than the code, but with all this special-case handling that is no longer the case.
src/tools/miri/tests/pass/float.rs
Outdated
// accept up to 64ULP (16ULP for host floats and 16ULP for miri artificial error and 32 for any rounding errors) | ||
assert_approx_eq!($a, $b, 64); | ||
// accept up to 52ULP (16ULP for host floats, 4ULP for miri artificial error and 32 for any rounding errors) | ||
assert_approx_eq!($a, $b, 52); |
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.
Do we really still need 52 ULP? I would have hoped that 32 works now.
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.
Indeed 32 works, don't know why I didn't tried it.
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 tried lower but got some ULP differences of 27.
src/tools/miri/tests/pass/float.rs
Outdated
// TODO: How to test NaN inputs? f*::NAN is not guaranteed | ||
// to be any specific bit pattern (in std). |
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 is guaranteed to be a NaN though. Why do you want a specific bit pattern?
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.
IEEE is not as permissive as C, so it doesn't matter much now, just one case: C standard says for pown
(powi
) the following:
pown(x, 0) returns 1 for all x not a signaling NaN
And since std
doesn't guarantee the specific bit pattern of NaN
, I'm not quite sure how to test it.
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, since NAN
might be signaling? That'd be quite bad.^^
Please rely on it being not signalling, I'll see if we can fix the docs.
it is failing the tests that use |
library/std/tests/floats/lib.rs
Outdated
macro_rules! assert_approx_eq { | ||
($a:expr, $b:expr) => {{ assert_approx_eq!($a, $b, 1.0e-6) }}; | ||
($a:expr, $b:expr) => {{ assert_approx_eq!($a, $b, 1.0e-4) }}; |
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 think I'd rather not change this for all tests, and in particular not for f64
.
An alternative would be to just set this precision with the affected tests in library/std/tests/floats/f32.rs
.
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.
Maybe the precision should be only loosened only with cfg!(miri)
as well. I think it is beneficial to learn which platforms, if any, have worse precision than we expect and address that on a case-by-case basis.
So I:
|
No, a macro is not right. The way to avoid this duplication is to add a function that is generic over the apfloat float type. I'd say just remove |
Yeah Sorry, I was confused with your comment: |
I was confused with your comment: avoid using soft floats.
sorry for that :(
|
No problems! This float stuff it getting to me too :) |
… for powi + a function that clamps a float based on the operation used
So I extended the There is still some repetitiveness, like applying the error or |
src/tools/miri/src/intrinsics/mod.rs
Outdated
@@ -522,3 +572,121 @@ fn apply_random_float_error_to_imm<'tcx>( | |||
|
|||
interp_ok(ImmTy::from_scalar_int(res, val.layout)) | |||
} | |||
|
|||
// TODO(lorrens): This can be moved to `helpers` when we implement the other intrinsics. |
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.
No please only but things in helpers
that are widely useful across Miri. Specific functionality should remain in its specific file.
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.
Alright, I thought that this would be the case when implementing the foreign_itmes
, since they probably will share this functionality, like asin
and such.
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 yeah... we'll figure that out when we get there.
src/tools/miri/src/intrinsics/mod.rs
Outdated
// x^(±0) = 1 for any x, even a NaN | ||
("powf32" | "powf64", [_, exp]) if exp.is_zero() => Some(one), | ||
|
||
// C standard doesn't specify or invalid combination |
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 is not a sentence...?
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.
Yeah, now that I am reading that again... I'll update it, excuse me for my bad English :)
src/tools/miri/src/intrinsics/mod.rs
Outdated
// TODO: not sure about this pattern matching stuff. It's definitly cleaner than if-else chains | ||
// Error code 0158 explains this: https://doc.rust-lang.org/stable/error_codes/E0158.html | ||
// The only reason I did this is to use the same function for powf as for sin/cos/exp/log |
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 the comment. The code looks nice though :)
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.
Okay, I should have explained why the error code applies here.
Consider this arm:
// sin(+- 0) = +- 0.
("sinf32" | "sinf64", [input]) if input.is_zero() => Some(*input),
I still have to put an if-guard on it to check that input
is Zero, but in my opinion, it would be a lot nicer if I could do the following:
// sin(+- 0) = +- 0.
("sinf32" | "sinf64", [IeeeFloat::<S>::Zero]) => Some(*input),
If I then were able to make a constant for 1
and maybe other values, I wouldn't need those if-guards. But unfortunately, the compiler doesn't try to see if this can work; it just assumes this to be non-exhaustive, regardless of a _ => ...
arm.
Luckily, you like the code :)
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 can't use consts that depend on generics in patterns like that, that is expected.
src/tools/miri/tests/pass/float.rs
Outdated
// TODO: How to test NaN inputs? f*::NAN is not guaranteed | ||
// to be any specific bit pattern (in std). |
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, since NAN
might be signaling? That'd be quite bad.^^
Please rely on it being not signalling, I'll see if we can fix the docs.
Regarding the library tests, I suggested a plan above for the doc tests. Have you tried that? EDIT: Ah, yes seems you did. :) I assume all tests pass now in the current state of the PR? Also:
I am not convinced we want to do that, so unless @tgross35 asks for it I'd say please stick to the current approach. |
@tgross35 could you have a look at the library diff again? // Miri adds some extra error to float functions, make sure the tests still pass.
const APPROX_DETLA: f32 = if cfg!(miri) { 1e-4 } else { 1e-6 }; |
Yes, they indeed pass now. On the topic of doc and doctests, for example, // part of powf doctest
let x = 2.0_f32;
let abs_difference = (x.powf(2.0) - (x * x)).abs();
assert!(abs_difference <= 8.0 * f32::EPSILON); This is not wrong, but shouldn't it be noted that this only works with small values? |
Links to #4208 and #3555 in Miri.
Non-determinism of floating point operations was disabled in #137594 because it breaks the tests and doc-tests in core/coretests and std.
This PR:
assert_eq!
toassert_approx_eq!
.assert_approx_eq!
macro to allow up to 1e-4 to make the tests passTODO:
assert_approx_eq
to use the same technique as Miri (i.e., using ULP instead of EPSILON)