Updating Math.Round and MathF.Round to be IEEE compliant so that the intrinsic and managed form are deterministic.#25901
Conversation
…intrinsic and managed form are deterministic.
|
CC. @danmosemsft. We want to backport this to nc3.0 given it impacts determinism under tiering, correct? |
|
CC. @EgorBo |
The impact certainly meets the bar - numeric behavior changing behavior spontanously is a recipe for heisenbugs. But the change seems large and potentially risky. Thoughts about that? Do we have enough coverage on different platforms? Is there a way to run all CoreFX tests without tiering (ie compare a run with all "optimized JIT" and another run with all "quick JIT") to see whether any other behavior is different in the "no tiering" regime? |
|
Is there a more targeted, safer fix that could be done for 3.0 only? I don't care about the perf improvement for 3.0 |
|
Are there more tests we can usefully add (on top of testing with and without tiering)? That could also reduce risk |
This is the targeted fix and is using a known good/existing implementation. It just happens to bring in a perf benefit at the same time.
Tests have to be added to CoreFX, and I am working on getting those up. |
| // This shortcut is necessary to workaround precision loss in borderline cases on some platforms | ||
|
|
||
| if (x == (float)((int)x)) | ||
| // This is based on the 'Berkeley SoftFloat Release 3e' algorithm |
There was a problem hiding this comment.
For reference: https://github.com/ucb-bar/berkeley-softfloat-3/blob/master/source/f32_roundToInt.c
The irrelevant support for the other rounding modes and error reporting was not included.
Code comments were added for readability, they do not exist in the reference.
|
|
||
| // If the number has no fractional part do nothing | ||
| // This shortcut is necessary to workaround precision loss in borderline cases on some platforms | ||
| // This is based on the 'Berkeley SoftFloat Release 3e' algorithm |
There was a problem hiding this comment.
For reference: https://github.com/ucb-bar/berkeley-softfloat-3/blob/master/source/f64_roundToInt.c
The irrelevant support for the other rounding modes and error reporting was not included.
Code comments were added for readability, they do not exist in the reference.
There was a problem hiding this comment.
It might be worth adding that note (i.e. that the other rounding modes from that algorithm are not included).
|
Also can you briefly educate me how tiering caused htis? |
Under Tier 0, the JIT will just use the pre-jitted After a certain number of calls (30), the JIT will rejit these methods for the current hardware. On machines with SSE4.1 support (most machines in the past 12 years), the JIT will treat these methods as "intrinsic" and just directly emit the This difference can cause cold vs hot methods to return different results for some inputs (for example, as is the case with |
|
This PR just updates the implementation to use a known good/correct implementation which should always return the correct result (and therefore the behavior should always match between the various tiers). |
|
I found the problematic values using this script: using System;
using System.Runtime.CompilerServices;
class Program
{
static void Main()
{
Random rand = new Random();
for (int i = 0; i < int.MaxValue; i++)
{
double rd = rand.NextDouble();
int ri = rand.Next(int.MinValue, int.MaxValue);
if (SSE41Round((float)rd) != ManagedRound((float)rd))
{
Console.WriteLine(rd);
}
if (SSE41Round((float)(rd * ri)) != ManagedRound((float)(rd * ri)))
{
Console.WriteLine(rd);
}
}
Console.WriteLine("Done.");
}
// aka R2R
[MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)]
static float ManagedRound(float x) => MathF.Round(x);
// aka tier1 (or tier0 if mscorlib is not prejitted)
[MethodImpl(MethodImplOptions.AggressiveOptimization | MethodImplOptions.NoInlining)]
static float SSE41Round(float x) => MathF.Round(x);
} |
This bug is about observable floating point behavior differences with optimizations on vs. off. Tiering switches between optimization off and on mid-flight that makes this type of bugs to be more severe. This specific bug is not new. It has been in the product for a long time. The following program will produce difference results for What is our overall confidence that floating point computation produce exact same results with optimizations on vs. optimizations off? If we are not confident in this, "safe fix" may be to disable tiering for any methods with floating point math. |
|
@jkotas at least it couldn't happen during the same app session/process at some random point. I guess it makes sense to revise all UPD it looks like only |
…n 0.5 and less than 1.0
I created and ran the following program for the entire 32-bit floating point range. Aside from NaN differences (software preserves the exact NaN, hardware normalizes to a single NaN), the results are identical: https://gist.github.com/tannergooding/a55001ef69b6ec0746fc05468786cf80 |
|
(I also found a bug in the codegen for |
|
Logged https://github.com/dotnet/coreclr/issues/25904 and opened #25905 for the aforementioned codegen issue. |
|
So there are 3 implementations
|
Correct, although the second two are meant to be identical copies of each other (the former in C# and the latter in C++). |
|
Could I get review/sign-off on this for .NET 5 at least, and then we can take it to tactics on Tuesday? Or are you wanting a different change for release/3.0? |
|
@jkotas is back Monday, I am interested in his take and someone on @dotnet/jit-contrib to help choose correct fix for 3.0 if any. If we want to do something different for 3.0, I suggest to put that smaller change in master as well, so it gets more coverage, and we can later put the better fix (if it is this) in master.
@kouvel thoughts on the above? It seems this is not a new bug, it is just not encountered easily without tiering. |
|
The confidence is fairly high given:
|
Point taken. @dotnet/jit-contrib ? On the numerics side, @GrabYourPitchforks any concern? |
|
This fix for
I meant floating point computation in general, not specific to |
Am I good to merge this and put up the backport to release/3.0 for tactics tomorrow then?
There are still a few known IEEE bugs and they are all tracked by issues. I don't think we have any other known ones for Debug vs Release or Tiered vs non-Tiered right now. As for finding them, I think all we can really do here is compare against known good implementations (the implementations that are slower but have been the "gold standard" for many years) and increase test coverage for known problematic values (not just the special values like positive/negative |
|
I'm fine with merging and porting if this is signed off on |
AndyAyersMS
left a comment
There was a problem hiding this comment.
Do we know when the original regression was first introduced?
|
|
||
| // If the number has no fractional part do nothing | ||
| // This shortcut is necessary to workaround precision loss in borderline cases on some platforms | ||
| // This is based on the 'Berkeley SoftFloat Release 3e' algorithm |
There was a problem hiding this comment.
It might be worth adding that note (i.e. that the other rounding modes from that algorithm are not included).
I believe that, as discussed above, this is a long-standing bug, but the difference in behavior was not readily observable without tiering. |
|
@AndyAyersMS, there would have been Debug vs Release differences since ~January of last year when support for generating The managed implementation has been "incorrect" and not matched the IEEE compliant behavior for as far back as I can find. Edit: As Carol called out, the tiering difference which impacts the same program is new in .NET Core 3.0. |
I'll add this in a follow up PR here. |
…t so that the intrinsic and managed form are deterministic. (#26017) * Updating Math.Round and MathF.Round to be IEEE compliant so that the intrinsic and managed form are deterministic. (#25901) * Updating Math.Round and MathF.Round to be IEEE compliant so that the intrinsic and managed form are deterministic. * Fixing the Math.Round and MathF.Round handling for values greater than 0.5 and less than 1.0 * Applying formatting patch. * Adding a comment about only having the roundToNearestTiesToEven code path
…intrinsic and managed form are deterministic. (dotnet/coreclr#25901) * Updating Math.Round and MathF.Round to be IEEE compliant so that the intrinsic and managed form are deterministic. * Fixing the Math.Round and MathF.Round handling for values greater than 0.5 and less than 1.0 * Applying formatting patch. Commit migrated from dotnet/coreclr@c384e36
This resolves https://github.com/dotnet/coreclr/issues/25857 and also brings in a nice little perf increase for the managed implementation.
Before Intrinsic
After Intrinsic
Before Managed
After Managed