[PATCH for-10.0 14/25] softfloat: Allow runtime choice of NaN propagation for muladd

Peter Maydell posted 25 patches 3 weeks, 6 days ago
[PATCH for-10.0 14/25] softfloat: Allow runtime choice of NaN propagation for muladd
Posted by Peter Maydell 3 weeks, 6 days ago
IEEE 758 does not define a fixed rule for which NaN to pick as the
result if both operands of a 3-operand fused multiply-add operation
are NaNs.  As a result different architectures have ended up with
different rules for propagating NaNs.

QEMU currently hardcodes the NaN propagation logic into the binary
because pickNaNMulAdd() has an ifdef ladder for different targets.
We want to make the propagation rule instead be selectable at
runtime, because:
 * this will let us have multiple targets in one QEMU binary
 * the Arm FEAT_AFP architectural feature includes letting
   the guest select a NaN propagation rule at runtime

In this commit we add an enum for the propagation rule, the field in
float_status, and the corresponding getters and setters.  We change
pickNaNMulAdd to honour this, but because all targets still leave
this field at its default 0 value, the fallback logic will pick the
rule type with the old ifdef ladder.

It's valid not to set a propagation rule if default_nan_mode is
enabled, because in that case there's no need to pick a NaN; all the
callers of pickNaNMulAdd() catch this case and skip calling it.

Signed-off-by: Peter Maydell <peter.maydell@linaro.org>
---
 include/fpu/softfloat-helpers.h |  11 +++
 include/fpu/softfloat-types.h   |  37 ++++++++
 fpu/softfloat-specialize.c.inc  | 160 +++++++++++++-------------------
 3 files changed, 112 insertions(+), 96 deletions(-)

diff --git a/include/fpu/softfloat-helpers.h b/include/fpu/softfloat-helpers.h
index 0bf44dc6087..cf06b4e16bf 100644
--- a/include/fpu/softfloat-helpers.h
+++ b/include/fpu/softfloat-helpers.h
@@ -81,6 +81,12 @@ static inline void set_float_2nan_prop_rule(Float2NaNPropRule rule,
     status->float_2nan_prop_rule = rule;
 }
 
+static inline void set_float_3nan_prop_rule(Float3NaNPropRule rule,
+                                            float_status *status)
+{
+    status->float_3nan_prop_rule = rule;
+}
+
 static inline void set_float_infzeronan_rule(FloatInfZeroNaNRule rule,
                                              float_status *status)
 {
@@ -143,6 +149,11 @@ static inline Float2NaNPropRule get_float_2nan_prop_rule(float_status *status)
     return status->float_2nan_prop_rule;
 }
 
+static inline Float3NaNPropRule get_float_3nan_prop_rule(float_status *status)
+{
+    return status->float_3nan_prop_rule;
+}
+
 static inline FloatInfZeroNaNRule get_float_infzeronan_rule(float_status *status)
 {
     return status->float_infzeronan_rule;
diff --git a/include/fpu/softfloat-types.h b/include/fpu/softfloat-types.h
index 27a1c96754d..79220f8c67f 100644
--- a/include/fpu/softfloat-types.h
+++ b/include/fpu/softfloat-types.h
@@ -207,6 +207,42 @@ typedef enum __attribute__((__packed__)) {
     float_2nan_prop_x87,
 } Float2NaNPropRule;
 
+/*
+ * 3-input NaN propagation rule, for fused multiply-add. Individual
+ * architectures have different rules for which input NaN is
+ * propagated to the output when there is more than one NaN on the
+ * input.
+ *
+ * If default_nan_mode is enabled then it is valid not to set a NaN
+ * propagation rule, because the softfloat code guarantees not to try
+ * to pick a NaN to propagate in default NaN mode.  When not in
+ * default-NaN mode, it is an error for the target not to set the rule
+ * in float_status if it uses a muladd, and we will assert if we need
+ * to handle an input NaN and no rule was selected.
+ *
+ * For QEMU, the multiply-add operation is A * B + C.
+ *
+ * NB: we don't list all 12 possibilities here or implement them
+ * in pickNaNMulAdd; if your architecture needs one of the missing
+ * combinations you should add it.
+ */
+typedef enum __attribute__((__packed__)) {
+    /* No propagation rule specified */
+    float_3nan_prop_none = 0,
+    /* Prefer SNaN over QNaN, then operand A over B over C */
+    float_3nan_prop_s_abc,
+    /* Prefer SNaN over QNaN, then operand C over A over B */
+    float_3nan_prop_s_cab,
+    /* Prefer SNaN over QNaN, then operand C over B over A */
+    float_3nan_prop_s_cba,
+    /* Prefer A over B over C regardless of SNaN vs QNaN */
+    float_3nan_prop_abc,
+    /* Prefer A over C over B regardless of SNaN vs QNaN */
+    float_3nan_prop_acb,
+    /* Prefer C over B over A regardless of SNaN vs QNaN */
+    float_3nan_prop_cba,
+} Float3NaNPropRule;
+
 /*
  * Rule for result of fused multiply-add 0 * Inf + NaN.
  * This must be a NaN, but implementations differ on whether this
@@ -241,6 +277,7 @@ typedef struct float_status {
     FloatRoundMode float_rounding_mode;
     FloatX80RoundPrec floatx80_rounding_precision;
     Float2NaNPropRule float_2nan_prop_rule;
+    Float3NaNPropRule float_3nan_prop_rule;
     FloatInfZeroNaNRule float_infzeronan_rule;
     bool tininess_before_rounding;
     /* should denormalised results go to zero and set the inexact flag? */
diff --git a/fpu/softfloat-specialize.c.inc b/fpu/softfloat-specialize.c.inc
index 3e4ec938b25..d7c0c90ea65 100644
--- a/fpu/softfloat-specialize.c.inc
+++ b/fpu/softfloat-specialize.c.inc
@@ -475,6 +475,7 @@ static int pickNaN(FloatClass a_cls, FloatClass b_cls,
 static int pickNaNMulAdd(FloatClass a_cls, FloatClass b_cls, FloatClass c_cls,
                          bool infzero, float_status *status)
 {
+    Float3NaNPropRule rule = status->float_3nan_prop_rule;
     /*
      * We guarantee not to require the target to tell us how to
      * pick a NaN if we're always returning the default NaN.
@@ -500,27 +501,44 @@ static int pickNaNMulAdd(FloatClass a_cls, FloatClass b_cls, FloatClass c_cls,
         }
     }
 
+    if (rule == float_3nan_prop_none) {
 #if defined(TARGET_ARM)
-
-    /* This looks different from the ARM ARM pseudocode, because the ARM ARM
-     * puts the operands to a fused mac operation (a*b)+c in the order c,a,b.
-     */
-    if (is_snan(c_cls)) {
-        return 2;
-    } else if (is_snan(a_cls)) {
-        return 0;
-    } else if (is_snan(b_cls)) {
-        return 1;
-    } else if (is_qnan(c_cls)) {
-        return 2;
-    } else if (is_qnan(a_cls)) {
-        return 0;
-    } else {
-        return 1;
-    }
+        /*
+         * This looks different from the ARM ARM pseudocode, because the ARM ARM
+         * puts the operands to a fused mac operation (a*b)+c in the order c,a,b
+         */
+        rule = float_3nan_prop_s_cab;
 #elif defined(TARGET_MIPS)
-    if (snan_bit_is_one(status)) {
-        /* Prefer sNaN over qNaN, in the a, b, c order. */
+        if (snan_bit_is_one(status)) {
+            rule = float_3nan_prop_s_abc;
+        } else {
+            rule = float_3nan_prop_s_cab;
+        }
+#elif defined(TARGET_LOONGARCH64)
+        rule = float_3nan_prop_s_cab;
+#elif defined(TARGET_PPC)
+        /*
+         * If fRA is a NaN return it; otherwise if fRB is a NaN return it;
+         * otherwise return fRC. Note that muladd on PPC is (fRA * fRC) + frB
+         */
+        rule = float_3nan_prop_acb;
+#elif defined(TARGET_S390X)
+        rule = float_3nan_prop_s_abc;
+#elif defined(TARGET_SPARC)
+        rule = float_3nan_prop_s_cba;
+#elif defined(TARGET_XTENSA)
+        if (status->use_first_nan) {
+            rule = float_3nan_prop_abc;
+        } else {
+            rule = float_3nan_prop_cba;
+        }
+#else
+        rule = float_3nan_prop_abc;
+#endif
+    }
+
+    switch (rule) {
+    case float_3nan_prop_s_abc:
         if (is_snan(a_cls)) {
             return 0;
         } else if (is_snan(b_cls)) {
@@ -534,8 +552,7 @@ static int pickNaNMulAdd(FloatClass a_cls, FloatClass b_cls, FloatClass c_cls,
         } else {
             return 2;
         }
-    } else {
-        /* Prefer sNaN over qNaN, in the c, a, b order. */
+    case float_3nan_prop_s_cab:
         if (is_snan(c_cls)) {
             return 2;
         } else if (is_snan(a_cls)) {
@@ -549,68 +566,21 @@ static int pickNaNMulAdd(FloatClass a_cls, FloatClass b_cls, FloatClass c_cls,
         } else {
             return 1;
         }
-    }
-#elif defined(TARGET_LOONGARCH64)
-    /* Prefer sNaN over qNaN, in the c, a, b order. */
-    if (is_snan(c_cls)) {
-        return 2;
-    } else if (is_snan(a_cls)) {
-        return 0;
-    } else if (is_snan(b_cls)) {
-        return 1;
-    } else if (is_qnan(c_cls)) {
-        return 2;
-    } else if (is_qnan(a_cls)) {
-        return 0;
-    } else {
-        return 1;
-    }
-#elif defined(TARGET_PPC)
-    /* If fRA is a NaN return it; otherwise if fRB is a NaN return it;
-     * otherwise return fRC. Note that muladd on PPC is (fRA * fRC) + frB
-     */
-    if (is_nan(a_cls)) {
-        return 0;
-    } else if (is_nan(c_cls)) {
-        return 2;
-    } else {
-        return 1;
-    }
-#elif defined(TARGET_S390X)
-    if (is_snan(a_cls)) {
-        return 0;
-    } else if (is_snan(b_cls)) {
-        return 1;
-    } else if (is_snan(c_cls)) {
-        return 2;
-    } else if (is_qnan(a_cls)) {
-        return 0;
-    } else if (is_qnan(b_cls)) {
-        return 1;
-    } else {
-        return 2;
-    }
-#elif defined(TARGET_SPARC)
-    /* Prefer SNaN over QNaN, order C, B, A. */
-    if (is_snan(c_cls)) {
-        return 2;
-    } else if (is_snan(b_cls)) {
-        return 1;
-    } else if (is_snan(a_cls)) {
-        return 0;
-    } else if (is_qnan(c_cls)) {
-        return 2;
-    } else if (is_qnan(b_cls)) {
-        return 1;
-    } else {
-        return 0;
-    }
-#elif defined(TARGET_XTENSA)
-    /*
-     * For Xtensa, the (inf,zero,nan) case sets InvalidOp and returns
-     * an input NaN if we have one (ie c).
-     */
-    if (status->use_first_nan) {
+    case float_3nan_prop_s_cba:
+        if (is_snan(c_cls)) {
+            return 2;
+        } else if (is_snan(b_cls)) {
+            return 1;
+        } else if (is_snan(a_cls)) {
+            return 0;
+        } else if (is_qnan(c_cls)) {
+            return 2;
+        } else if (is_qnan(b_cls)) {
+            return 1;
+        } else {
+            return 0;
+        }
+    case float_3nan_prop_abc:
         if (is_nan(a_cls)) {
             return 0;
         } else if (is_nan(b_cls)) {
@@ -618,7 +588,15 @@ static int pickNaNMulAdd(FloatClass a_cls, FloatClass b_cls, FloatClass c_cls,
         } else {
             return 2;
         }
-    } else {
+    case float_3nan_prop_acb:
+        if (is_nan(a_cls)) {
+            return 0;
+        } else if (is_nan(c_cls)) {
+            return 2;
+        } else {
+            return 1;
+        }
+    case float_3nan_prop_cba:
         if (is_nan(c_cls)) {
             return 2;
         } else if (is_nan(b_cls)) {
@@ -626,19 +604,9 @@ static int pickNaNMulAdd(FloatClass a_cls, FloatClass b_cls, FloatClass c_cls,
         } else {
             return 0;
         }
+    default:
+        g_assert_not_reached();
     }
-#else
-    /* A default implementation: prefer a to b to c.
-     * This is unlikely to actually match any real implementation.
-     */
-    if (is_nan(a_cls)) {
-        return 0;
-    } else if (is_nan(b_cls)) {
-        return 1;
-    } else {
-        return 2;
-    }
-#endif
 }
 
 /*----------------------------------------------------------------------------
-- 
2.34.1
Re: [PATCH for-10.0 14/25] softfloat: Allow runtime choice of NaN propagation for muladd
Posted by Richard Henderson 3 weeks, 6 days ago
On 11/28/24 04:42, Peter Maydell wrote:
> +/*
> + * 3-input NaN propagation rule, for fused multiply-add. Individual
> + * architectures have different rules for which input NaN is
> + * propagated to the output when there is more than one NaN on the
> + * input.
> + *
> + * If default_nan_mode is enabled then it is valid not to set a NaN
> + * propagation rule, because the softfloat code guarantees not to try
> + * to pick a NaN to propagate in default NaN mode.  When not in
> + * default-NaN mode, it is an error for the target not to set the rule
> + * in float_status if it uses a muladd, and we will assert if we need
> + * to handle an input NaN and no rule was selected.
> + *
> + * For QEMU, the multiply-add operation is A * B + C.
> + *
> + * NB: we don't list all 12 possibilities here or implement them
> + * in pickNaNMulAdd; if your architecture needs one of the missing
> + * combinations you should add it.
> + */
> +typedef enum __attribute__((__packed__)) {
> +    /* No propagation rule specified */
> +    float_3nan_prop_none = 0,
> +    /* Prefer SNaN over QNaN, then operand A over B over C */
> +    float_3nan_prop_s_abc,
> +    /* Prefer SNaN over QNaN, then operand C over A over B */
> +    float_3nan_prop_s_cab,
> +    /* Prefer SNaN over QNaN, then operand C over B over A */
> +    float_3nan_prop_s_cba,
> +    /* Prefer A over B over C regardless of SNaN vs QNaN */
> +    float_3nan_prop_abc,
> +    /* Prefer A over C over B regardless of SNaN vs QNaN */
> +    float_3nan_prop_acb,
> +    /* Prefer C over B over A regardless of SNaN vs QNaN */
> +    float_3nan_prop_cba,
> +} Float3NaNPropRule;

Oof.  I was imagining a bit more data driven solution, rather than explicitly enumerating.

For instance:

FIELD(3NAN, 1ST, 0, 2)
FIELD(3NAN, 2ND, 2, 2)
FIELD(3NAN, 3RD, 4, 2)
FIELD(3NAN, SNAN, 6, 1)

float_3nan_prop_abc = (0 << R_3NAN_1ST_SHIFT)
                     | (1 << R_3NAN_2ND_SHIFT)
                     | (2 << R_3NAN_3RD_SHIFT),

float_3nan_prop_s_abc = float_3nan_prop_abc | R_3NAN_SNAN_MASK,


FloatClass cls[3] = { a_cls, b_cls, c_cls };
if (rule & R_3NAN_SNAN_MASK && abc_mask & float_cmask_snan) {
     do {
         which = rule & R_3NAN_1ST_MASK;
         rule >>= R_3NAN_1ST_LENGTH;
     } while (!is_snan(cls[which]));
} else {
     do {
         which = rule & R_3NAN_1ST_MASK;
         rule >>= R_3NAN_1ST_LENGTH;
     } while (!is_nan(cls[which]));
}
return which;


r~