You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
What are the intended semantics for simd_fmin/fmax and simd_reduce_min/max vs signaling NaN? #153395
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 ).
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.
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.
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.
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.)
s390x. This is a soup of undocumented macros in stdarch, and the public API is not doing any better: vec_min is literally documented as "vector minimum" 😂 . What even is the expected behavior for vec_min/vec_max on floats when it comes to NaNs and signed zeros? Cc @folkertdev
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:
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.
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.
#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).
Activity
Amanieu commentedon Mar 4, 2026
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:
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.
RalfJung commentedon Mar 4, 2026
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.Note that simd_fmin/fmax as currently documented do have the
nszbehavior: IEEE 754-2008 says that signed zero behavior is non-deterministic. So stdarch using those operations is already sketchy.Also note that LLVM's
minnumwithout 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 thinkminnumcan ever be used for any stdarch intrinsic.sayantn commentedon Mar 4, 2026
We can use a const enum parameter to select which variant of
simd_fmaxyou want, that'll reduce the number of distinct intrinsic needed.RalfJung commentedon Mar 5, 2026
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.
RalfJung commentedon Mar 5, 2026
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.)
RalfJung commentedon Mar 10, 2026
There are three architectures where stdarch currently uses simd_fmin/fmax:
vec_minis literally documented as "vector minimum" 😂 . What even is the expected behavior for vec_min/vec_max on floats when it comes to NaNs and signed zeros? Cc @folkertdevfolkertdev commentedon Mar 10, 2026
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
uweigand commentedon Mar 10, 2026
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
floatonz13, 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.vfminsband related intrinsics - that provide multiple modes of operations:minNumminimumminimumNumberThe only difference between our mode M=4 and
minimumNumberis that M=4 may return an input SNaN as-is, without changing it to the corresponding QNaN. We do not have a mode that matchesminimumNumberprecisely.As of the recently merged llvm/llvm-project#184595, we anyway map the
llvm.minimumnumintrinsic to M=4, given that the LLVM semantics for signaling NaNs explicitly allow this behavior.We map
llvm.minimumto mode M=1 (i.e IEEE 754-2019minimum).For
llvm.minnumthe 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.minimumto M=1 (which is exactly correct), and we currently mapllvm.experimental.constrained.minnumto M=4 (which is a bug - it should be M=0). There is currently no "constrained" variant ofllvm.minimumnum.RalfJung commentedon Mar 10, 2026
Okay, thanks. For stdarch the question is, what is
vec_minsupposed 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 ofvec_minfor 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 onvec_mindoing anything specific for NaN / signed zero.I see, so we could use those directly from stdarch? But
vec_mindoesn'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_minmaps to mode 0. So we should probably do the same then.5 remaining items
RalfJung commentedon Mar 18, 2026
#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/maximumnumat 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).Rollup merge of rust-lang#154043 - RalfJung:simd-min-max, r=Amanieu,c…
Rollup merge of rust-lang#154043 - RalfJung:simd-min-max, r=Amanieu,c…
Rollup merge of rust-lang#154043 - RalfJung:simd-min-max, r=Amanieu,c…
Unrolled build for #154043
Rollup merge of #154043 - RalfJung:simd-min-max, r=Amanieu,calebzulaw…
Rollup merge of #154043 - RalfJung:simd-min-max, r=Amanieu,calebzulaw…
Rollup merge of #154043 - RalfJung:simd-min-max, r=Amanieu,calebzulaw…
Rollup merge of #154043 - RalfJung:simd-min-max, r=Amanieu,calebzulaw…
Rollup merge of rust-lang#155189 - RalfJung:reduce-minmax-float, r=pe…
Rollup merge of rust-lang#155189 - RalfJung:reduce-minmax-float, r=pe…
Rollup merge of rust-lang#155189 - RalfJung:reduce-minmax-float, r=pe…
Unrolled build for #155189
Rollup merge of #155189 - RalfJung:reduce-minmax-float, r=petrochenkov