Crystal: Support half-precision floating point (Float16)

Created on 24 Apr 2020  路  9Comments  路  Source: crystal-lang/crystal

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.

feature discussion lang topicnumeric

Most helpful comment

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.

All 9 comments

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.

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.

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

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.

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.

@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 :)

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.

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:

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.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

asterite picture asterite  路  3Comments

cjgajard picture cjgajard  路  3Comments

asterite picture asterite  路  3Comments

Papierkorb picture Papierkorb  路  3Comments

lbguilherme picture lbguilherme  路  3Comments