Skip to content

Add ChainRules rule for sqrt via a ChainRulesCore extension - #111

Open
devmotion wants to merge 6 commits into
masterfrom
dmw/chainrules-sqrt
Open

devmotion wants to merge 6 commits into
masterfrom
dmw/chainrules-sqrt

Conversation

@devmotion

@devmotion devmotion commented Oct 16, 2026 •

Copy link
Copy Markdown
Member

Fixes #109 and JuliaDiff/ChainRules.jl#844.

For IEEE floats, NaNMath.sqrt calls the intrinsic sqrt_llvm, so Zygote fails with "Non-differentiable function Core.Intrinsics.sqrt_llvm". A rule on the intrinsic can't go in ChainRules: all intrinsics share the type Core.IntrinsicFunction, so it would apply to every unary intrinsic. A rule on NaNMath.sqrt only matches this function.

This PR adds frule and rrule for NaNMath.sqrt(::Union{Float16, Float32, Float64}) in a ChainRulesCore extension, and bumps the version to 1.2.0. Compared with ChainRules' rule for Base.sqrt:

🤖 Generated with Claude Code

For `Float16`, `Float32` and `Float64`, `NaNMath.sqrt` calls the intrinsic
`Base.Intrinsics.sqrt_llvm`, which reverse-mode AD such as Zygote cannot
differentiate through. A rule for the intrinsic can't be added to ChainRules:
all intrinsics share the type `Core.IntrinsicFunction`, so it would also apply
to every other unary intrinsic. A rule on `NaNMath.sqrt` only matches this
function.

Fixes #109, JuliaDiff/ChainRules.jl#844.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@codecov

codecov Bot commented Oct 16, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 96.93%. Comparing base (405f31d) to head (847161e).
⚠️ Report is 2 commits behind head on master.

Additional details and impacted files
@@            Coverage Diff             @@
##           master     #111      +/-   ##
==========================================
+ Coverage   96.73%   96.93%   +0.20%     
==========================================
  Files           1        2       +1     
  Lines         153      163      +10     
==========================================
+ Hits          148      158      +10     
  Misses          5        5              

☔ 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.

devmotion and others added 3 commits October 16, 2026 14:00
Multiplying a zero (co)tangent with the derivative `Inf` at `x = 0` or `NaN`
at `x < 0` gave `NaN`, e.g. in `ifelse(x > 0, NaNMath.sqrt(x), 0)`; zero
(co)tangents now stay zero (cf. JuliaDiff/ChainRules.jl#576). At `x = -0.0`,
where `NaNMath.sqrt` returns `-0.0`, the derivative was `-Inf` instead of
`Inf`.

Tests use the ChainRulesTestUtils defaults for `Float64`, at inputs far enough
from 0 that finite differencing stays in the domain, and test the `frule`
closer to 0 with tangents scaled to the input. `Float16` and `Float32` are
compared with the `Float64` rules, since finite differencing is too inaccurate
in lower precision.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Comment thread test/runtests.jl Outdated
devmotion and others added 2 commits October 24, 2026 02:06
On x86, LLVM folds `x >= 0 ? sqrt_llvm(x) : NaN` into a bare `sqrtsd`,
which returns a negative NaN, while const-folded calls return the positive
`NaN` literal. With coverage enabled (as in CI) both occur in the tests,
so bitwise `===` comparisons of the NaN outputs failed on Julia 1.13.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@oscardssmith

Copy link
Copy Markdown
Member

why is the derivative different than that of sqrt?

@devmotion

Copy link
Copy Markdown
Member Author

What difference? The 0 at 0? That's a bug of the generic sqrt rule, see JuliaDiff/ChainRules.jl#599 and JuliaDiff/ChainRulesCore.jl#551

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.

NaNMath.sqrt cannot be differentiated by Zygote

2 participants