Skip to content

What are the intended semantics for simd_fmin/fmax and simd_reduce_min/max vs signaling NaN? #153395

Closed

Description

@RalfJung

After a bunch of discussion, the intended semantics of min/max on scalar floats have been clarified to be exactly what we have documented for years already: they behave like the IEEE 754-2019 min/maximumNumber operation, except that signed zeros are ordered non-deterministically. (Equivalently: they behave like IEEE 754-2008 min/maxNum except that when one input is a signed NaN and the other is a number, the number is returned.) This corresponds to llvm.min/maximumnum nsz in LLVM (as clarified by llvm/llvm-project#172012). #153343 fixes codegen to actually implement those semantics.

For SIMD, we have a bunch of intrinsics where the same question arises: simd_fmin and simd_fmax as well as simd_reduce_min and simd_reduce_max which also support floats. They are documented to use "IEEE-754 maxNum", which I presume refers to the IEEE 754-2008 operation. This is strangely inconsistent with the scalar operation we expose. It is also not what const-eval and Miri do; they implement the same semantics for SIMD min/max as they do for scalar min/max. The only difference is in the behavior for signaling NaNs, which apparently none of the test suites are covering. But obviously, Miri having different behavior than rustc is bad.

So... should we change the SIMD intrinsics to match what the scalar operations do? I think we should. However, LLVM does not currently seem to have a version of llvm.vector.reduce.* that uses min/maximumnum semantics (Cc @nikic), so currently it's tricky to implement this correctly. Also, we have to be careful about stdarch using these intrinsics to implement vendor intrinsics -- there are some uses of all of these intrinsics in stdarch (Cc @folkertdev @Amanieu ). This also affects portable-simd but since that is unstable that seems fine, and AFAIK there the design goal generally is to match the scalar standard library (Cc @workingjubilee @calebzulawski ).

Activity

  1. added
    needs-triageThis issue may need triage. Remove when done. See docs forge.rust-lang.org/release/issue-triaging
    on Mar 4, 2026
  2. Amanieu commented on Mar 4, 2026

    @Amanieu
    Member

    It would probably be useful to expose all the different variants of min/max as intrinsics so that stdarch can select the appropriate one for various platform-specific intrinsics. Specifically we would want:

    • 2019 minimumNumber without nsz for RISC-V
    • 2008 minNum without nsz for ARM.
    • minimum/maximum without nsz for ARM.

    I think we should just have intrinsics for the full set of {minimum, minNum, minimumNumber} x {with nsz, without nsz}. Whether we expose those in the public API is a separate matter.

  3. RalfJung commented on Mar 4, 2026

    @RalfJung
    MemberAuthor

    I think we should just have intrinsics for the full set of {minimum, minNum, minimumNumber} x {with nsz, without nsz}.

    That's 6 variants for 4 intrinsics, making it 24 intrinsics total. I don't think that's worth it.

    stdarch can only really use simd_* intrinsics when there's consistent behavior across targets and LLVM provides a "portable" intrinsic for that. If we end up with as many intrinsics as we have targets, we gained nothing over directly calling LLVM operations from stdarch.

    2008 minNum without nsz for ARM.

    Note that simd_fmin/fmax as currently documented do have the nsz behavior: IEEE 754-2008 says that signed zero behavior is non-deterministic. So stdarch using those operations is already sketchy.

    Also note that LLVM's minnum without nsz still has non-determinism: when one input is an SNaN, the result may be a NaN or the other input. I doubt that's the behavior you want for ARM. So I don't think minnum can ever be used for any stdarch intrinsic.

  4. added
    T-libsRelevant to the library team, which will review and decide on the PR/issue.
    A-floating-pointArea: Floating point numbers and arithmetic
    C-discussionCategory: Discussion or questions that doesn't represent real issues.
    A-SIMDArea: SIMD (Single Instruction Multiple Data)
    and removed
    needs-triageThis issue may need triage. Remove when done. See docs forge.rust-lang.org/release/issue-triaging
    on Mar 4, 2026
  5. sayantn commented on Mar 4, 2026

    @sayantn
    Contributor

    We can use a const enum parameter to select which variant of simd_fmax you want, that'll reduce the number of distinct intrinsic needed.

  6. RalfJung commented on Mar 5, 2026

    @RalfJung
    MemberAuthor

    We could, but that's still a lot of boilerplate and I don't see the point of doing that. When behavior differs across targets, we typically use target-specific intrinsics in stdarch; we do not try to add enough intrinsics so that every target's behavior can be directly accessed on all targets.

  7. RalfJung commented on Mar 5, 2026

    @RalfJung
    MemberAuthor

    Also, as already mentioned above, none of the LLVM intrinsics is actually suitable for targets which guarantee that SNaN inputs lead to SNaN outputs but QNaN inputs return the other input if that is a number -- and I assume ARM guarantees that. LLVM is always permitted to treat an SNaN as if it was a QNaN. So, even with a whole family of intrinsics, it'd still be wrong to use them to encode ARM vendor intrinsics, unless we're willing to have the intrinsics diverge from the actual instruction like this.

    (FWIW, using the intrinsics always diverges from the actual instruction when NaNs are returned. The SIMD intrinsics are subject to our usual NaN rules. But that just affects the sign and payload of a NaN, not whether a NaN is returned in the first place, so that may be an acceptable tradeoff for stdarch intrinsics.)

  8. RalfJung commented on Mar 10, 2026

    @RalfJung
    MemberAuthor

    There are three architectures where stdarch currently uses simd_fmin/fmax:

  9. folkertdev commented on Mar 10, 2026

    @folkertdev
    Contributor

    For s390x, based on https://www.ibm.com/docs/en/cpp-oel-zos/2.0.0?topic=arithmetic-vec-max-vector-maximum I believe the intent is to match what the scalar operation would do. cc @uweigand

  10. uweigand commented on Mar 10, 2026

    @uweigand
    Contributor

    For s390x, based on https://www.ibm.com/docs/en/cpp-oel-zos/2.0.0?topic=arithmetic-vec-max-vector-maximum I believe the intent is to match what the scalar operation would do. cc @uweigand

    That documentation is a bit unclear unfortunately. What actually happens is that these implement the IEEE 754-2008 min/maxNum exactly (using our HW instructions), which the exception that for float on z13, we don't yet have the HW instruction so we'll fall back to an emulation that does not precisely match the IEEE semantics.

    In general, for Rust I would certainly prefer that the simd and scalar versions match the same semantics.

    As background on our HW support, we have VECTOR FP MINIMUM/MAXIMUM instructions - exposed to the front end via the llvm.s390.vfminsb and related intrinsics - that provide multiple modes of operations:

    • mode M=0 matches IEEE 754-2008 minNum
    • mode M=1 matches IEEE 754-2019 minimum
    • mode M=4 mostly matches IEEE 754-2019 minimumNumber

    The only difference between our mode M=4 and minimumNumber is that M=4 may return an input SNaN as-is, without changing it to the corresponding QNaN. We do not have a mode that matches minimumNumber precisely.

    As of the recently merged llvm/llvm-project#184595, we anyway map the llvm.minimumnum intrinsic to M=4, given that the LLVM semantics for signaling NaNs explicitly allow this behavior.

    We map llvm.minimum to mode M=1 (i.e IEEE 754-2019 minimum).

    For llvm.minnum the LLVM semantics are vague enough that they can be satisfied by either M=0 or M=4. We currently map to M=4, but will likely change to M=0 in the future (see discussion in the above LLVM PR).

    This is a bit different for the "constrained" LLVM variants - we map llvm.experimental.constrained.minimum to M=1 (which is exactly correct), and we currently map llvm.experimental.constrained.minnum to M=4 (which is a bug - it should be M=0). There is currently no "constrained" variant of llvm.minimumnum.

  11. RalfJung commented on Mar 10, 2026

    @RalfJung
    MemberAuthor

    Okay, thanks. For stdarch the question is, what is vec_min supposed to do? I guess it is supposed to match what the hardware does, i.e. IEEE 754-2008 minNum? However, IEEE 754-2008 minNum is underspecified -- it doesn't say how signed zeros are handled. What does the hardware do for those? Also, if the hardware has multiple modes, then "what the hardware does" doesn't have a single answer. But then also, IBM's own emulation of vec_min for older hardware uses a completely different semantics (as far as these edge cases are concerned) -- the docs say (a>b)?a:b, so if either input is NaN the 2nd input is returned and if both are zeros then the 2nd input is returned. So maybe it doesn't matter what we do here as anyway programmers cannot rely on vec_min doing anything specific for NaN / signed zero.

    As background on our HW support, we have VECTOR FP MINIMUM/MAXIMUM instructions - exposed to the front end via the llvm.s390.vfminsb and related intrinsics - that provide multiple modes of operations:

    I see, so we could use those directly from stdarch? But vec_min doesn't say which "mode" it corresponds to... what do clang and GCC do?

    EDIT: I found a test in the LLVM repo which seems to indicate that vec_min maps to mode 0. So we should probably do the same then.

  12. 5 remaining items

  13. RalfJung commented on Mar 18, 2026

    @RalfJung
    MemberAuthor

    #154043 does this for simd_fmin/max -- it makes them match the scalar version.

    I didn't touch the "reduce" operations yet. The problem there is that LLVM simply doesn't have a "reduce" version of minimumnum/maximumnum at the moment. Also those intrinsics are only used in portable-simd, so my inclination is to simply remove float support from these intrinsics for now and use a fold loop in portable-simd (which it already does for a few other operations).

  14. added 2 commits that reference this issue on Mar 28, 2026
  15. added a commit that references this issue on Mar 28, 2026
  16. added a commit that references this issue on Mar 29, 2026
  17. added 3 commits that reference this issue on Apr 29, 2026
  18. added a commit that references this issue on Apr 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    A-SIMDArea: SIMD (Single Instruction Multiple Data)A-floating-pointArea: Floating point numbers and arithmeticC-discussionCategory: Discussion or questions that doesn't represent real issues.T-libsRelevant to the library team, which will review and decide on the PR/issue.

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions