-
Notifications
You must be signed in to change notification settings - Fork 15.5k
[LangRef] Clarify specification for float min/max operations #172012
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
base: main
Are you sure you want to change the base?
Conversation
This implements some clarifications for the specification of floating point min/max operations based on the discussion in https://discourse.llvm.org/t/rfc-a-consistent-set-of-semantics-for-the-floating-point-minimum-and-maximum-operations/89006. The key changes are: * Explicitly specify minnum and maxnum with an sNaN operand as non-deterministically either returning NaN or treating sNaN as qNaN. This was implied by our general NaN semantics, but is important to call out here due to the special behavior of sNaN. * Explicitly specify the same non-determinism for the minnum/maxnum based vector reductions as well. * Explicitly specify the meaning of nsz on float min/max ops. In particular, clarify that unlike normal nsz semantics, it does not allow introducing a zero with a different sign out of thin air. * Remove the semantics comparison section. I tried to adjust this, but there's just so many caveats in here that it's hard to create something that is both concise and correct.
|
@llvm/pr-subscribers-llvm-ir Author: Nikita Popov (nikic) ChangesThis implements some clarifications for the specification of floating point min/max operations based on the discussion in https://discourse.llvm.org/t/rfc-a-consistent-set-of-semantics-for-the-floating-point-minimum-and-maximum-operations/89006. The key changes are:
Full diff: https://github.com/llvm/llvm-project/pull/172012.diff 1 Files Affected:
diff --git a/llvm/docs/LangRef.rst b/llvm/docs/LangRef.rst
index 5fa3a4ebb2472..3ce5244a39b31 100644
--- a/llvm/docs/LangRef.rst
+++ b/llvm/docs/LangRef.rst
@@ -17288,96 +17288,6 @@ The returned value is completely identical to the input except for the sign bit;
in particular, if the input is a NaN, then the quiet/signaling bit and payload
are perfectly preserved.
-.. _i_fminmax_family:
-
-'``llvm.min.*``' Intrinsics Comparation
-^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-
-Standard:
-"""""""""
-
-IEEE754 and ISO C define some min/max operations, and they have some differences
-on working with qNaN/sNaN and +0.0/-0.0. Here is the list:
-
-.. list-table::
- :header-rows: 2
-
- * - ``ISO C``
- - fmin/fmax
- - fmininum/fmaximum
- - fminimum_num/fmaximum_num
-
- * - ``IEEE754``
- - minNum/maxNum (2008)
- - minimum/maximum (2019)
- - minimumNumber/maximumNumber (2019)
-
- * - ``+0.0 vs -0.0``
- - either one
- - +0.0 > -0.0
- - +0.0 > -0.0
-
- * - ``NUM vs sNaN``
- - qNaN, invalid exception
- - qNaN, invalid exception
- - NUM, invalid exception
-
- * - ``qNaN vs sNaN``
- - qNaN, invalid exception
- - qNaN, invalid exception
- - qNaN, invalid exception
-
- * - ``NUM vs qNaN``
- - NUM, no exception
- - qNaN, no exception
- - NUM, no exception
-
-LLVM Implementation:
-""""""""""""""""""""
-
-LLVM implements all ISO C flavors as listed in this table, except in the
-default floating-point environment exceptions are ignored. The constrained
-versions of the intrinsics respect the exception behavior.
-
-.. list-table::
- :header-rows: 1
- :widths: 16 28 28 28
-
- * - Operation
- - minnum/maxnum
- - minimum/maximum
- - minimumnum/maximumnum
-
- * - ``NUM vs qNaN``
- - NUM, no exception
- - qNaN, no exception
- - NUM, no exception
-
- * - ``NUM vs sNaN``
- - qNaN, invalid exception
- - qNaN, invalid exception
- - NUM, invalid exception
-
- * - ``qNaN vs sNaN``
- - qNaN, invalid exception
- - qNaN, invalid exception
- - qNaN, invalid exception
-
- * - ``sNaN vs sNaN``
- - qNaN, invalid exception
- - qNaN, invalid exception
- - qNaN, invalid exception
-
- * - ``+0.0 vs -0.0``
- - +0.0(max)/-0.0(min)
- - +0.0(max)/-0.0(min)
- - +0.0(max)/-0.0(min)
-
- * - ``NUM vs NUM``
- - larger(max)/smaller(min)
- - larger(max)/smaller(min)
- - larger(max)/smaller(min)
-
.. _i_minnum:
'``llvm.minnum.*``' Intrinsic
@@ -17413,30 +17323,26 @@ type.
Semantics:
""""""""""
-Follows the semantics of minNum in IEEE-754-2008, except that -0.0 < +0.0 for the purposes
-of this intrinsic. As for signaling NaNs, per the minNum semantics, if either operand is sNaN,
-the result is qNaN. This matches the recommended behavior for the libm
-function ``fmin``, although not all implementations have implemented these recommended behaviors.
-If either operand is a qNaN, returns the other non-NaN operand. Returns NaN only if both operands are
-NaN or if either operand is sNaN. Note that arithmetic on an sNaN doesn't consistently produce a qNaN,
-so arithmetic feeding into a minnum can produce inconsistent results. For example,
-``minnum(fadd(sNaN, -0.0), 1.0)`` can produce qNaN or 1.0 depending on whether ``fadd`` is folded.
+If both operands are qNaNs, returns a :ref:`NaN <floatnan>`. If one operand is
+qNaN and another operand is a number, returns the number. If both operands are
+numbers, returns the lesser of the two arguments. -0.0 is considered to be less
+than +0.0 for this intrinsic.
-IEEE-754-2008 defines minNum, and it was removed in IEEE-754-2019. As the replacement, IEEE-754-2019
-defines :ref:`minimumNumber <i_minimumnum>`.
+If an operand is a signaling NaN, then the intrinsic will non-deterministically
+either:
-If the intrinsic is marked with the nsz attribute, then the effect is as in the definition in C
-and IEEE-754-2008: the result of ``minnum(-0.0, +0.0)`` may be either -0.0 or +0.0.
+ * Return a :ref:`NaN <floatnan>`.
+ * Or treat the signaling NaN as a quiet NaN. In this case the intrinsic will
+ behave the same as ``llvm.minimumnum``.
-Some architectures, such as ARMv8 (FMINNM), LoongArch (fmin), MIPSr6 (min.fmt), PowerPC/VSX (xsmindp),
-have instructions that match these semantics exactly; thus it is quite simple for these architectures.
-Some architectures have similar ones while they are not exact equivalent. Such as x86 implements ``MINPS``,
-which implements the semantics of C code ``a<b?a:b``: NUM vs qNaN always return qNaN. ``MINPS`` can be used
-if ``nsz`` and ``nnan`` are given.
+If the ``nsz`` flag is specified, ``llvm.minnum`` with one +0.0 and one
+-0.0 operand may non-deterministically return either operand. Contrary to normal
+``nsz`` semantics, if both operands have the same sign, the result must also
+have the same sign.
-For existing libc implementations, the behaviors of fmin may be quite different on sNaN and signed zero behaviors,
-even in the same release of a single libm implementation.
+When used with the ``nsz`` flag, this intrinsics follows the semantics of
+``fmin`` in C.
.. _i_maxnum:
@@ -17473,30 +17379,26 @@ type.
Semantics:
""""""""""
-Follows the semantics of maxNum in IEEE-754-2008, except that -0.0 < +0.0 for the purposes
-of this intrinsic. As for signaling NaNs, per the maxNum semantics, if either operand is sNaN,
-the result is qNaN. This matches the recommended behavior for the libm
-function ``fmax``, although not all implementations have implemented these recommended behaviors.
-If either operand is a qNaN, returns the other non-NaN operand. Returns NaN only if both operands are
-NaN or if either operand is sNaN. Note that arithmetic on an sNaN doesn't consistently produce a qNaN,
-so arithmetic feeding into a maxnum can produce inconsistent results. For example,
-``maxnum(fadd(sNaN, -0.0), 1.0)`` can produce qNaN or 1.0 depending on whether ``fadd`` is folded.
+If both operands are qNaNs, returns a :ref:`NaN <floatnan>`. If one operand is
+qNaN and another operand is a number, returns the number. If both operands are
+numbers, returns the greater of the two arguments. -0.0 is considered to be
+less than +0.0 for this intrinsic.
-IEEE-754-2008 defines maxNum, and it was removed in IEEE-754-2019. As the replacement, IEEE-754-2019
-defines :ref:`maximumNumber <i_maximumnum>`.
+If an operand is a signaling NaN, then the intrinsic will non-deterministically
+either:
-If the intrinsic is marked with the nsz attribute, then the effect is as in the definition in C
-and IEEE-754-2008: the result of maxnum(-0.0, +0.0) may be either -0.0 or +0.0.
+ * Return a :ref:`NaN <floatnan>`.
+ * Or treat the signaling NaN as a quiet NaN. In this case the intrinsic will
+ behave the same as ``llvm.maximumnum``.
-Some architectures, such as ARMv8 (FMAXNM), LoongArch (fmax), MIPSr6 (max.fmt), PowerPC/VSX (xsmaxdp),
-have instructions that match these semantics exactly; thus it is quite simple for these architectures.
-Some architectures have similar ones while they are not exact equivalent. Such as x86 implements ``MAXPS``,
-which implements the semantics of C code ``a>b?a:b``: NUM vs qNaN always return qNaN. ``MAXPS`` can be used
-if ``nsz`` and ``nnan`` are given.
+If the ``nsz`` flag is specified, ``llvm.maxnum`` with one +0.0 and one
+-0.0 operand may non-deterministically return either operand. Contrary to normal
+``nsz`` semantics, if both operands have the same sign, the result must also
+have the same sign.
-For existing libc implementations, the behaviors of fmin may be quite different on sNaN and signed zero behaviors,
-even in the same release of a single libm implementation.
+When used with the ``nsz`` flag, this intrinsics follows the semantics of
+``fmax`` in C.
.. _i_minimum:
@@ -17538,6 +17440,11 @@ of the two arguments. -0.0 is considered to be less than +0.0 for this
intrinsic. Note that these are the semantics specified in the draft of
IEEE 754-2019.
+If the ``nsz`` flag is specified, ``llvm.maximum`` with one +0.0 and one
+-0.0 operand may non-deterministically return either operand. Contrary to normal
+``nsz`` semantics, if both operands have the same sign, the result must also
+have the same sign.
+
.. _i_maximum:
'``llvm.maximum.*``' Intrinsic
@@ -17578,6 +17485,11 @@ of the two arguments. -0.0 is considered to be less than +0.0 for this
intrinsic. Note that these are the semantics specified in the draft of
IEEE 754-2019.
+If the ``nsz`` flag is specified, ``llvm.maximum`` with one +0.0 and one
+-0.0 operand may non-deterministically return either operand. Contrary to normal
+``nsz`` semantics, if both operands have the same sign, the result must also
+have the same sign.
+
.. _i_minimumnum:
'``llvm.minimumnum.*``' Intrinsic
@@ -17619,12 +17531,17 @@ one operand is NaN (including sNaN) and another operand is a number,
return the number. Otherwise returns the lesser of the two
arguments. -0.0 is considered to be less than +0.0 for this intrinsic.
+If the ``nsz`` flag is specified, ``llvm.minimumnum`` with one +0.0 and one
+-0.0 operand may non-deterministically return either operand. Contrary to normal
+``nsz`` semantics, if both operands have the same sign, the result must also
+have the same sign.
+
Note that these are the semantics of minimumNumber specified in
IEEE-754-2019 with the usual :ref:`signaling NaN <floatnan>` exception.
-It has some differences with '``llvm.minnum.*``':
-1)'``llvm.minnum.*``' will return qNaN if either operand is sNaN.
-2)'``llvm.minnum*``' may return either one if we compare +0.0 vs -0.0.
+This intrinsic differs from ``llvm.minnum`` in that it is guaranteed to treat
+sNaN the same way as qNaN. ``llvm.minnum`` will instead non-deterministically
+either act like ``llvm.minimumnum`` or return a :ref:`NaN <floatnan>`.
.. _i_maximumnum:
@@ -17668,12 +17585,17 @@ another operand is a number, return the number. Otherwise returns the
greater of the two arguments. -0.0 is considered to be less than +0.0
for this intrinsic.
+If the ``nsz`` flag is specified, ``llvm.maximumnum`` with one +0.0 and one
+-0.0 operand may non-deterministically return either operand. Contrary to normal
+``nsz`` semantics, if both operands have the same sign, the result must also
+have the same sign.
+
Note that these are the semantics of maximumNumber specified in
IEEE-754-2019 with the usual :ref:`signaling NaN <floatnan>` exception.
-It has some differences with '``llvm.maxnum.*``':
-1)'``llvm.maxnum.*``' will return qNaN if either operand is sNaN.
-2)'``llvm.maxnum*``' may return either one if we compare +0.0 vs -0.0.
+This intrinsic differs from ``llvm.maxnum`` in that it is guaranteed to treat
+sNaN the same way as qNaN. ``llvm.maxnum`` will instead non-deterministically
+either act like ``llvm.maximumnum`` or return a :ref:`NaN <floatnan>`.
.. _int_copysign:
@@ -20379,9 +20301,17 @@ The '``llvm.vector.reduce.fmax.*``' intrinsics do a floating-point
``MAX`` reduction of a vector, returning the result as a scalar. The return type
matches the element-type of the vector input.
-This instruction has the same comparison semantics as the '``llvm.maxnum.*``'
-intrinsic. If the intrinsic call has the ``nnan`` fast-math flag, then the
-operation can assume that NaNs are not present in the input vector.
+This instruction has the same comparison and ``nsz`` semantics as the
+'``llvm.maxnum.*``' intrinsic.
+
+If any of the vector elements is a signaling NaN, the intrinsic will
+non-deterministically either:
+
+ * Return a :ref:`NaN <floatnan>`.
+ * Treat the signaling NaN as a quiet NaN.
+
+If the intrinsic call has the ``nnan`` fast-math flag, then the operation can
+assume that NaNs are not present in the input vector.
Arguments:
""""""""""
@@ -20408,9 +20338,17 @@ The '``llvm.vector.reduce.fmin.*``' intrinsics do a floating-point
``MIN`` reduction of a vector, returning the result as a scalar. The return type
matches the element-type of the vector input.
-This instruction has the same comparison semantics as the '``llvm.minnum.*``'
-intrinsic. If the intrinsic call has the ``nnan`` fast-math flag, then the
-operation can assume that NaNs are not present in the input vector.
+This instruction has the same comparison and ``nsz`` semantics as the
+'``llvm.minnum.*``' intrinsic.
+
+If any of the vector elements is a signaling NaN, the intrinsic will
+non-deterministically either:
+
+ * Return a :ref:`NaN <floatnan>`.
+ * Treat the signaling NaN as a quiet NaN.
+
+If the intrinsic call has the ``nnan`` fast-math flag, then the operation can
+assume that NaNs are not present in the input vector.
Arguments:
""""""""""
|
llvm/docs/LangRef.rst
Outdated
| intrinsic. Note that these are the semantics specified in the draft of | ||
| IEEE 754-2019. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| intrinsic. Note that these are the semantics specified in the draft of | |
| IEEE 754-2019. | |
| intrinsic. Note that these are the semantics of maximum specified in | |
| IEEE-754-2019 with the usual :ref:`signaling NaN <floatnan>` exception. |
For consistency with maximumNumber.
(same suggestion for minimum)
llvm/docs/LangRef.rst
Outdated
| For existing libc implementations, the behaviors of fmin may be quite different on sNaN and signed zero behaviors, | ||
| even in the same release of a single libm implementation. | ||
| When used with the ``nsz`` flag, this intrinsics follows the semantics of | ||
| ``fmax`` in C. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it worth mentioning IEEE-754 maxnum? With the nsz flag, it matches that, modulo the SNaN exception.
|
This sounds good to me! I don't have a strong opinion on whether |
jyknight
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM modulo wording tweaks.
llvm/docs/LangRef.rst
Outdated
| and IEEE-754-2008: the result of ``minnum(-0.0, +0.0)`` may be either -0.0 or +0.0. | ||
| * Return a :ref:`NaN <floatnan>`. | ||
| * Or treat the signaling NaN as a quiet NaN. In this case the intrinsic will | ||
| behave the same as ``llvm.minimumnum``. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Documenting how this differs from minimumnum is a good idea, but having it in this bullet point seems confusing. I suggest to move it out, maybe to the top paragraph, saying something like "This behaves identically to llvm.minimumnum other than its treatment of sNaN inputs."
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What I was trying to highlight here is that minnum non-deterministically chooses between two behaviors, and one of those behaviors is equivalent to minimumnum. This is what makes it legal to refine minnum to minimumnum.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Embedding the comment within "If an operand is a signaling NaN [...]" doesn't clearly communicate that minnum is equivalent to minimumnum for everything other than sNaN.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah yes, I see what you mean. I guess I can just explicitly say that minnum can be refined to minimumnum and save people the inference.
llvm/docs/LangRef.rst
Outdated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: "returns a :ref:`NaN <floatnan>`" for consistency with other locations.
llvm/docs/LangRef.rst
Outdated
| It has some differences with '``llvm.minnum.*``': | ||
| 1)'``llvm.minnum.*``' will return qNaN if either operand is sNaN. | ||
| 2)'``llvm.minnum*``' may return either one if we compare +0.0 vs -0.0. | ||
| This intrinsic differs from ``llvm.minnum`` in that it is guaranteed to treat |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Doesn't need to re-explain what the semantics of minnum. I suggest to use the same sentence I suggested for minnum, and similarly place it in the top paragraph, "This behaves identically to llvm.minnum other than its treatment of sNaN inputs."
jcranmer-intel
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I do somewhat miss having the table that organizes all of the different flavors of min/max operations to help identify which ones are in use.
That said, I've long been considering the need to have a dedicated docs page on floating-point semantics for LLVM IR and what different stakeholders need to understand about those semantics (like the existing atomics page), and the table would definitely fit there better. This entire saga is upping work on that document in my priority list, but nothing's going to happen before the end of the calendar year.
llvm/docs/LangRef.rst
Outdated
| For existing libc implementations, the behaviors of fmin may be quite different on sNaN and signed zero behaviors, | ||
| even in the same release of a single libm implementation. | ||
| When used with the ``nsz`` flag, this intrinsics follows the semantics of | ||
| ``fmin`` in C. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would mention the equivalence to IEEE 754-2008's minNum here, I think.
(IEEE 754-2008 minNum has the same semantics as C's fmin with respect to sNaN (if supported) and indifference as to which value is returned for -0/+0--I'm not entirely clear how many people in the discussion are aware of this equivalency, so it's better to just spell it out).
llvm/docs/LangRef.rst
Outdated
| intrinsic. Note that these are the semantics specified in the draft of | ||
| IEEE 754-2019. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove "the draft of" here; it's been released for a while.
arsenm
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So if it's nondeterministic, what do we constant fold to for a signaling nan?
|
We can do either. Is there one option that's better for backends / follow-up optimizations? What other operations do ( We also need to ensure that scalar evolution is aware of all this non-determinism and doesn't try to predict what these operations do. With |
Folding to the qNaN will allow more downstream operations to fold, since just about everything else simply propagates input nans |
Either option would be legal. I think using the IEEE 2008 sNaN semantics might be slightly preferable to match the constrained variants. Or we could also uphold #170181 as a permanent rather than temporary change, and just not fold sNaN arguments for these intrinsics at all.
Currently we conservatively assume that all FP libcalls/intrinsics are non-deterministic: llvm-project/llvm/lib/Analysis/ConstantFolding.cpp Lines 4540 to 4544 in f3c1645
|
It could also be: fold to QNaN if the input is known to be an SNaN, otherwise fold to other operand if the input is just known to be any NaN. (Not sure if that's something that can happen with how LLVM represents partial knowledge of floating-point values.) |
llvm/docs/LangRef.rst
Outdated
| If both operands are qNaNs, returns a :ref:`NaN <floatnan>`. If one operand is | ||
| qNaN and another operand is a number, returns the number. If both operands are | ||
| numbers, returns the lesser of the two arguments. -0.0 is considered to be less | ||
| than +0.0 for this intrinsic. | ||
|
|
||
| IEEE-754-2008 defines minNum, and it was removed in IEEE-754-2019. As the replacement, IEEE-754-2019 | ||
| defines :ref:`minimumNumber <i_minimumnum>`. | ||
| If an operand is a signaling NaN, then the intrinsic will non-deterministically | ||
| either: | ||
|
|
||
| If the intrinsic is marked with the nsz attribute, then the effect is as in the definition in C | ||
| and IEEE-754-2008: the result of ``minnum(-0.0, +0.0)`` may be either -0.0 or +0.0. | ||
| * Return a :ref:`NaN <floatnan>`. | ||
| * Or treat the signaling NaN as a quiet NaN. In this case the intrinsic will | ||
| behave the same as ``llvm.minimumnum``. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It took me a while to work out what's allowed if both inputs are sNaN. Does it have to return a qNaN? On x86, this operation can be implemented in a much faster way if we allow a signaling NaN input to be passed through to the output. I think this phrasing allows that behavior, but you have to read between the lines a bit.
Right now, it's clear that if both operands are qNaN, it returns "a NaN" (following LLVM semantics). If one operand is sNaN and the other is a number, we are free to return either the number (following llvm.minimumnum) or "a NaN" (like IEEE754-2008, but we don't have to quiet the NaN).
However, it doesn't explicitly enumerate the case where both operands are sNaN. If we assume it's a subset of the case where "an operand is a signaling NaN", we can non-deterministically choose to return "a NaN" and pass through the sNaN. I think it would be helpful to make that case explicit.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note that returning "a NaN" also allows passing through the SNaN, so I think both possible interpretations of the two-SNaN case yield the same overall semantics.
|
I've tried to address the review feedback, please take a look. |
|
Based on https://discourse.llvm.org/t/rfc-a-consistent-set-of-semantics-for-the-floating-point-minimum-and-maximum-operations/89006/61?u=nikic, I've dropped the requirement for zero ordering from maxnum/minnum. So basically now minnum == fmin == IEEE 754-2008 minNum modulo usual sNaN exception. I can revert that commit if the discussion reaches a different conclusion... |
This implements some clarifications for the specification of floating point min/max operations based on the discussion in https://discourse.llvm.org/t/rfc-a-consistent-set-of-semantics-for-the-floating-point-minimum-and-maximum-operations/89006.
The key changes are: