[Mlir-commits] [mlir] [mlir][math] Preserve signed zero when expanding round below half (PR #237039)
llvmlistbot at llvm.org
llvmlistbot at llvm.org
Thu Oct 29 18:08:27 PDT 2026
llvmorg-github-actions[bot] wrote:
<!--LLVM PR SUMMARY COMMENT-->
@llvm/pr-subscribers-mlir-math
Author: Demetrios Chiuratto Agourakis (agourakis82)
<details>
<summary>Changes</summary>
Fixes #<!-- -->235156. Thanks to @<!-- -->lin334141-cell for reporting and analyzing the defect.
The f32 expansion of `math.round` adds a signed half before truncating. For the predecessor of half, this addition can round across the integer boundary before truncation. Inputs `0x3effffff` and `0xbeffffff` consequently produce `+1` and `-1` instead of `+0` and `-0`.
Reuse the existing exponent calculation to select a signed zero when the input magnitude is below half. Exact halves continue through the existing rounding path. The existing large/special-value handling and f64 behavior are retained.
Update the scalar expansion checks, including the nested round expansion used by `roundeven`. Add shaped expansion/filter coverage and a native vector boundary regression that compares output bits, including the sign of zero.
Validation against upstream base `807f9b705b7b4afaa9bac4a4e0fdfcf9addb0d22`:
- Built original and patched LLVM/MLIR tools independently.
- The 38-case C++23 reference reproduces two mismatches before the fix and zero after it at O0/O2. The unexpanded source has zero mismatches. The vector boundary regression also fails before and passes after the fix.
- Both existing `expand-math.mlir` RUN commands, new shape/filter checks, and the new integration test pass. A separate 38-case `roundeven` comparison against C++23 `std::nearbyint` passes at O0/O2.
- An external C++23 native harness enumerates all 4,294,967,296 f32 input bit patterns. Original O2 has exactly the two known mismatches; patched O0 and O2 have zero. Non-NaNs are compared bitwise against `std::round(float)`; NaNs are checked for classification only. Each thread checks FE_TONEAREST.
The exhaustive result is empirical x86-64 validation under FE_TONEAREST. It does not qualify NaN payload preservation, exception flags, other dynamic rounding modes, or other backends. Scalable-vector and tensor coverage is structural.
Assisted-by: OpenAI Codex
---
Full diff: https://github.com/llvm/llvm-project/pull/237039.diff
4 Files Affected:
- (modified) mlir/lib/Dialect/Math/Transforms/ExpandOps.cpp (+8)
- (modified) mlir/test/Dialect/Math/expand-math.mlir (+10-2)
- (added) mlir/test/Dialect/Math/expand-round-shapes.mlir (+47)
- (added) mlir/test/Integration/Dialect/Math/CPU/expand-round.mlir (+22)
``````````diff
diff --git a/mlir/lib/Dialect/Math/Transforms/ExpandOps.cpp b/mlir/lib/Dialect/Math/Transforms/ExpandOps.cpp
index 41816abf073398..e05aea47b0c240 100644
--- a/mlir/lib/Dialect/Math/Transforms/ExpandOps.cpp
+++ b/mlir/lib/Dialect/Math/Transforms/ExpandOps.cpp
@@ -575,6 +575,14 @@ static LogicalResult convertRoundOp(math::RoundOp op,
Value result = arith::SelectOp::create(b, isSpecialValOrLargeVal, operand,
fpFixedConvert);
+ // Values with |x| < 0.5 round to signed zero. Adding 0.5 to the predecessor
+ // of 0.5 can round to 1.0 before the integer truncation.
+ Value minusOne = createIntConst(loc, i32Ty, -1, b, operand);
+ Value isBelowHalf = arith::CmpIOp::create(
+ b, arith::CmpIPredicate::slt, operandBiasedExp, minusOne);
+ Value zero = createFloatConst(loc, opType, 0.0, b, operand);
+ Value signedZero = math::CopySignOp::create(b, zero, operand);
+ result = arith::SelectOp::create(b, isBelowHalf, signedZero, result);
rewriter.replaceOp(op, result);
return success();
}
diff --git a/mlir/test/Dialect/Math/expand-math.mlir b/mlir/test/Dialect/Math/expand-math.mlir
index 84b0bb0aa314ae..eee82dac2177b1 100644
--- a/mlir/test/Dialect/Math/expand-math.mlir
+++ b/mlir/test/Dialect/Math/expand-math.mlir
@@ -247,7 +247,12 @@ func.func @roundf_func(%a: f32) -> f32 {
// CHECK-DAG: %[[ARG_BIASED_EXP:.*]] = arith.subi %[[ARG_EXP]], %[[C127]]
// CHECK-DAG: %[[IS_SPECIAL_VAL:.*]] = arith.cmpi sge, %[[ARG_BIASED_EXP]], %[[C23]]
// CHECK-DAG: %[[RESULT:.*]] = arith.select %[[IS_SPECIAL_VAL]], %[[ARG0]], %[[FP_FIXED_CONVERT_1]]
- // CHECK: return %[[RESULT]]
+ // CHECK-DAG: %[[MINUS_ONE:.*]] = arith.constant -1 : i32
+ // CHECK-DAG: %[[ZERO:.*]] = arith.constant 0.000000e+00 : f32
+ // CHECK-DAG: %[[BELOW_HALF:.*]] = arith.cmpi slt, %[[ARG_BIASED_EXP]], %[[MINUS_ONE]]
+ // CHECK-DAG: %[[SIGNED_ZERO:.*]] = math.copysign %[[ZERO]], %[[ARG0]]
+ // CHECK-DAG: %[[ROUNDED:.*]] = arith.select %[[BELOW_HALF]], %[[SIGNED_ZERO]], %[[RESULT]]
+ // CHECK: return %[[ROUNDED]]
%ret = math.round %a : f32
return %ret : f32
}
@@ -468,7 +473,10 @@ func.func @roundeven32(%arg: f32) -> f32 {
// CHECK: %[[ARG_EXP:.*]] = arith.andi %[[ARG_BITCAST_SHIFTED]], %[[EXP_MASK]]
// CHECK: %[[ARG_BIASED_EXP:.*]] = arith.subi %[[ARG_EXP]], %[[C_127]]
// CHECK: %[[IS_SPECIAL_VAL:.*]] = arith.cmpi sge, %[[ARG_BIASED_EXP]], %[[C_23]]
-// CHECK: %[[ROUND:.*]] = arith.select %[[IS_SPECIAL_VAL]], %[[VAL_0]], %[[FP_FIXED_CONVERT_1]]
+// CHECK: %[[UNGUARDED_ROUND:.*]] = arith.select %[[IS_SPECIAL_VAL]], %[[VAL_0]], %[[FP_FIXED_CONVERT_1]]
+// CHECK: %[[BELOW_HALF:.*]] = arith.cmpi slt, %[[ARG_BIASED_EXP]], %{{.*}}
+// CHECK: %[[SIGNED_ZERO:.*]] = math.copysign %{{.*}}, %[[VAL_0]]
+// CHECK: %[[ROUND:.*]] = arith.select %[[BELOW_HALF]], %[[SIGNED_ZERO]], %[[UNGUARDED_ROUND]]
// CHECK: %[[ROUND_BITCAST:.*]] = arith.bitcast %[[ROUND]] : f32 to i32
// Get biased exponents of `round` and `operand`
diff --git a/mlir/test/Dialect/Math/expand-round-shapes.mlir b/mlir/test/Dialect/Math/expand-round-shapes.mlir
new file mode 100644
index 00000000000000..69d92cb7f50be0
--- /dev/null
+++ b/mlir/test/Dialect/Math/expand-round-shapes.mlir
@@ -0,0 +1,47 @@
+// RUN: mlir-opt %s --math-expand-ops=ops=round | FileCheck %s
+// RUN: mlir-opt %s --math-expand-ops=ops=tanh | FileCheck %s --check-prefix=FILTER
+
+// CHECK-LABEL: func.func @round_fixed
+// CHECK: arith.cmpi slt, %{{.*}}, %{{.*}} : vector<4xi32>
+// CHECK: math.copysign %{{.*}}, %{{.*}} : vector<4xf32>
+// CHECK: arith.select
+// CHECK: return
+// FILTER-LABEL: func.func @round_fixed
+// FILTER: math.round
+func.func @round_fixed(%x: vector<4xf32>) -> vector<4xf32> {
+ %r = math.round %x : vector<4xf32>
+ return %r : vector<4xf32>
+}
+
+// CHECK-LABEL: func.func @round_scalable
+// CHECK: arith.cmpi slt, %{{.*}}, %{{.*}} : vector<[4]xi32>
+// CHECK: math.copysign %{{.*}}, %{{.*}} : vector<[4]xf32>
+// CHECK: arith.select
+// CHECK: return
+// FILTER-LABEL: func.func @round_scalable
+// FILTER: math.round
+func.func @round_scalable(%x: vector<[4]xf32>) -> vector<[4]xf32> {
+ %r = math.round %x : vector<[4]xf32>
+ return %r : vector<[4]xf32>
+}
+
+// CHECK-LABEL: func.func @round_tensor
+// CHECK: arith.cmpi slt, %{{.*}}, %{{.*}} : tensor<2x3xi32>
+// CHECK: math.copysign %{{.*}}, %{{.*}} : tensor<2x3xf32>
+// CHECK: arith.select
+// CHECK: return
+// FILTER-LABEL: func.func @round_tensor
+// FILTER: math.round
+func.func @round_tensor(%x: tensor<2x3xf32>) -> tensor<2x3xf32> {
+ %r = math.round %x : tensor<2x3xf32>
+ return %r : tensor<2x3xf32>
+}
+
+// CHECK-LABEL: func.func @round_f64
+// CHECK: math.round
+// FILTER-LABEL: func.func @round_f64
+// FILTER: math.round
+func.func @round_f64(%x: f64) -> f64 {
+ %r = math.round %x : f64
+ return %r : f64
+}
diff --git a/mlir/test/Integration/Dialect/Math/CPU/expand-round.mlir b/mlir/test/Integration/Dialect/Math/CPU/expand-round.mlir
new file mode 100644
index 00000000000000..75124dc1db4b43
--- /dev/null
+++ b/mlir/test/Integration/Dialect/Math/CPU/expand-round.mlir
@@ -0,0 +1,22 @@
+// RUN: mlir-opt %s --math-expand-ops=ops=round --convert-vector-to-llvm --convert-math-to-llvm --convert-arith-to-llvm --convert-func-to-llvm --convert-ub-to-llvm --reconcile-unrealized-casts | mlir-runner -O0 -e main -entry-point-result=i32 | FileCheck %s
+// RUN: mlir-opt %s --math-expand-ops=ops=round --convert-vector-to-llvm --convert-math-to-llvm --convert-arith-to-llvm --convert-func-to-llvm --convert-ub-to-llvm --reconcile-unrealized-casts | mlir-runner -O2 -e main -entry-point-result=i32 | FileCheck %s
+
+// CHECK: {{^0$}}
+// Keep round's operand dynamic until after expansion.
+func.func private @round_bits(%x: vector<8xf32>) -> vector<8xi32> {
+ %r = math.round %x : vector<8xf32>
+ %bits = arith.bitcast %r : vector<8xf32> to vector<8xi32>
+ return %bits : vector<8xi32>
+}
+
+func.func @main() -> i32 {
+ // Half predecessors, exact halves, signed zeros and signed subnormals.
+ %input = arith.constant dense<[0x3EFFFFFF, 0xBEFFFFFF, 0x3F000000, 0xBF000000, 0x00000000, 0x80000000, 0x00000001, 0x80000001]> : vector<8xi32>
+ %expected = arith.constant dense<[0x00000000, 0x80000000, 0x3F800000, 0xBF800000, 0x00000000, 0x80000000, 0x00000000, 0x80000000]> : vector<8xi32>
+ %x = arith.bitcast %input : vector<8xi32> to vector<8xf32>
+ %actual = func.call @round_bits(%x) : (vector<8xf32>) -> vector<8xi32>
+ %bad = arith.cmpi ne, %actual, %expected : vector<8xi32>
+ %bad_i32 = arith.extui %bad : vector<8xi1> to vector<8xi32>
+ %count = vector.reduction <add>, %bad_i32 : vector<8xi32> into i32
+ return %count : i32
+}
``````````
</details>
https://github.com/llvm/llvm-project/pull/237039
More information about the Mlir-commits
mailing list