Skip to content

Half: An IEEE 754 compliant float16 type #37630

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 14 commits into from
Jun 25, 2020
Merged

Conversation

pgovind
Copy link

@pgovind pgovind commented Jun 9, 2020

Fixes #936.

A lot of this code is a port of what have in corefxlab.

@ghost
Copy link

ghost commented Jun 9, 2020

Tagging subscribers to this area: @tannergooding
Notify danmosemsft if you want to be subscribed.

@Dotnet-GitSync-Bot
Copy link
Collaborator

Note regarding the new-api-needs-documentation label:

This serves as a reminder for when your PR is modifying a ref *.cs file and adding/modifying public APIs, to please make sure the API implementation in the src *.cs file is documented with triple slash comments, so the PR reviewers can sign off that change.

// Well-defined and commonly used values
//

public static readonly Half Epsilon = new Half(EpsilonBits); // 5.9604645E-08
Copy link
Author

Choose a reason for hiding this comment

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

Need to document the APIs

@pgovind pgovind closed this Jun 9, 2020
@pgovind
Copy link
Author

pgovind commented Jun 9, 2020

Closing this for a bit. I need to implement IConvertible too and then this will be ready for review

@tannergooding
Copy link
Member

I need to implement IConvertible too and then this will be ready for review

`IConvertible wasn't part of the reviewed/approved surface. It looks like all of the methods were removed from the proposal, but not the actual interface.

Implementing IConvertible is tricky because it is an interface and only supports a limited subset of types. So there isn't a proper TypeCode to support Half nor are there methods to successfully convert to Half, instead only a handful of methods convert from Half would be supported, the vast majority of which would presently give an InvalidCastException today (giving only two or three methods that actually succeed).

@pgovind
Copy link
Author

pgovind commented Jun 9, 2020

Implementing IConvertible is tricky because it is an interface and only supports a limited subset of types. So there isn't a proper TypeCode to support Half nor are there methods to successfully convert to Half, instead only a handful of methods convert from Half would be supported, the vast majority of which would presently give an InvalidCastException today (giving only two or three methods that actually succeed).

Ah ok. I went through that exact same thought process, but assumed that IConvertible was added to the interface list after consideration in the API review. Cool, I'll clean up the comments on the PR and open it back up

@tannergooding
Copy link
Member

Just noting, I believe you want to reopen the PR before pushing any new commits. I believe GitHub won't allow you to reopen the PR otherwise (although that may have changed).

@pgovind pgovind reopened this Jun 9, 2020
@tannergooding tannergooding changed the title Half: An IEEE 768 compliant float16 type Half: An IEEE 754 compliant float16 type Jun 9, 2020
Comment on lines 332 to 342
bool ret = false;
if (float.TryParse(s, style, provider, out float floatResult))
{
result = (Half)floatResult;
ret = true;
}
else
{
result = default(Half);
}
return ret;
Copy link
Member

@danmoseley danmoseley Jun 9, 2020

Choose a reason for hiding this comment

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

Suggested change
bool ret = false;
if (float.TryParse(s, style, provider, out float floatResult))
{
result = (Half)floatResult;
ret = true;
}
else
{
result = default(Half);
}
return ret;
bool ret = float.TryParse(s, style, provider, out float floatResult)
result = ret ? (Half)floatResult : default(Half);
return ret;

? assume it makes the same code

Copy link
Member

Choose a reason for hiding this comment

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

Rather than calling into float.TryParse we should add a Number.TryParseHalf method. The method would ultimately be the same as https://source.dot.net/#System.Private.CoreLib/Number.Parsing.cs,1792 but it would return Half.PositiveInfinity, Half.NegativeInfinity, Half.NaN, and use a new HalfNumberBufferLength constant (this should be 21 if I did my math correctly).

There would then also be a new NumberToHalf method which likewise mirrors https://source.dot.net/#System.Private.CoreLib/Number.Parsing.cs,2002, but passes in FloatingPointInfo.Half. The rest of the logic should already be correct and should not require any touchups.

Similar should be true for the formatting code paths.

/// </summary>
[Serializable]
[StructLayout(LayoutKind.Sequential)]
public readonly struct Half : IComparable, IFormattable, IComparable<Half>, IEquatable<Half>, ISpanFormattable
Copy link
Member

Choose a reason for hiding this comment

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

@TamarChristinaArm, I was llooking at the ARM calling convention for half-precision floating point and it looks like its treated as a ushort rather than as a floating-point type for the purposes of argument passing/returning, is that correct?

A Half-precision Floating Point Type is returned in the least significant 16 bits of r0.
If the argument is a Half-precision Floating Point Type its size is set to 4 bytes as if it had been copied to the least significant bits of a 32-bit register and the remaining bits filled with unspecified values.

There also seems to be a section on variants of the calling convention which details:

A half precision floating point type is passed as if it were loaded from its memory format into the least significant 16 bits of a single precision register.

Copy link
Contributor

Choose a reason for hiding this comment

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

its treated as a ushort rather than as a floating-point type for the purposes of argument passing/returning, is that correct?

@tannergooding passing it in GPR is only for the base variant, i.e. soft or softfp float ABI where you don't have hardware support for floats. When you have hardware support and use the hard float ABI it uses the VFP calling convention which passes it in the s register as the second part you quoted.

I'm guessing coreclr only supports hard? in which case the VFP one is what you want :).

Copy link
Member

Choose a reason for hiding this comment

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

@CarolEidt, @sandreenko, @jkotas

Given the recent changes around retyping, what would the recommended way be to support this in the JIT/VM?

We could simply block this from interop and AOT scenarios for .NET 5 and address it in .NET 6...
But, if the changes aren't terribly difficult, it would be nice to also ensure it works in those scenarios.

As per the above, it looks like no changes are required for x86, x64, or System V as they have defined no ABI semantics for a half-precision floating-point type so any changes would only be required for ARM32/ARM64 where the ABI specifies it should be passed in the lowest 16-bits of a floating-point register.

Copy link
Contributor

@sandreenko sandreenko Jun 10, 2020

Choose a reason for hiding this comment

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

@tannergooding we need to know the type when we call and when we return, and without retyping all logic is located in Compiler::getReturnTypeForStruct, so necessary changes would be localized in the JIT inside this function and functions that are called by it.

I think it could be done similar to #37499 by adding a new type into VM and saying that we are returning it as a float it JIT, lower will add bit casts as necessary.

Do you expect methods that are using private readonly ushort m_value; directly, like:

        public static bool IsPositiveInfinity(Half value)
        {
            return value.m_value == PositiveInfinityBits;
        }

would get value as float, but access its field as a short that would cause a memory spill?

Copy link
Member

Choose a reason for hiding this comment

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

Thanks for the explanation!

would get value as float, but access its field as a short that would cause a memory spill?

We have similar logic for System.Single and Siystem.Double: https://source.dot.net/#System.Private.CoreLib/Single.cs,79
Prior to .NET 5 it did cause a memory spill but I believe it now just emits a movd to directly transition between the floating-point and integer registers.

#11413 tracks the more general issue there and is something we could workaround for the type on ARM32/ARM64 as well.
I don't believe it will be a problem for x86/x64 as none of the architectures we support define an ABI for Half and so passing it around as an integer is as expected.

Copy link
Contributor

Choose a reason for hiding this comment

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

So ... I could probably look this up, but I'm guessing you already know, so I'll ask here: Is Half considered a floating point type for the purposes of HFA recognition? If so, I think there might be a bit more work in the JIT to handle non-singletons correctly.

Copy link
Member

Choose a reason for hiding this comment

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

The short answer is yes. The actual handling of ARM32 vs ARM64 appears to differ slightly and the docs on ARM64 appear to be quite a bit clearer.

The ARM64 specs are apparently on GitHub now 😄: https://github.com/ARM-software/abi-aa/releases
See also: https://developer.arm.com/architectures/system-architectures/software-standards/abi

/// </summary>
[Serializable]
[StructLayout(LayoutKind.Sequential)]
public readonly struct Half : IComparable, IFormattable, IComparable<Half>, IEquatable<Half>, ISpanFormattable
Copy link
Member

Choose a reason for hiding this comment

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

I also checked the System V ABI and x86/x64 Windows ABIs and did not see any notes on special handling required for a half or fp16 type.

[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static unsafe short HalfToInt16Bits(Half value)
{
return *((short*)&value);
Copy link
Member

Choose a reason for hiding this comment

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

@tannergooding
Copy link
Member

LGTM now. Just a few nits and one fix left.

@tannergooding
Copy link
Member

A few of the test "expected" strings need to be updated to account for Half differences.

  • 4568 should be 4570
  • 6312 should be 6310
  • 65504 should be 65500
  • 5.9604645E-08 should be 6E-08

Prashanth Govindarajan added 2 commits June 23, 2020 11:37
@pgovind
Copy link
Author

pgovind commented Jun 25, 2020

@jkotas I think I need signoff from you too. Or GH won't let me merge

@jkotas jkotas merged commit 6a7c3be into dotnet:master Jun 25, 2020
@danmoseley
Copy link
Member

Nice work @pgovind ..glad to get this into 5.0

pgovind pushed a commit to pgovind/runtime that referenced this pull request Jun 25, 2020
Anipik pushed a commit that referenced this pull request Jun 26, 2020
Comment on lines +2200 to +2205
        public static System.Half Epsilon { get { throw null; } }
        public static System.Half MaxValue { get { throw null; } }
        public static System.Half MinValue { get { throw null; } }
        public static System.Half NaN { get { throw null; } }
        public static System.Half NegativeInfinity { get { throw null; } }
        public static System.Half PositiveInfinity { get { throw null; } }
Copy link
Member

Choose a reason for hiding this comment

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

Shouldn't these values be constants?

Copy link
Member

Choose a reason for hiding this comment

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

Discussed in #37630 (comment)

Copy link
Member

Choose a reason for hiding this comment

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

They can't be actual constants either because constant can only be a limited set of types in IL and in C#.

@ghost ghost locked as resolved and limited conversation to collaborators Dec 8, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

API Proposal: Add System.Numerics.Half 16 bit floating point number conforming to IEEE 754:2008 binary16
10 participants