[Mlir-commits] [mlir] [mlir][complex] Fix log1p(-1 + 0i) returning 0 (PR #233043)

Chang Yong Lik llvmlistbot at llvm.org
Sat Oct 17 00:32:58 PDT 2026


https://github.com/ahlag created https://github.com/llvm/llvm-project/pull/233043

The `complex.log1p` lowering in `ComplexToStandard` computes the real part as `log(max) + 0.5 * log1p((min / max)^2)`, where `min` and `max` are `|real + 1|` and `|imag|`. When that is NaN, it falls back to `min`. For `log1p(-1 + 0i)` both are 0, so `min / max = 0 / 0 = NaN`, and the fallback returns `0 + 0i` instead of `-inf + 0i`.

This patch falls back to `log(max)` instead. That is `-inf` for the zero case and still `inf` when both are inf, which is the case the fallback was written for. Other NaN results come from NaN inputs, where `max` and `min` are both NaN, so they are unchanged.

This fixes [tensorflow#128983](https://github.com/tensorflow/tensorflow/issues/128983) for XLA, which lowers complex `log-plus-one` through this pass. With tf-nightly 2.23.0-dev20261014 on macOS arm64:

```
inputs: -1+0j, -1-0j, inf+inf*j, -inf+inf*j
XLA:   [ 0.+0.j    0.-0.j    inf+0.7853982j  inf+2.3561945j]
numpy: [-inf+0.j  -inf-0.j   inf+0.7853982j  inf+2.3561945j]
```

With this patch, the lowered code matches NumPy on these inputs.

Tests:
- `convert-to-standard.mlir`: updated the select in the two log1p checks.
- `Integration/Dialect/Complex/CPU/correctness.mlir`: added log1p cases, including the inf/inf ones to show they don't change. Integration tests are not enabled in my build, so I ran its RUN pipeline by hand: it passes with the fix and fails without it.

AI use: I used Claude to help write the patch and tests; I reviewed and tested the change.

>From 96eb35f368be3de544982f4f4e3aa93499bec0f2 Mon Sep 17 00:00:00 2001
From: ahlag <theeahlag at gmail.com>
Date: Sat, 17 Oct 2026 16:32:46 +0900
Subject: [PATCH] [mlir][complex] Fix log1p(-1 + 0i) returning 0

The complex.log1p lowering computes the real part as
log(max) + 0.5 * log1p((min / max)^2), where min and max are |real + 1|
and |imag|, and falls back to min when that is NaN. For log1p(-1 + 0i)
both are 0, so min / max = 0 / 0 = NaN and the fallback returned 0
instead of -inf. Fall back to log(max) instead, which is -inf here and
still inf when both are inf, the case the fallback was written for.

Fixes https://github.com/tensorflow/tensorflow/issues/128983 for XLA CPU
and GPU, which use this lowering.

Assisted-by: Claude
---
 .../ComplexToStandard/ComplexToStandard.cpp   |  5 ++-
 .../convert-to-standard.mlir                  |  4 +-
 .../Dialect/Complex/CPU/correctness.mlir      | 37 +++++++++++++++++++
 3 files changed, 43 insertions(+), 3 deletions(-)

diff --git a/mlir/lib/Conversion/ComplexToStandard/ComplexToStandard.cpp b/mlir/lib/Conversion/ComplexToStandard/ComplexToStandard.cpp
index d1cdd8ae17e3f4..db32f12c737049 100644
--- a/mlir/lib/Conversion/ComplexToStandard/ComplexToStandard.cpp
+++ b/mlir/lib/Conversion/ComplexToStandard/ComplexToStandard.cpp
@@ -522,11 +522,14 @@ struct Log1pOpConversion : public OpConversionPattern<complex::Log1pOp> {
     Value r = arith::AddFOp::create(
         b, arith::MulFOp::create(b, half, logOfSqrtPart, fmfWithNaNInf),
         logOfMaxAbsOfRealPlusOneAndImag, fmfWithNaNInf);
+    // r is NaN when min/max is 0/0 or inf/inf, i.e. for log1p(-1 + 0i) and
+    // when both |real + 1| and |imag| are inf. log(max) is then the answer:
+    // -inf and inf respectively.
     Value resultReal = arith::SelectOp::create(
         b,
         arith::CmpFOp::create(b, arith::CmpFPredicate::UNO, r, r,
                               fmfWithNaNInf),
-        minAbs, r);
+        logOfMaxAbsOfRealPlusOneAndImag, r);
     Value resultImag = math::Atan2Op::create(b, imag, realPlusOne, fmf);
     rewriter.replaceOpWithNewOp<complex::CreateOp>(op, type, resultReal,
                                                    resultImag);
diff --git a/mlir/test/Conversion/ComplexToStandard/convert-to-standard.mlir b/mlir/test/Conversion/ComplexToStandard/convert-to-standard.mlir
index 448d82df503202..a28c765ce91ea7 100644
--- a/mlir/test/Conversion/ComplexToStandard/convert-to-standard.mlir
+++ b/mlir/test/Conversion/ComplexToStandard/convert-to-standard.mlir
@@ -339,7 +339,7 @@ func.func @complex_log1p(%arg: complex<f32>) -> complex<f32> {
 // CHECK: %[[HALF_LOG_SQ:.*]] = arith.mulf %cst, %[[LOG_SQ]] : f32
 // CHECK: %[[R:.*]] = arith.addf %[[HALF_LOG_SQ]], %[[LOG_1]] : f32
 // CHECK: %[[ISNAN:.*]] = arith.cmpf uno, %[[R]], %[[R]] : f32
-// CHECK: %[[RESULT_REAL:.*]] = arith.select %[[ISNAN]], %[[MIN]], %[[R]] : f32
+// CHECK: %[[RESULT_REAL:.*]] = arith.select %[[ISNAN]], %[[LOG_1]], %[[R]] : f32
 // CHECK: %[[RESULT_IMAG:.*]] = math.atan2 %[[IMAG]], %[[REAL_PLUS_ONE]] : f32
 // CHECK: %[[RESULT:.*]] = complex.create %[[RESULT_REAL]], %[[RESULT_IMAG]] : complex<f32>
 // CHECK: return %[[RESULT]] : complex<f32>
@@ -926,7 +926,7 @@ func.func @complex_log1p_with_fmf(%arg: complex<f32>) -> complex<f32> {
 // CHECK: %[[HALF_LOG_SQ:.*]] = arith.mulf %cst, %[[LOG_SQ]] fastmath<contract> : f32
 // CHECK: %[[R:.*]] = arith.addf %[[HALF_LOG_SQ]], %[[LOG_1]] fastmath<contract> : f32
 // CHECK: %[[ISNAN:.*]] = arith.cmpf uno, %[[R]], %[[R]] fastmath<contract> : f32
-// CHECK: %[[RESULT_REAL:.*]] = arith.select %[[ISNAN]], %[[MIN]], %[[R]] : f32
+// CHECK: %[[RESULT_REAL:.*]] = arith.select %[[ISNAN]], %[[LOG_1]], %[[R]] : f32
 // CHECK: %[[RESULT_IMAG:.*]] = math.atan2 %[[IMAG]], %[[REAL_PLUS_ONE]] fastmath<nnan,contract> : f32
 // CHECK: %[[RESULT:.*]] = complex.create %[[RESULT_REAL]], %[[RESULT_IMAG]] : complex<f32>
 // CHECK: return %[[RESULT]] : complex<f32>
diff --git a/mlir/test/Integration/Dialect/Complex/CPU/correctness.mlir b/mlir/test/Integration/Dialect/Complex/CPU/correctness.mlir
index 29ad9603d1b9b2..120dcf916af835 100644
--- a/mlir/test/Integration/Dialect/Complex/CPU/correctness.mlir
+++ b/mlir/test/Integration/Dialect/Complex/CPU/correctness.mlir
@@ -44,6 +44,11 @@ func.func @rsqrt(%arg: complex<f32>) -> complex<f32> {
   func.return %sqrt : complex<f32>
 }
 
+func.func @log1p(%arg: complex<f32>) -> complex<f32> {
+  %log1p = complex.log1p %arg : complex<f32>
+  func.return %log1p : complex<f32>
+}
+
 func.func @conj(%arg: complex<f32>) -> complex<f32> {
   %conj = complex.conj %arg : complex<f32>
   func.return %conj : complex<f32>
@@ -362,6 +367,38 @@ func.func @entry() {
   call @test_element_f64(%abs_test_cast, %abs_func)
     : (tensor<?xcomplex<f64>>, (complex<f64>) -> f64) -> ()
 
+  // complex.log1p test
+  %log1p_test = arith.constant dense<[
+    (1.0, 2.0),
+    // CHECK:       1.0397
+    // CHECK-NEXT:  0.7853
+
+    // log1p(-1 + 0i) = log(0 + 0i) = -inf + 0i. Both |real + 1| and |imag|
+    // are 0 here, so their ratio is 0 / 0 = NaN.
+    (-1.0, 0.0),
+    // CHECK-NEXT:  -inf
+    // CHECK-NEXT:  {{^}}0{{$}}
+    (-1.0, 0x80000000),
+    // CHECK-NEXT:  -inf
+    // CHECK-NEXT:  {{^}}-0{{$}}
+
+    // Both |real + 1| and |imag| are inf, their ratio is inf / inf = NaN.
+    // (inf, inf)
+    (0x7F800000, 0x7F800000),
+    // CHECK-NEXT:  inf
+    // CHECK-NEXT:  0.7853
+    // (-inf, inf)
+    (0xFF800000, 0x7F800000)
+    // CHECK-NEXT:  inf
+    // CHECK-NEXT:  2.3561
+  ]> : tensor<5xcomplex<f32>>
+  %log1p_test_cast = tensor.cast %log1p_test
+    :  tensor<5xcomplex<f32>> to tensor<?xcomplex<f32>>
+
+  %log1p_func = func.constant @log1p : (complex<f32>) -> complex<f32>
+  call @test_unary(%log1p_test_cast, %log1p_func)
+    : (tensor<?xcomplex<f32>>, (complex<f32>) -> complex<f32>) -> ()
+
   // complex.exp test
   %exp_test = arith.constant dense<[
     (1.0, 2.0),



More information about the Mlir-commits mailing list