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

wasm32 queitens half signalling NaNs in some situations when passing/returning them #96438

Open
beetrees opened this issue Jun 23, 2024 · 4 comments

Comments

@beetrees
Copy link
Contributor

The following IR (compiler explorer):

target triple = "wasm32-unknown-wasi"

define half @from_bits(i16 %x) {
  %res = bitcast i16 %x to half
  ret half %res
}

define i16 @to_bits(half %x) {
    %res = bitcast half %x to i16
    ret i16 %res
}

define i16 @roundtrip() {
    %h = call half @from_bits(i16 64513) ; A bitpattern of a signalling NaN
    %res = call i16 @to_bits(half %h)
    ret i16 %res
}

Is compiled into the following WASM:

from_bits:                              # @from_bits
        local.get       0
        call    __extendhfsf2
        end_function
to_bits:                                # @to_bits
        local.get       0
        call    __truncsfhf2
        i32.const       65535
        i32.and 
        end_function
roundtrip:                              # @roundtrip
        i32.const       64513
        call    from_bits
        call    to_bits
        end_function

rountrip should return 64513 (0xfc01), as all from_bits and to_bits do is bitcast to and from half. However, on WASM it instead returns 65025 (0xfe01), as __extendhfsf2 and __truncsfhf2 both quieten signalling NaNs.

This Rust program, when compiled with rustc 1.81.0-nightly (3cb521a43 2024-06-22) with rustc --target wasm32-wasip1 code.rs and run with wasmtime, demonstrates the issue.

#![feature(f16)]

fn main() {
	assert_eq!(f16::from_bits(0xfc01).to_bits(), 0xfc01);
}

The assertion should succeed, but on WASM it fails.

This solution to this is probably to either:

  1. Change the half ABI to be passed and returned in the low bits of a WASM i32, the same as a LLVM i16. This would match the ABI of half in the __extendhfsf2 and __truncsfhf2 builtins. I noticed that the ABI for 16-bit floats is not specified in the WASM Basic C ABI document, and Clang doesn't support _Float16 on WASM either, so this might be possible with regards to backwards compatibility concerns.
  2. If that's not possible or desirable, convert half to and from f32 losslessly with regards to NaN bits. This would mean either adding extra codegen around the relevant __extendhfsf2 and __truncsfhf2 calls to ensure that signalling NaNs don't get quietened, or adding new builtins that are the same as __extendhfsf2 and __truncsfhf2 but don't quieten signalling NaNs.
@beetrees beetrees changed the title wasm32 queitens half signalling NaNs when passing/returning them wasm32 queitens half signalling NaNs in some situations when passing/returning them Jun 23, 2024
@llvmbot
Copy link
Collaborator

llvmbot commented Jun 23, 2024

@llvm/issue-subscribers-backend-webassembly

Author: None (beetrees)

The following IR ([compiler explorer](https://godbolt.org/z/6cP4Mq9dz)):
target triple = "wasm32-unknown-wasi"

define half @<!-- -->from_bits(i16 %x) {
  %res = bitcast i16 %x to half
  ret half %res
}

define i16 @<!-- -->to_bits(half %x) {
    %res = bitcast half %x to i16
    ret i16 %res
}

define i16 @<!-- -->roundtrip() {
    %h = call half @<!-- -->from_bits(i16 64513) ; A bitpattern of a signalling NaN
    %res = call i16 @<!-- -->to_bits(half %h)
    ret i16 %res
}

Is compiled into the following WASM:

from_bits:                              # @<!-- -->from_bits
        local.get       0
        call    __extendhfsf2
        end_function
to_bits:                                # @<!-- -->to_bits
        local.get       0
        call    __truncsfhf2
        i32.const       65535
        i32.and 
        end_function
roundtrip:                              # @<!-- -->roundtrip
        i32.const       64513
        call    from_bits
        call    to_bits
        end_function

rountrip should return 64513 (0xfc01), as all from_bits and to_bits do is bitcast to and from half. However, on WASM it instead returns 65025 (0xfe01), as __extendhfsf2 and __truncsfhf2 both quieten signalling NaNs.

This Rust program, when compiled with rustc 1.81.0-nightly (3cb521a43 2024-06-22) with rustc --target wasm32-wasip1 code.rs and run with wasmtime, demonstrates the issue.

#![feature(f16)]

fn main() {
	assert_eq!(f16::from_bits(0xfc01).to_bits(), 0xfc01);
}

The assertion should succeed, but on WASM it fails.

This solution to this is probably to either:

  1. Change the half ABI to be passed and returned in the low bits of a WASM i32, the same as a LLVM i16. This would match the ABI of half in the __extendhfsf2 and __truncsfhf2 builtins. I noticed that the ABI for 16-bit floats is not specified in the WASM Basic C ABI document, and Clang doesn't support _Float16 on WASM either, so this might be possible with regards to backwards compatibility concerns.
  2. If that's not possible or desirable, convert half to and from f32 losslessly with regards to NaN bits. This would mean either adding extra codegen around the relevant __extendhfsf2 and __truncsfhf2 calls to ensure that signalling NaNs don't get quietened, or adding new builtins that are the same as __extendhfsf2 and __truncsfhf2 but don't quieten signalling NaNs.

@ppenzin
Copy link

ppenzin commented Jun 30, 2024

Quitting signaling NaNs is expected behavior, Wasm spec explicitly states that. That said, if the value is never represented as a Wasm f32, then it might be fine to not quiet it (until Wasm does add a f16 type, of course).

@beetrees
Copy link
Contributor Author

At the LLVM IR level, signalling NaNs are only quietened when conversions are explicitly done between different float types (or when floating point arithmetic is done; as a side note, to allow more scope for optimisations LLVM IR doesn't guarantee that signalling NaNs are quietened, merely allows it). LLVM IR guarantees that the exact bitpattern of a float is preserved when passing/returning floats to and from functions; this is all stated in the LangRef. WASM itself has similar guarantees. For instance, the equivalent Rust program for f32 (for which 0xff80_0001 is a bitpattern of a signalling NaN)

fn main() {
	assert_eq!(f32::from_bits(0xff80_0001).to_bits(), 0xff80_0001);
}

does not fail the assertion when run on WASM, whereas the f16 version in the first post does fail the assertion. This all means that the f16 version is miscompiled by LLVM and violates the semantics of LLVM IR. The problem here is that the codegen backend is inserting a quietening conversion when no quietening occurs in the LLVM IR. The __extendhfsf2/__truncsfhf2 builtins are behaving correctly, the problem is that the codegen backend is calling the builtins in the first place. If it is decided that the ABI will require converting f16s to f32s, then the codegen backend needs to be changed to do that in a way that does not quieten signalling NaNs.

To put it a different way, this should not quieten signalling NaNs:

define half @func1(i16 %x) {
  %res = bitcast i16 %x to half
  ret half %res
}

Whereas this may quieten signalling NaNs:

define float @func2(i16 %x) {
  %f = bitcast i16 %x to half
  %res = fpext half %f to float
  ret float %res
}

However,, both of the above functions result in the exact same WASM which quietens signalling NaNs (compiler explorer):

func1:                                  # @func1
        local.get       0
        call    __extendhfsf2
        end_function
func2:                                  # @func2
        local.get       0
        call    __extendhfsf2
        end_function

@tlively
Copy link
Collaborator

tlively commented Jul 1, 2024

cc @brendandahl, this may be related to your recent work

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

5 participants