Repository navigation
Conversation
The generic implementations of `norm` in LinearAlgebra and StaticArrays rescale by the maximum absolute value to avoid overflow and underflow. Their branches (`iszero`, `isfinite`, `<`) take partials into account for `Dual`s, resulting in NaNs, Infs, and even incorrect primal values (e.g., at zero or for very small or large inputs). Instead, the norm of the primal values is computed and the chain rule is applied with weights that are bounded by 1 in absolute value. At zero, the subgradient zero is used (as in ChainRules). Derivatives with respect to `p` are supported as well. Additionally, `hypot` uses the same convention at zero, and the three-argument `hypot` no longer drops the partials of outer tags in nested differentiation. Fixes #243, fixes #794. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #872 +/- ##
==========================================
+ Coverage 91.24% 92.24% +1.00%
==========================================
Files 11 12 +1
Lines 1085 1174 +89
==========================================
+ Hits 990 1083 +93
+ Misses 95 91 -4 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
- Compute the partials of `norm(x, p)` with respect to `x` for all `p` in one code path: the derivative is zero for `p = 0` (whose primal value was incorrect), only the first element with the largest (smallest) absolute value contributes for `p = Inf` (`p = -Inf`), and zero elements get the subgradient zero (also for `p = 1`). - Compute the derivative factors in the precision of the primal norm, which fixes a type instability for Float32 inputs. - Do not dispatch on `p.value` when differentiating with respect to both `x` and `p`: with the same tag at multiple levels (as in `hessian`), it was confused with `p` (#845). - Support tuples (promoted to a common element type) and `hypot` with four or more arguments, and use the tuple of values of `Partials`. - Reinterpret dense arrays of Duals without copying (strided, e.g., for BLAS), and sum the partials without allocating. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #243, fixes #794.
Problem
The generic implementations of
norm(LinearAlgebra.generic_norm2/generic_normpand StaticArrays'_norm/_norm_scaled) andhypotwith four or more arguments (Base.Math._hypot) rescale by the maximum absolute value to avoid overflow and underflow. Their branches (iszero,isfinite,<) take partials into account forDuals (<since #791), soDuals end up in the wrong branch:gradient(norm, [0.0, 0.0])[NaN, NaN][0.0, 0.0]gradient(x -> norm(x)^2, [0.0, 0.0])[NaN, NaN][0.0, 0.0]gradient(x -> norm(x, 3), [0.0, 0.0])[NaN, NaN][0.0, 0.0]norm([Dual(1e-200, 1e-100), Dual(0.0, 0.0)])Dual(0.0, Inf)(wrong primal)Dual(1e-200, 1e-100)gradient(norm, [1e-200, 1e-200])[Inf, Inf][0.707, 0.707]norm(SA[Dual(0, 1), Dual(0, 1), Dual(0, 1)])Dual(NaN, NaN)Dual(0.0, 0.0)norm((Dual(0.0, 1.0), Dual(0.0, 1.0)))Dual(NaN, NaN)Dual(0.0, 0.0)norm([Dual(0.0, 1.0), Dual(1.0, 0.0)], 0)Dual(2.0, 0.0)(wrong primal)Dual(1.0, 0.0)derivative(p -> norm([3.0, 4.0], p), 3.0)MethodError-0.304hypot(Dual(0.0, 1.0), Dual(0.0, 1.0))Dual(0.0, NaN)Dual(0.0, 0.0)hypot(Dual(0.0, 1.0), Dual(0.0, 1.0), Dual(0.0, 1.0), Dual(0.0, 1.0))Dual(NaN, NaN)Dual(0.0, 0.0)All of these fail with NaN-safe mode enabled as well.
Approach
Similar to the rules in ChainRules, the norm of the primal values is computed with the existing (scale-robust) implementations, and the chain rule is applied with weights
w_i = |u_i|^(p-1) * sign(u_i), whereu_i = x_i / norm(x, p):p ≥ 1andp < 0, so they cannot overflow or underflow there. For0 < p < 1they are at least 1, as is the true derivative.puse the same code path: the derivative is zero forp = 0, and only the first element with the largest (smallest) absolute value contributes forp = Inf(p = -Inf).norm(x, p) == 0and for zero elementsx_i == 0, the subgradient zero is used. ChainRules uses the same convention (viapinvandsign).Float32inputs giveFloat32results).p:norm(x, p) / p * sum(|u_i|^p * log(|u_i|)). This is bounded and avoids the cancellation insum(|x_i|^p * log(|x_i|)) - norm(x, p) * log(norm(x, p)).The following are supported:
xandpStaticArrays,Adjoint/Transposeand sparse arrays (new weak dependency/extension)Method ambiguities with LinearAlgebra, StaticArrays and SparseArrays are resolved explicitly.
hypotuses the same convention at zero. With four or more arguments it reuses the rule fornorm: Base promotes the arguments and callsBase.Math._hypotwith a tuple, and that internal function has the same signature on Julia 1.10 through 1.14-alpha. Additionally, the three-argumenthypotno longer drops the partials of outer tags in nested differentiation (it used the tag-agnosticvalue/partials; this overlaps with #835).Performance
Dense arrays of
Duals with isbits values (including nestedDuals) are reinterpreted without copying, so the primal norm of realDuals uses BLASnrm2on a strided view. The partials are summed without allocating (a lazy broadcastsumfor arrays,sum(map(...))for tuples andStaticArrays).norm(x),x::Vector{<:Dual}, length 100norm(x, Inf), length 100norm(x), length 3For very short vectors, master's single fused (but unscaled) pass is faster than the two passes needed here: the scale-robust primal norm, then the bounded weights.
Behavior changes
norm(x, 1)uses the subgradient zero for zero elements, as for all otherp. For example,gradient(x -> norm(x, 1), [1.0, 0.0, -2.0])returns[1.0, 0.0, -1.0]instead of[1.0, 1.0, -1.0].NaNpartials, e.g.,norm([Dual(Inf, 0.0), Dual(1.0, 1.0)]) == Dual(Inf, NaN)instead ofDual(Inf, 0.0): their weight isInf / Inf. With NaN-safe mode enabled, the result isDual(Inf, 0.0).Limitations
normat zero remain incorrect (e.g.,hessian(x -> norm(x)^2, zeros(2))returns zeros instead of2I): Sincenormis not differentiable at zero, no choice of partials ofnorm(x)can recover the correct result. This is documented in the limitations section of the docs;sum(abs2, x)should be used instead.Duals with non-Dualnumbers (e.g.,norm((Dual(0.0, 1.0), 0.0))) still use the generic implementation: dispatching on "some element is aDual" would require type piracy.normof their elements.🤖 Generated with Claude Code