Skip to content

Navigation Menu

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

BUG: Fix rounding of denormals in double and float to half casts … #12722

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

Merged
merged 6 commits into from
Jan 24, 2019

Conversation

seberg
Copy link
Member

@seberg seberg commented Jan 12, 2019

Previously the significand was shifted right to align denormals of
different magnitude. This loses some bits that can make a difference
for rounding. This is fixed:

  1. For floats, by inspecting the original last bits when this may
    make a difference (should happen rarely)
  2. For doubles by shifting the bits left to align the denromals and
    thus not lose the lowest orginal bits.

I am not sure if the double branch is not being a bit too smart and it is nicer to just also use the || logic from above, since it uses different constants compared to the normal branch because of this.

I do not know if there may be nicer ways with larger code changes, but I could not think of one.

This closes gh-12721

Previously the significand was shifted right to align denormals of
different magnitude. This loses some bits that can make a difference
for rounding. This is fixed:
 1. For floats, by inspecting the original last bits when this may
    make a difference (should happen rarely)
 2. For doubles by shifting the bits left to align the denromals and
    thus not lose the lowest orginal bits.
The test assumes that half to even is active, if this is ever
changed or allowed to change, the test will fail.
}
#else
d_sig += 0x0000020000000000ULL;
d_sig += 0x0000e00000000000ULL;
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this right? I'm not sure I'm following the logic, but it seems you're only adding a single bit before, and now you're adding something with 3 bits set. It looks like you changed 2 to 2 * 70 = 0xe0, but shouldn't it be changed to 2 << 7 = 0x100 as above? I.e., 0x0001000000000000ULL

Copy link
Member Author

Choose a reason for hiding this comment

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

wow, yes you are right :(, that is embaressing.

Copy link
Member Author

Choose a reason for hiding this comment

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

Ah, that was some left-over in a code path that is never used (and thus cannot be tested), we could remove that completely in principle.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes, I guess we always do proper round-to-even now.

@@ -69,6 +69,58 @@ def test_half_conversions(self):
j = np.array(i_f16, dtype=int)
assert_equal(i_int, j)

@pytest.mark.parametrize("offset", [None, "up", "down"])
@pytest.mark.parametrize("shift", [None, "up", "down"])
@pytest.mark.parametrize("float_t", [np.float32,])
Copy link
Contributor

Choose a reason for hiding this comment

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

Also test with np.float64? Otherwise why parametrize it?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah, forgot to read after trying, hopefully the float64s fail ;) because of that 3 bit thing above.

Copy link
Contributor

@mhvk mhvk left a comment

Choose a reason for hiding this comment

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

One possibly significant comment about the changes.

If this is wrong, then we need more tests, since the tests we have all passed...

*/
if ((f_sig&0x00003fffu) != 0x00001000u) {
if (((f_sig&0x00003fffu) != 0x00001000u) || (f&0x008ffu)) {
Copy link
Member

Choose a reason for hiding this comment

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

Where did 008ff come from? And why are you giving only 5 hex digits?

Copy link
Member Author

Choose a reason for hiding this comment

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

Jeesh, I messed up, probably a typo, it should have been 7ff and of course full digits. 7ff for the last 11 bits, that would otherwise be gone.

Copy link
Member Author

Choose a reason for hiding this comment

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

Dang, the tests probably do not catch this, because it is in the wrong bits... maybe should add a test for this part more specifically.

/* Handle rounding by adding 1 to the bit beyond half precision */
#if NPY_HALF_ROUND_TIES_TO_EVEN
/*
* If the last bit in the half significand is 0 (already even), and
* the remaining bit pattern is 1000...0, then we do not add one
* to the bit after the half significand. In all other cases, we do.
*/
if ((d_sig&0x000007ffffffffffULL) != 0x0000020000000000ULL) {
d_sig += 0x0000020000000000ULL;
if ((d_sig&0x003fffffffffffffULL) != 0x0010000000000000ULL) {
Copy link
Member

Choose a reason for hiding this comment

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

Might be more readable to express these as (1 << n) - 1 and (1 << n) for some n. Fine to leave as it too.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah, should change it everywhere then though probably.

Copy link
Member

Choose a reason for hiding this comment

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

Given you got this wrong at least twice in this patch, I think it might be wise to put this patch on hold, and make this change first in a standalone MAINT PR. A simple npy_uint64 n_low_bits(int n) function would probably be helpful here.

Copy link
Member Author

Choose a reason for hiding this comment

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

Probably a good idea, these bit patterns are a error prone :/, so I may do that but not super soon maybe. For now I had already fixed it up and added a test for the one actual problem.

Copy link
Member

Choose a reason for hiding this comment

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

spaces needed around &

Copy link
Member

Choose a reason for hiding this comment

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

Without spaces is par for the course for this file - I don't think we need to block this PR on that.

The one code path cannot be used. The other must have been a typo, but
is a valid bug, a new test for it in the next commit.
The first test only tested the off by one, this one specifically tests
that all bits are used to decide if "round to nearest even" should
be used, in the example of rounding towards 0.
@eric-wieser
Copy link
Member

Would having #10741 make testing easier here?

@DickJC123
Copy link

I looked at the change made to the following code and it appears correct.
https://github.com/seberg/numpy/blob/460455863c3128dcc2acbe1af7874f3791612955/numpy/core/src/npymath/halffloat.c#L307-L316

* significand. This changes the constant compared to normal branch.
*/
assert(d_exp - 998 >= 0);
d_sig <<= (d_exp - 998);
Copy link
Member

Choose a reason for hiding this comment

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

If you can shed any light in a comment on on where 998 comes from, that would be appreciated.

@seberg
Copy link
Member Author

seberg commented Jan 14, 2019

Actually, first had ffff since there area already plenty of known 0 bits around, then (badly) changed it to 11 bits. Actually, might really switch over to the minimal fix for 64bits. This seems like unnecessarily trying to be smart.

@eric-wieser I guess float to integer ratio is likely nice more generally. I am not sure how it would help here specifically (unless maybe to not rely on float16->float32/64 being correct). But we may want to reactivate that PR in any case, since python has long moved on and the maintenance burden seems not high?

@mattip
Copy link
Member

mattip commented Jan 15, 2019

LGTM. Still needs release note

@seberg
Copy link
Member Author

seberg commented Jan 15, 2019

Hmm, I thought/think release notes make sense, because it is a subtle change (we can still remove it anyway). But would do we want to backport it, and so that it should be already in 1.16.1 or both?

@seberg
Copy link
Member Author

seberg commented Jan 15, 2019

Plus, in that case I should maybe look into giving it a nice backportable merge base.

@charris
Copy link
Member

charris commented Jan 15, 2019

But would do we want to backport it, and so that it should be already in 1.16.1 or both?

Needs to be decided for the LTS branch, we don't yet have a policy for that. I'm inclined to go for duplicate entries because the LTS branch is the end of Python 2.7 support, so a bit more separate than usual.

@charris charris added the 09 - Backport-Candidate PRs tagged should be backported label Jan 15, 2019
@charris charris added this to the 1.16.1 release milestone Jan 21, 2019
@charris
Copy link
Member

charris commented Jan 23, 2019

Checking if reviewers all think this is ready.

@seberg
Copy link
Member Author

seberg commented Jan 23, 2019

Sorry, got a bit side tracked, I don't quite remember myself if I thought it was. I think I was considering doing the simple fix in both versions still. I doubt it makes a noticable speed difference anyway.

@mhvk
Copy link
Contributor

mhvk commented Jan 23, 2019

It seems to me this is ready - there was a sensible question about a handy helper macro, but probably best to put it in: at some level, the tests are what really counts here, and those now pass.

@charris
Copy link
Member

charris commented Jan 24, 2019

OK, no more feedback, so putting this in. Thanks Sebastian.

@charris charris merged commit 7e3d558 into numpy:master Jan 24, 2019
charris pushed a commit to charris/numpy that referenced this pull request Jan 24, 2019
…umpy#12722)

* BUG: Fix rounding of denormals in double and float to half casts

Previously the significand was shifted right to align denormals of
different magnitude. This loses some bits that can make a difference
for rounding. This is fixed:
 1. For floats, by inspecting the original last bits when this may
    make a difference (should happen rarely)
 2. For doubles by shifting the bits left to align the denromals and
    thus not lose the lowest orginal bits.

* TST: Test half denormal rounding

The test assumes that half to even is active, if this is ever
changed or allowed to change, the test will fail.

* Fixup: Fixup for halffloat.c

The one code path cannot be used. The other must have been a typo, but
is a valid bug, a new test for it in the next commit.

* TST: half casting lower bits are not lost for denormal results

The first test only tested the off by one, this one specifically tests
that all bits are used to decide if "round to nearest even" should
be used, in the example of rounding towards 0.

* TST: Test not just denormals but all positive finite float16s

* DOC: Add lots of comments and add a short release note.
@charris charris added component: numpy._core and removed 09 - Backport-Candidate PRs tagged should be backported 56 - Needs Release Note. Needs an entry in doc/release/upcoming_changes labels Jan 24, 2019
@charris charris removed this from the 1.16.1 release milestone Jan 24, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

np.array(..., dtype=np.float32).astype(np.float16) doesn't handle denorms properly
6 participants
Morty Proxy This is a proxified and sanitized view of the page, visit original site.