Skip to content

Guard LogSimplify rewrites against domain narrowing (#2570)#2576

Merged
wsmoses merged 15 commits into
EnzymeAD:mainfrom
kimjune01:fix/logsimplify-domain-guards-2570
Jul 23, 2026
Merged

Guard LogSimplify rewrites against domain narrowing (#2570)#2576
wsmoses merged 15 commits into
EnzymeAD:mainfrom
kimjune01:fix/logsimplify-domain-guards-2570

Conversation

@kimjune01

Copy link
Copy Markdown
Contributor

Fixes #2570 (maintainer-approved: "makes sense, PR welcome!").

Problem

Four LogSimplify sub-cases (EnzymeHLOOpt.cpp) apply a real-analysis identity valid only for positive bases, with no domain guard, so the optimized program returns NaN where the unoptimized one returns a finite real:

rewrite unsound when example (f64)
log(a*a) -> 2*log(a) any a < 0 a=-2: log(4)=1.386 becomes 2*log(-2)=NaN
log(pow(x,y)) -> y*log(x) x < 0 (even y), or y = 0 y=0,x=-2: log(1)=0 becomes 0*log(-2)=NaN
log(a*b) -> log(a)+log(b) (one const) const < 0 b=-3,a=-2: log(6)=1.79 becomes NaN
log(a/b) -> log(a)-log(b) (one const) const < 0 b=-3,a=-2: log(2/3)=-0.405 becomes NaN

This is not the same as the opt-in float policy behind log(sqrt(x)) -> 0.5*log(x). That rewrite is an exact identity in the reals and only perturbs float rounding. These four are false in the reals themselves: as partial functions the two sides have different domains (log(a*a) is real for a != 0, 2*log(a) only for a > 0), so the divergence appears before floating point is involved. Same finite-to-NaN domain narrowing as the CbrtOp derivative in #2571 / #2575.

Fix

Reuse the existing guaranteedNonNegativeResult analysis to gate each case on a provably-non-negative base:

  • log(a*a) -> 2*log(abs(a)): unconditionally domain-sound, since a*a and abs(a) share a domain. No guard needed; just take the log of |a|.
  • log(pow(x,y)): fire only when x is provably non-negative.
  • log(a*b) / log(a/b): fire only when the constant operand is non-negative. A positive constant agrees (NaN-for-NaN) on negative inputs; a negative constant manufactures the NaN.

Test

test/lit_tests/logsimplify.mlir's @main2/@main6 (the log(a*a) cases) are updated to the new abs form, and three cases are added asserting the guarded rewrites no longer fire (negative-constant mul, negative-constant div, unknown-sign pow base). The pipeline enables only log_simplify;log_const_prop, so the positive-constant cases (@main3/@main4/@main7/@main8) still fire unchanged.

Notes:

  • I cannot build Enzyme-JAX locally, so the updated CHECK blocks are a best-effort prediction of the post-pass IR (operand order, SSA numbering, the inserted stablehlo.abs). If CI's FileCheck flags a mismatch I will push a fixup; the source guards are the substantive change.
  • The abs form for log(a*a) fixes the domain (finite-to-NaN) bug but is not bit-exact under the multiply's overflow/underflow/rounding, so it remains a precision matter under the float-rewrite policy, distinct from the domain bug fixed here. Happy to gate it the same way as the others instead if you'd prefer.

Found mechanically with a value-and-gradient soundness gate over the rewrite library; happy to share the process if useful.

Four LogSimplify sub-cases applied real-analysis identities valid only for
positive bases, with no domain guard, so the optimized program returned NaN
where the original computed a finite real:

  log(a*a)   -> 2*log(a)            NaN for any a < 0
  log(pow(x,y)) -> y*log(x)         NaN for x < 0 (even y), or y = 0
  log(a*b)   -> log(a)+log(b)       NaN when the constant operand < 0
  log(a/b)   -> log(a)-log(b)       NaN when the constant operand < 0

These are not a fast-math/precision question: as partial functions the two
sides have different domains (log(a*a) is real for a != 0, 2*log(a) only for
a > 0), so the divergence appears before floating point is involved.

  - log(a*a) -> 2*log(abs(a)): unconditionally domain-sound (a*a and abs(a)
    share a domain), so no guard needed; just take log of |a|.
  - log(pow(x,y)): fire only when x is provably non-negative.
  - log(a*b) / log(a/b): fire only when the constant operand is non-negative
    (a positive constant agrees, NaN-for-NaN, on negative inputs; a negative
    constant manufactures the NaN).

Reuses the existing guaranteedNonNegativeResult analysis. Updates the two
log(a*a) cases in logsimplify.mlir to the abs form and adds negative-constant
/ unknown-sign-base cases asserting the guarded rewrites no longer fire.
Four LogSimplify sub-cases applied real-analysis identities valid only for
positive bases, with no domain guard, so the optimized program returned NaN
where the original computed a finite real:

  log(a*a)   -> 2*log(a)            NaN for any a < 0
  log(pow(x,y)) -> y*log(x)         NaN for x < 0 (even y), or y = 0
  log(a*b)   -> log(a)+log(b)       NaN when the constant operand < 0
  log(a/b)   -> log(a)-log(b)       NaN when the constant operand < 0

These are not a fast-math/precision question: as partial functions the two
sides have different domains (log(a*a) is real for a != 0, 2*log(a) only for
a > 0), so the divergence appears before floating point is involved.

  - log(a*a) -> 2*log(abs(a)): unconditionally domain-sound (a*a and abs(a)
    share a domain), so no guard needed; just take log of |a|.
  - log(pow(x,y)): fire only when x is provably non-negative.
  - log(a*b) / log(a/b): fire only when the constant operand is non-negative
    (a positive constant agrees, NaN-for-NaN, on negative inputs; a negative
    constant manufactures the NaN).

Reuses the existing guaranteedNonNegativeResult analysis. Updates the two
log(a*a) cases in logsimplify.mlir to the abs form and adds negative-constant
/ unknown-sign-base cases asserting the guarded rewrites no longer fire.
@vimarsh6739
vimarsh6739 force-pushed the fix/logsimplify-domain-guards-2570 branch from 86eef28 to 4f2c333 Compare July 9, 2026 19:07
@Antipath1

Copy link
Copy Markdown
Collaborator

//test/lit_tests:logsimplify.mlir.test is failing in CI

The domain guard for negative-constant log(mul/div) correctly declines to
split, but guaranteedNonNegativeResult stamps enzymexla.non_negative on the
inspected constant. Update the main_neg_mul/main_neg_div CHECK lines to
match the NOTGUARANTEED annotation (repo convention, cf. abspositive.mlir).
Also apply clang-format 16 to the AbsOp::create wrapping.
# Conflicts:
#	src/enzyme_ad/jax/Passes/EnzymeHLOOpt.cpp
#	test/lit_tests/logsimplify.mlir
@codecov

codecov Bot commented Jul 14, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 31.70%. Comparing base (e850e6b) to head (ab6cee3).
⚠️ Report is 13 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2576      +/-   ##
==========================================
+ Coverage   31.07%   31.70%   +0.63%     
==========================================
  Files         225      226       +1     
  Lines       44509    44892     +383     
==========================================
+ Hits        13830    14234     +404     
+ Misses      30679    30658      -21     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@kimjune01

Copy link
Copy Markdown
Contributor Author

Lit test fixed in ba6fd786. The remaining red lanes (ct6e TPU, a100 GPU, macOS) are failing on main too, so they're not from this change.

@Antipath1 Antipath1 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this improves the status quo quite a lot thank you so I think that this should actually get merged even in this state but I think the comments should be updated to reflect the remaining problems.

Comment on lines +27763 to +27767
// Only sound when the base x is provably non-negative. For x < 0,
// log(pow(x, y)) can be a finite real (e.g. even integer y, where
// pow(x, y) > 0) while y * log(x) is NaN; likewise y = 0 gives
// log(pow(x, 0)) = 0 but 0 * log(x) = NaN. See issue #2570.
if (defOp && guaranteedNonNegativeResult(defOp.getLhs(), rewriter)) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment here claims that this would fix the log(pow(x, 0)) case, but it actually does not. In the specific case where $x = 0$ and $y = 0$:If we start with
$\log(\text{pow}(0, 0)) = \log(1) = 0$,
our pattern rewrites it into $0 \cdot \log(0)$,
which evaluates to NaN (because $\log(0)$ is $-\infty$). That is mathematically incorrect. Soooooooooooooo we still have a correctness bug here, actually. Proving that $x$ or $y$ is never $0$ is kind of hard, so we might genuinely need to put that behind a flag, or just outright remove this rewrite, or try to prove that $x$ is strictly greater than zero (which, I might be wrong, is only provable in very specific scenarios, like the definingOp of $x$ or $y$ being an exponential op which can never be $0$). Nevertheless, I don't think you need to fix this in this PR, since having guaranteedNonNegativeResult is already a massive improvement. So, I think I just need you to update the comment to include this info.

(cc @wsmoses I think this needs your authority on how to deal with this)

Comment on lines +27799 to +27808
// Only sound when the constant operand is non-negative: a negative
// constant makes log(const) NaN where log(a*b) was a finite real
// (a < 0 makes both sides NaN, so they still agree). Issue #2570.
Value cst = matchPattern(lhs, m_Constant()) ? lhs : rhs;
if (guaranteedNonNegativeResult(cst, rewriter)) {
rewriter.replaceOpWithNewOp<stablehlo::AddOp>(
op, stablehlo::LogOp::create(rewriter, op.getLoc(), lhs),
stablehlo::LogOp::create(rewriter, op.getLoc(), rhs));
return success();
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fails when the constant is exactly 0 and the variable is negative. Because guaranteedNonNegativeResult allows the constant to be exactly 0
if cst = 0 and the variable is -2:
Original: $\log(0 \cdot -2) \rightarrow \log(-0.0) \rightarrow \mathbf{-\infty}$
Optimized: $\log(0) + \log(-2) \rightarrow -\infty + \text{NaN} \rightarrow \mathbf{\text{NaN}}$

Similar problem as before, and I still think this should not necessarily block the PR since the status quo is actually a good bit worse.

Comment on lines +27858 to +27867
// Only sound when the constant operand is non-negative: a negative
// constant makes log(const) NaN where log(a/b) was a finite real.
// Issue #2570.
Value cst = matchPattern(lhs, m_Constant()) ? lhs : rhs;
if (guaranteedNonNegativeResult(cst, rewriter)) {
rewriter.replaceOpWithNewOp<stablehlo::SubtractOp>(
op, stablehlo::LogOp::create(rewriter, op.getLoc(), lhs),
stablehlo::LogOp::create(rewriter, op.getLoc(), rhs));
return success();
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same problem and still do not think this should be blocking

Fails when the numerator ($a$) is the constant 0, and the denominator ($b$) is negative.The code allows the constant to be either the LHS ($a$) or the RHS ($b$). If the constant happens to be the LHS, and it equals 0, we hit the same trap.
$a = 0$ and $b = -2$:
Original: $\log(0 / -2) \rightarrow \log(-0.0) \rightarrow \mathbf{-\infty}$
Optimized: $\log(0) - \log(-2) \rightarrow -\infty - \text{NaN} \rightarrow \mathbf{\text{NaN}}$(Note: If the denominator $b$ is the constant 0, and $a$ is negative, both sides correctly evaluate to NaN. The bug only exists when the numerator is 0).

@kimjune01

kimjune01 commented Jul 16, 2026

Copy link
Copy Markdown
Contributor Author

Good catches, thanks. I tightened the patch:

  • removed the log(pow(x, y)) rewrite, since non-negativity isn’t enough;
  • require strictly positive constants for the multiply rewrite and for a constant division numerator, while preserving the safe zero-denominator case;
  • added regressions for the zero and pow cases.

But even with positive constants, the multiply/divide identities aren’t fully IEEE-equivalent because rounding, overflow, and underflow can differ. It may be out of scope. If required, those rewrites may need separate gating or removal.

@wsmoses
wsmoses requested a review from sbrantq July 21, 2026 20:50
@wsmoses

wsmoses commented Jul 22, 2026

Copy link
Copy Markdown
Member

@Antipath1 and I spoke about this during the meeting today.

@kimjune01 rather than folding to an abs (which may be slower than the mul), what if you gated the transformation on the result of the is value positive analysis

Per review, replace the abs-based log(mul(a,a)) -> 2*log(abs(a)) rewrite
with a guard on guaranteedNonNegativeResult, emitting 2*log(a) only when a
is provably non-negative. Avoids inserting an abs (potentially slower than
the mul) while staying correct for a < 0. Updates lit tests and adds
positive/negative coverage.
@kimjune01

Copy link
Copy Markdown
Contributor Author

Done -- dropped the abs and gated on guaranteedNonNegativeResult as suggested. It now folds log(a*a) -> 2*log(a) only when a is provably non-negative (the a*a, abs, exp/sqrt producers), and leaves log(a*a) untouched otherwise. Added positive + negative lit coverage.

While wiring this up I hit a latent soundness bug in that analysis's square rule -- it compares defining ops, so mul(%argN, %argM) of two distinct block args gets null == null and is wrongly proven non-negative. Filed separately as #2648 to keep this PR focused; the gate here is correct against the analysis contract and doesn't regress current behavior.

// domain for negative or zero constants. This does not make the
// rewrite bit-exact under floating-point rounding. See issue #2570.
Value cst = matchPattern(lhs, m_Constant()) ? lhs : rhs;
if (isValidLogSplitConstant(cst, /*allowZero=*/false)) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you rename this from isValidLogSplitConstant to isPositiveFiniteConstant for clarity

@wsmoses wsmoses left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm modulo the fn name for simplicity

Per review, clearer name for the log-split constant guard.
@kimjune01

Copy link
Copy Markdown
Contributor Author

Renamed to isPositiveFiniteConstant in aa065977.

One nuance on the name so it doesn't mislead later: the predicate currently accepts +inf (it rejects NaN and negatives but not infinity), and in the division-denominator case it's called with allowZero=true, so there it's really "non-negative finite-or-inf" rather than strictly positive. Happy to tighten the body to match the name (reject +inf, and/or split the zero-allowing path) if you'd like -- just flagging that it's a behavior change rather than folding it into the rename.

@wsmoses

wsmoses commented Jul 22, 2026

Copy link
Copy Markdown
Member

maybe the isPositiveNonNanConstant then?

Per review, the predicate accepts +inf, so isPositiveNonNanConstant names it
more accurately than isPositiveFiniteConstant.
@kimjune01

Copy link
Copy Markdown
Contributor Author

Good call -- isPositiveNonNanConstant it is (b76081e1). That name is honest about what the predicate checks (rejects NaN and negatives, doesn't assert finiteness).


LogicalResult matchAndRewriteImpl(stablehlo::LogOp op,
PatternRewriter &rewriter) const {
auto isPositiveNonNanConstant = [](Value value, bool allowZero) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sorry one final minor comment, can you make this a static member function rather than a lambda?

it'll be easier to debug in the future [and faster to compile/run]

Per review, hoist the log-split constant predicate out of matchAndRewriteImpl
into a static member function for easier debugging and faster compile.
@kimjune01

Copy link
Copy Markdown
Contributor Author

Done in 51ec6b63 -- isPositiveNonNanConstant is now a static member of LogSimplify instead of a local lambda.

Tighten the domain-guard comments toward the file's terse style: drop the
duplicated 'provably non-negative' phrasing and the repeated bit-exactness
caveat, keep the non-obvious reasoning.

@Antipath1 Antipath1 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm thank you :)

@wsmoses

wsmoses commented Jul 22, 2026

Copy link
Copy Markdown
Member

ci fails:


[17,048 / 18,101] 16 / 1069 tests; Testing //test/lit_tests:mpi_comm_rank.mlir.test; 17s remote-cache, processwrapper-sandbox ... (32 actions running)
[17,050 / 18,101] 18 / 1069 tests; Testing //test/lit_tests:dus_to_pad.mlir.test; 18s remote-cache, processwrapper-sandbox ... (32 actions running)
[17,050 / 18,101] 18 / 1069 tests; Testing //test/lit_tests:dus_to_pad.mlir.test; 19s remote-cache, processwrapper-sandbox ... (32 actions running)
[17,053 / 18,101] 21 / 1069 tests; Testing //test/lit_tests:dus_to_pad.mlir.test; 20s remote-cache, processwrapper-sandbox ... (32 actions running)
FAIL: //test/lit_tests:logsimplify.mlir.test (see /root/.bazel/execroot/__main__/bazel-out/aarch64-opt/testlogs/test/lit_tests/logsimplify.mlir.test/test.log)
INFO: From Testing //test/lit_tests:logsimplify.mlir.test:
==================== Test output for //test/lit_tests:logsimplify.mlir.test:
-- Testing: 1 tests, 1 workers --
FAIL: Enzyme-JaX :: lit_tests/logsimplify.mlir (1 of 1)
******************** TEST 'Enzyme-JaX :: lit_tests/logsimplify.mlir' FAILED ********************
Exit Code: 1

Command Output (stderr):
--
enzymexlamlir-opt /root/.bazel/sandbox/processwrapper-sandbox/6746/execroot/__main__/bazel-out/aarch64-opt/bin/test/lit_tests/logsimplify.mlir.test.runfiles/__main__/test/lit_tests/logsimplify.mlir --pass-pipeline="builtin.module(enzyme-hlo-generate-td{patterns=log_simplify;log_const_prop},transform-interpreter,enzyme-hlo-remove-transform)" | FileCheck /root/.bazel/sandbox/processwrapper-sandbox/6746/execroot/__main__/bazel-out/aarch64-opt/bin/test/lit_tests/logsimplify.mlir.test.runfiles/__main__/test/lit_tests/logsimplify.mlir # RUN: at line 1
+ enzymexlamlir-opt /root/.bazel/sandbox/processwrapper-sandbox/6746/execroot/__main__/bazel-out/aarch64-opt/bin/test/lit_tests/logsimplify.mlir.test.runfiles/__main__/test/lit_tests/logsimplify.mlir '--pass-pipeline=builtin.module(enzyme-hlo-generate-td{patterns=log_simplify;log_const_prop},transform-interpreter,enzyme-hlo-remove-transform)'
+ FileCheck /root/.bazel/sandbox/processwrapper-sandbox/6746/execroot/__main__/bazel-out/aarch64-opt/bin/test/lit_tests/logsimplify.mlir.test.runfiles/__main__/test/lit_tests/logsimplify.mlir
/root/.bazel/sandbox/processwrapper-sandbox/6746/execroot/__main__/bazel-out/aarch64-opt/bin/test/lit_tests/logsimplify.mlir.test.runfiles/__main__/test/lit_tests/logsimplify.mlir:85:16: error: CHECK-NEXT: is not on the line after the previous match
// CHECK-NEXT: %0 = stablehlo.multiply %arg0, %arg0 : tensor<f64>
               ^
<stdin>:55:2: note: 'next' match was here
 %0 = stablehlo.multiply %arg0, %arg0 : tensor<f64>
 ^
<stdin>:30:61: note: previous match ended here
 %cst = stablehlo.constant dense<2.000000e+00> : tensor<f64>
                                                            ^
<stdin>:31:1: note: non-matching line after previous match is here
 %0 = stablehlo.multiply %arg0, %arg0 {enzymexla.non_negative = [#enzymexla<guaranteed GUARANTEED>]} : tensor<f64>
^

Input file: <stdin>
Check file: /root/.bazel/sandbox/processwrapper-sandbox/6746/execroot/__main__/bazel-out/aarch64-opt/bin/test/lit_tests/logsimplify.mlir.test.runfiles/__main__/test/lit_tests/logsimplify.mlir

-dump-input=help explains the following input dump.

Input was:
<<<<<<
           .
           .
           .
          25:  %0 = stablehlo.power %cst, %arg0 : tensor<f64> 
          26:  %1 = stablehlo.log %0 : tensor<f64> 
          27:  return %1 : tensor<f64> 
          28:  } 
          29:  func.func @main2(%arg0: tensor<f64>) -> tensor<f64> { 
          30:  %cst = stablehlo.constant dense<2.000000e+00> : tensor<f64> 
next:85'0                                                                {   search range start (exclusive)
          31:  %0 = stablehlo.multiply %arg0, %arg0 {enzymexla.non_negative = [#enzymexla<guaranteed GUARANTEED>]} : tensor<f64> 
          32:  %1 = stablehlo.log %0 : tensor<f64> 
          33:  %2 = stablehlo.multiply %cst, %1 : tensor<f64> 
          34:  return %2 : tensor<f64> 
          35:  } 
           .
           .
           .
          50:  %0 = stablehlo.log %arg0 : tensor<f64> 
          51:  %1 = stablehlo.add %cst, %0 : tensor<f64> 
          52:  return %1 : tensor<f64> 
          53:  } 
          54:  func.func @main6(%arg0: tensor<f64>) -> tensor<f64> { 
          55:  %0 = stablehlo.multiply %arg0, %arg0 : tensor<f64> 
next:85'1      !~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~   error: match on wrong line
          56:  %1 = stablehlo.log %0 : tensor<f64> 
          57:  return %1 : tensor<f64> 
          58:  } 
          59:  func.func @main7(%arg0: tensor<f64>) -> tensor<f64> { 
          60:  %cst = stablehlo.constant dense<1.3862943611198906> : tensor<f64> 
           .
           .
           .
         109:  %1 = stablehlo.log %0 : tensor<f64> 
         110:  %2 = stablehlo.multiply %cst, %1 : tensor<f64> 
//test/lit_tests:while_licm.mlir.test                                    PASSED in 0.2s
//test/lit_tests:while_noop.mlir.test                                    PASSED in 0.2s
//test/lit_tests:while_pad_induction_reduction.mlir.test                 PASSED in 0.2s
//test/lit_tests:while_repeated_induction_reduction.mlir.test            PASSED in 0.2s
//test/lit_tests:while_simplify.mlir.test                                PASSED in 0.2s
//test/lit_tests:whileconcat.mlir.test                                   PASSED in 0.2s
//test/lit_tests:whiledeadarg.mlir.test                                  PASSED in 0.3s
//test/lit_tests:whiledus.mlir.test                                      PASSED in 0.2s
//test/lit_tests:whiledus2.mlir.test                                     PASSED in 0.2s
//test/lit_tests:whileupdatewithoutcorners.mlir.test                     PASSED in 0.2s
//test/lit_tests:whilewrap.mlir.test                                     PASSED in 0.2s
//test/lit_tests:widen_extend.mlir.test                                  PASSED in 0.2s
//test/lit_tests:widen_wrap.mlir.test                                    PASSED in 0.2s
//test/lit_tests:widen_wrap_reshape.mlir.test                            PASSED in 0.2s
//test/lit_tests:wrap_const.mlir.test                                    PASSED in 0.2s
//test/lit_tests:wrap_unary_elementwise.mlir.test                        PASSED in 0.2s
//test/lit_tests:wrapelementwise.mlir.test                               PASSED in 0.2s
//test/lit_tests:xlawrapper_alloca.mlir.test                             PASSED in 0.2s
//test/lit_tests:xor.mlir.test                                           PASSED in 0.2s
//test/lit_tests:zerobasepowsimplify.mlir.test                           PASSED in 0.2s
//test/lit_tests:zeroproductreshapepad.mlir.test                         PASSED in 0.2s
//test/lit_tests:logsimplify.mlir.test                                   FAILED in 0.9s
  /root/.bazel/execroot/__main__/bazel-out/aarch64-opt/testlogs/test/lit_tests/logsimplify.mlir.test/test.log

The guaranteedNonNegativeResult gate stamps enzymexla.non_negative on the
inner arg0*arg0 when the log(mul(sq,sq)) rewrite queries it, so the CHECK-NEXT
must match the annotated op. main6 is unaffected (arg0 itself is queried, not
the multiply).
@kimjune01

Copy link
Copy Markdown
Contributor Author

Fixed in 48aaf2b1. The failure was the main2 CHECK block: the non-negativity gate stamps enzymexla.non_negative on the inner arg0*arg0 when the log(mul(sq,sq)) rewrite queries the analysis, so FileCheck skipped past it to main6's unannotated multiply and flagged a wrong-line match. Updated the CHECK-NEXT to match the annotated op. main6 is untouched (there arg0 itself is queried, not the multiply, so no stamp). Only the n2/build lit lane was real; the TPU/GPU/macOS lanes are red on main too.

isPositiveNonNanConstant passed +inf (only NaN and negatives were
rejected), so log(x * inf) / log(x / inf) could split and flip a
finite/NaN result to the other. Gate on isFinite() and rename to
isPositiveFiniteConstant (now accurate). Adds an inf-mul guard test.
Found by codex review of EnzymeAD#2576.
@kimjune01

Copy link
Copy Markdown
Contributor Author

Also excluded infinite constants from the splits.

log(mul(abs,abs)) queries guaranteedNonNegativeResult(abs), which stamps
enzymexla.non_negative on the abs op; the CHECK expected a bare abs. Same
stamping pattern as main2. This was the last unreached CHECK, so it never
surfaced until the earlier lanes went green.
@wsmoses
wsmoses merged commit d9c601e into EnzymeAD:main Jul 23, 2026
11 of 22 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

log(a*a) → 2*log(a) returns NaN for negative inputs

3 participants