Skip to content
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

Add unit-of-least-precision float comparison #13723

Merged
merged 12 commits into from
Sep 14, 2024

Conversation

stefanvodita
Copy link
Contributor

Comparing floats with a fixed epsilon doesn't really work. We add comparison based on unit-of-lest-precision (ULP) and use it to fix a failing test.

Closes #13720


// Avoid possible overflow from adding the deltas by splitting the comparison
assertTrue(deltaPlus <= maxUlps);
assertTrue(deltaMinus <= (maxUlps - deltaPlus));
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should add a return inside this if conditional. Otherwise you fall through to a condition check that may return an incorrect result if xInt - yInt overflows.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Of course - my mistake! Thanks for pointing it out.

@uschindler
Copy link
Contributor

Should we add the "double" variant, too?

@@ -654,7 +654,7 @@ private void assertFloatFacetResultsEqual(List<FacetResult> expected, List<Facet

assertEquals(expectedResult.dim, actualResult.dim);
assertArrayEquals(expectedResult.path, actualResult.path);
assertEquals((float) expectedResult.value, (float) actualResult.value, 2e-1);
assertUlpEquals((float) expectedResult.value, (float) actualResult.value, (short) 2);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is 2 ulps correct here?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question @uschindler -- now we have the fun problem of figuring out how many ULPs can be lost due to different order of operations ...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's correct in the sense that it's the smallest value that will pass the test that was failing. Is it the smallest value that will pass this test on any seed? That I'm not sure. I've run 500 iterations and didn't see the test fail again.

@uschindler
Copy link
Contributor

Can we have a test for the assertion method?

@mikemccand
Copy link
Member

Should we add the "double" variant, too?

+1, maybe as follow-on.

Copy link
Member

@mikemccand mikemccand left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you @stefanvodita!

@@ -422,7 +422,9 @@ Build

Other
--------------------
(No changes)

* GITHUB#13720: Add float comparison based on unit of least precision and use it to stop test failures because caused by
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove because?

@@ -654,7 +654,7 @@ private void assertFloatFacetResultsEqual(List<FacetResult> expected, List<Facet

assertEquals(expectedResult.dim, actualResult.dim);
assertArrayEquals(expectedResult.path, actualResult.path);
assertEquals((float) expectedResult.value, (float) actualResult.value, 2e-1);
assertUlpEquals((float) expectedResult.value, (float) actualResult.value, (short) 2);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question @uschindler -- now we have the fun problem of figuring out how many ULPs can be lost due to different order of operations ...

@stefanvodita
Copy link
Contributor Author

Thank you for the feedback! I've added a comparison method for doubles and a test.

@uschindler
Copy link
Contributor

I don't like the last commit because it changes from a assert-like method to a boolean returning method.

Could we not keep the previous method signature and still add a test?

@stefanvodita
Copy link
Contributor Author

I changed it away from an assertion because I liked this more. It makes it so you can assert on floats not being equal or use their equality in a condition, without making an assertion statement much longer. Do you not like it because it's too generic? We could move it to TestUtil or we could provide assertion methods alongside the equality methods, if that helps.

@mikemccand
Copy link
Member

I don't like the last commit because it changes from a assert-like method to a boolean returning method.

I changed it away from an assertion because I liked this more. It makes it so you can assert on floats not being equal or use their equality in a condition, without making an assertion statement much longer. Do you not like it because it's too generic? We could move it to TestUtil or we could provide assertion methods alongside the equality methods, if that helps.

Maybe we could do both?

I.e. add the TestUtil boolean-returning methods (float/doubleEquals) to TestUtil but then also add the sugar methods void assertFloat/DoubleUlpEquals?

Copy link
Member

@mikemccand mikemccand left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @stefanvodita! It's wild how complicated floating point numbers are for computers yet how tantalizingly simple our modern programming languages make them seem.


// Test signed zeros
assertTrue(doubleEquals(0.0d, -0.0d, 0));
assertTrue(floatEquals(0.0f, -0.0f, (short) 0));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fun.


// Test NaNs
assertFalse(doubleEquals(Double.NaN, Double.NaN, Integer.MAX_VALUE));
assertFalse(floatEquals(Float.NaN, Float.NaN, (short) 32767));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could maybe also use Math.nextAfter to add one or two ulps to a random float/double and then assert that float/doubleEquals agrees?

@@ -422,7 +422,9 @@ Build

Other
--------------------
(No changes)

* GITHUB#13720: Add float comparison based on unit of least precision and use it to stop test failures caused by float
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe add IEEE 754 float summation implemented by Java not being commutative or so?

Mathematically float summation is fine :)

@uschindler
Copy link
Contributor

I don't like the last commit because it changes from a assert-like method to a boolean returning method.

I changed it away from an assertion because I liked this more. It makes it so you can assert on floats not being equal or use their equality in a condition, without making an assertion statement much longer. Do you not like it because it's too generic? We could move it to TestUtil or we could provide assertion methods alongside the equality methods, if that helps.

Maybe we could do both?

I.e. add the TestUtil boolean-returning methods (float/doubleEquals) to TestUtil but then also add the sugar methods void assertFloat/DoubleUlpEquals?

That was my intention.

@stefanvodita
Copy link
Contributor Author

I've moved the methods around and, as I was writing more tests, realised I'm not going to be as comprehensive as the originals tests, so I adapted those instead.

Copy link
Member

@mikemccand mikemccand left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @stefanvodita!

(No changes)

* GITHUB#13720: Add float comparison based on unit of least precision and use it to stop test failures caused by float
summation not being commutative in Java's IEEE 754 implementation. (Alex Herbert, Stefan Vodita)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is always not commutative regardless of implementation.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that I think about it, it's not even a commutativity issue. It's an associativity issue.
a + b == b + a
But
(a + b) + c != a + (b + c)
I'll fix the entry.

@@ -864,6 +864,14 @@ public static void assumeNoException(String msg, Exception e) {
RandomizedTest.assumeNoException(msg, e);
}

public static void assertFloatUlpEquals(final float x, final float y, final short maxUlps) {
assertTrue(TestUtil.floatUlpEquals(x, y, maxUlps));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe add a message showing string of both values.
The problem with assert true is that you get no useful output.
This is why I wanted to have the assert methods.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, will do.

Copy link
Contributor

@uschindler uschindler left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks fine.

One place where we should add this is the comparison of Panama vector vs. scalar vector test. The multiplication and summations when using Panama are executed in different order (the reason why we need a hand optimized impl). The test uses a quote large epsilon. It is only unclear to ne how many ulps we need for a dot product! Number of dimensions or maybe only number of splits of dimensions?

@stefanvodita
Copy link
Contributor Author

Thank you Mike and Uwe! I opened a separate issue to replace various epsilon-based equality checks (#13789), since that could be a large enough task, and I'll merge this one.

@stefanvodita stefanvodita merged commit aa86a81 into apache:main Sep 14, 2024
3 checks passed
@uschindler
Copy link
Contributor

👍

@stefanvodita stefanvodita added this to the 9.12.0 milestone Sep 14, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Reproducible test failure in TestTaxonomyFacetAssociations.testFloatSumAssociation -- ULP float issue?
4 participants