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

Support half-precision floating point (Float16) #9172

Open
albertorestifo opened this issue Apr 24, 2020 · 9 comments
Open

Support half-precision floating point (Float16) #9172

albertorestifo opened this issue Apr 24, 2020 · 9 comments

Comments

@albertorestifo
Copy link

While working on an implementation of a CBOR encoder/decoder I hit a roadblock, as the protocol allows for the transmission of half-precision floating point numbers, but crystal is lacking a Float16 type.

It would be nice to have a Float16 type in Crystal.

@jhass
Copy link
Member

jhass commented Apr 24, 2020

Meanwhile it's probably possible to encode a small enough Float32 as Float16 into a UInt16 with casting it into a UInt32 first and then doing the right dance of masks and shifts.

@albertorestifo
Copy link
Author

I tried, but it was very difficult to handle all cases properly (like NaN and Infinity). Most likely I was also doing something wrong as I'm not very familiar with bit operations, I based it off this gist.

@RX14
Copy link
Contributor

RX14 commented Apr 25, 2020

This would need compiler support, we definitely won't find time for this before 1.0.

@RX14
Copy link
Contributor

RX14 commented Apr 25, 2020

Actually, taking for example rust, there's no Float16 type, but there's a crate which can convert back and forth from Float32. I think a shard to do that would be the best option, and all the arithmetic is performed on Float32. I'd like to try that approach first before adding anything to the compiler or stdlib.

@albertorestifo
Copy link
Author

albertorestifo commented Apr 25, 2020

That's a very good idea!

Looking at the rust crate, I have absolutely not idea what is going on here, however this function might be a very good starting point for a shard.
I'll have a look and see if I can pull something good off it.

@RX14
Copy link
Contributor

RX14 commented Apr 25, 2020

@albertorestifo the first function is a function which uses the VCVTPH2PS X86_64 instruction to speed up the conversion from f16 to f32. That would be done in crystal with some custom assembly, but getting something working first is good, before you make it fast :)

@albertorestifo
Copy link
Author

Here it is a first basic implementation as part of the CBOR library I'm working on.

All the CBOR Float16 tests provided in the RFC are now passing, giving me a reasonable certainty that the conversion from the Rust code was correct.

I'll now focus on finishing the CBOR library before properly extracting this into a Float16 library.

@ysbaddaden
Copy link
Contributor

LLVM exposes intrinsics for f16 conversion from/to f32 and f64 (f16 is actually an u16):
https://llvm.org/docs/LangRef.html#half-precision-floating-point-intrinsics

Also see:

@asterite
Copy link
Member

asterite commented May 5, 2020

I guess we can add it to the standard library, given that it's an LLVM intrinsic.

I was thinking of an implementation like this:

lib LibInstrinsics
  fun f16tof32 = "llvm.convert.from.fp16.f32"(Int16) : Float32
  fun f16tof64 = "llvm.convert.from.fp16.f64"(Int16) : Float64

  fun f32tof16 = "llvm.convert.to.fp16.f32"(Float32) : Int16
  fun f64tof16 = "llvm.convert.to.fp16.f64"(Float64) : Int16
end

@[Extern]
struct Float16
  @value : Int16

  def self.new(value : Float32)
    new LibIntrinsics.f32tof16(value)
  end

  def self.new(value : Float64)
    new LibIntrinsics.f64tof16(value)
  end

  private def initialize(@value : Int16)
  end

  def to_f32
    LibIntrinsics.f16tof32(@value)
  end

  def to_f64
    LibIntrinsics.f16tof64(@value)
  end

  def to_f
    to_f64
  end
end

The idea is that Float16 only provides conversions to and from Float32 and Float64. We could provide all of the arithmetic methods, but I think that would be very inefficient, to convert all the time from and to Float16 and Float32/Float64.

With this API, if you have a C function that returns a Float16, because it's marked as @[Extern], you simply use it:

lib LibSome
  fun give_me_an_f16 : Float16

  fun accept_f16(value : Float16)
end

# Ask a Float16 and immediately go to safe ground: Float64
f = LibSome.give_me_an_f16.to_f64

# You do the math you need with f as a Float64

# Then you convert it to Float16 at the end:
LibSome.accept_f16(Float16.new(f))

We could also add to_f16 to Float32 and Float64 to it's a bit more convenient than doing Float16.new(...).

I also tried this code and it worked well:

i1 = 0b0_00000_0000000001_i16
p! LibIntrinsics.f16tof64(i1)
p! i1.unsafe_as(Float16).to_f64

i2 = 0b0_00000_1111111111_i16
p LibIntrinsics.f16tof64(i2)
p! i2.unsafe_as(Float16).to_f64

i3 = 0b0_11110_1111111111_i16
p LibIntrinsics.f16tof64(i3)
p! i3.unsafe_as(Float16).to_f64

The above are some examples found in Wikipedia.

Let me know if you think this is fine, I can send a PR.

@asterite asterite mentioned this issue May 6, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

6 participants