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

Implement optional NaN canonicalization mode #19

Open
NikVolf opened this issue Jan 26, 2018 · 7 comments · May be fixed by #1236
Open

Implement optional NaN canonicalization mode #19

NikVolf opened this issue Jan 26, 2018 · 7 comments · May be fixed by #1236

Comments

@NikVolf
Copy link
Contributor

NikVolf commented Jan 26, 2018

No description provided.

@recmo
Copy link

recmo commented Sep 27, 2018

I'm assuming this issue tries to solve the non-determinism problem like so: WebAssembly/design#582 (comment)

For NaN bits, it'd be straightforward for a preprocessor to inject NaN canonicalization code after every floating-point computation, and that would suffice.

Doing this after every operation might be excessive, NaNs with different bit patterns still mostly behave the same. It should be enough to canonicalize in the few places where they don't: reinterpret, store, etc. There is a list here: https://github.com/WebAssembly/design/blob/master/Rationale.md#nan-bit-pattern-nondeterminism

Although it would be fun to write a contract that pays out when someone mines it with ARM instead of x86. Might be good for an ARM node bounty! 🤡

@NikVolf
Copy link
Contributor Author

NikVolf commented Sep 28, 2018

Yep, the motivation is to prevent non-determinism still having some base2 floats.

it's not very important atm, since we can ban floats on the embedder and/or use soft floats in the user space

But still might be useful!

@pepyakin
Copy link
Collaborator

Although it would be fun to write a contract that pays out when someone mines it with ARM instead of x86. Might be good for an ARM node bounty! 🤡

Interesting idea, but how would you actually proof that? : D

@MaxGraey
Copy link

MaxGraey commented Feb 7, 2019

I don't think NaN is only non-determinism part of float points in WebAssembly. Operations like sqrt and inverse sqrt (rsqrt) implement differently even on x86 by some vendors. ARM for example flush all subnormal values to zero (FTZ and DAZ modes). Conversion from floats to integers also behave differently on hardware and by compiler's mode.

@sunfishcode
Copy link

Wasm's sqrt really is deterministic; while other languages can use x87 which rounds things like sqrt differently in some cases, wasm doesn't permit this, and this is specifically tested for in the spec testsuite.

The ARM subnormal issue is only on 32-bit ARM, and only NEON, not VFP, so wasm's current scalar operations aren't affected. This is a potential concern for the SIMD proposal, however.

WebAssembly also fully specifies the behavior of conversion from floats to integers, and the behavior in all the cases where hardware is known to differ is covered in the testsuite too.

So while wasm could add rsqrt approximations or other things in the future, and SIMD will have to cope with 32-bit ARM NEON, in the current spec, NaN bit patterns really are the only floating-point nondeterminism.

@MaxGraey
Copy link

MaxGraey commented Feb 7, 2019

@sunfishcode Thank you for so detail clarifying! Now you had dispelled any doubts)

@Robbepop Robbepop changed the title Optional NaN canonicalization mode? Implement optional NaN canonicalization mode Dec 8, 2023
@Robbepop
Copy link
Member

Robbepop commented Oct 11, 2024

I thought about how this could be implemented efficiently for Wasmi.

Design

  1. Add Config::canonicalize_nans(flag: bool) to wasmi::Config.
    • This allows to enable or disable this feature on a per-wasmi::Engine basis.
    • Wasmtime API
  2. Add wasmi_ir::Instruction variants for the following float instructions that require NaN canonicalization:
    • {f32, f64}.add
    • {f32, f64}.sub
    • {f32, f64}.mul
    • {f32, f64}.div
    • {f32, f64}.min
    • {f32, f64}.max
    • {f32, f64}.copysign
    • {f32, f64}.neg
    • {f32, f64}.sqrt
    • Note:
      • NaN canonicalization needed only for intermediate computations: {i32, i64}.trunc_f{32,64}_{s,u}
        • No new variants required since this could be implemented in all configs.
      • No NaN canonicalization needed for:
        • {f32,f64}.cmp (e.g. f32.eq, f64.lt)
        • {f32,f64}.branch_cmp (fused instructions)
      • {f32,f64}.{load,store} instructions do not need NaN canonicalization since they do not perform float operations that could result in different NaN bit patterns on different hardware.
      • No NaN canonicalization needed for {f32,f64}.{abs,ceil,floor,trunc} because they do not manipulate the input bit patterns and simply propagate them.
  3. During Wasmi translation the new variants are emitted instead of the existing ones if NaN canonicalition is enabled.
  4. The Wasmi executor performs NaN canonicalization for the new Instruction variants.
  5. The Wasmi translator needs to take optional NaN canonicalization into account when propagating constants.

This way no performance is lost when NaN canonicalization is disabled but users who require the additional determinism can enable it with minor runtime costs since there won't be runtime checks during execution.

NaN canonicalization is a post-processing of a f32 or f64 value as simple as:

pub trait CanonicalizeNan {
    fn canonicalize_nan(self) -> Self;
}

impl CanonicalizeNan for f32 {
    fn canonicalize_nan(self) -> Self {
        if self.is_nan() {
            // Note: This pattern ensures that the sign bit can be either 0 or 1,
            //       the exponent bits are all set to 1 (indicating an infinity or NaN),
            //       and the fraction (mantissa) bits are all zero, except the most significant bit
            //       of the fraction is set to 1 to indicate a quiet NaN.
            let canonicalized = Self::from_bits(0x7FC00000_u32);
            debug_asssert!(canonicalized.is_nan());
            return canonicalized
        }
        self
    }
}

impl CanonicalizeNan for f64 {
    fn canonicalize_nan(self) -> Self {
        if self.is_nan() {
            // Note: This pattern ensures that the sign bit can be either 0 or 1,
            //       the exponent bits are all set to 1 (indicating an infinity or NaN),
            //       and the fraction (mantissa) bits are all zero, except the most significant bit
            //       of the fraction is set to 1 to indicate a quiet NaN.
            let canonicalized = Self::from_bits(0x7FF8000000000000);
            debug_asssert!(canonicalized.is_nan());
            return canonicalized
        }
        self
    }
}

@Robbepop Robbepop linked a pull request Oct 14, 2024 that will close this issue
4 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

6 participants