Conversation
`value(T, d)`, `partials(T, d, i)` and `valtype(T, D)` now descend through layers with other tags instead of throwing `DualMismatchError` when `T` is not the outermost tag. If `T` does not occur at all, the value is the number itself and the partials are zero, regardless of the order of tags. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
`value(x)`, `partials(x)`, `partials(x, i...)`, `npartials(d)` and
`partials(T, x, i, j...)` extract the outermost layer of a nested `Dual`,
which depends on the order of the tags. They are deprecated in favor of
`value(T, x)`, `partials(T, x)`, `partials(T, x, i)` and
`npartials(T, D)`, which all internal code now uses.
Dispatching on the tag requires it to be a type, so constructing a `Dual`
with a non-type tag such as `Dual{1}` now throws an `ArgumentError`.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Replace the impure `tagcount` counter with a total order on tag types that depends only on their structure, so it is the same in every session and unaffected by precompilation. A generated function computes a constant `(rank, key)` ID per tag, and `≺` compares the IDs, which is evaluated at compile time. The rank strictly increases from a type to any type containing it, so every tag is greater than the tags in its parameters and seeding outermost keeps nested `Dual`s sorted. The `Dual` constructor rejects a tag stored outside a greater tag. Since every pair of tags is now ordered, `DualMismatchError` is removed. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Nesting a `Dual` in a `Dual` with the same tag merges two perturbations,
so the `Dual` constructor now requires every tag to be strictly greater
than the tags of its value and partials. It checks their runtime types, so
tags hidden behind an abstract value type are rejected as well.
Hence `HessianConfig` uses a separate tag `Tag{F,Dual{T,V,N}}` for its
gradient layer (#845), which adds a type parameter, and so does `hessian!`
for StaticArrays with immutable results. The latter now reads the gradient
from the gradient layer, as all other Hessian paths do, so that all paths
agree with the Jacobian of the gradient. Untagged `Dual`s and configs
created with `f = nothing` use the tag `Tag{Nothing,V}` instead of
`Nothing`, where `V` is the promoted value type, so that nesting them
yields distinct tags. Promoting `Dual`s
with the same tag but different numbers of partials throws an error
instead of nesting the tag in itself.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #853 +/- ##
==========================================
+ Coverage 91.23% 93.54% +2.31%
==========================================
Files 11 12 +1
Lines 1072 1146 +74
==========================================
+ Hits 978 1072 +94
+ Misses 94 74 -20 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Contributor
|
The new structure switches the need for tagging API away from the tag type itself and into For example, #313 could be resolved by the owner of the mutable struct by defining an appropiate #815 conflicts with this PR, because what needs to be exposes as an API changes. so merging this PR should close #815. |
…and cover new lines
- `Dual{T}(args...)` and `Dual(args...)` take the value type from the partials, so
e.g. `zero(Dual{T,Real,N})` and `convert(Dual{T,Real,N}, x)` work again
- `promote_rule` for the same tag with different numbers of partials returns `Union{}`
instead of throwing, so `promote_type` falls back to `typejoin`
- Include the package UUID in the tag key, so types of packages with the same name are
distinguished
- Remove the redundant `Tag(::Nothing, V)` method
- Test `≺` with symbols, values and `Vararg`s as parameters, `npartials` without the tag,
the deprecated `partials(x, i)` and `show`
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With an abstract input type, the tag of an inner call can be smaller than tags of the input elements. Seeding now keeps layers with greater tags outside, and the work buffers use a bound on the seeded type (`Real` if the value type is abstract and can contain `Dual`s). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With an abstract value type, the components of a `Dual` can carry different tag layers, so the type no longer determines where a tag occurs. Seeding now converts the seeds to the type of each element, so elements of abstract input arrays are seeded with concrete value types. 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.
Prototype of a redesign of the tag machinery, opened as a draft to discuss the design. Fixes #714, #801 and #845.
Motivation
Nested
Duals represent a tensor product D_A ⊗ D_B of dual-number algebras, one factor per tag. Preventing perturbation confusion requires onlyTis absent).An order of tags is not needed mathematically. It only fixes a canonical storage order, so that binary operations just compare the outermost tags, each element has a single type, and seeding is plain wrapping. But since operations rely on it, the order must be the same in all code exchanging
Duals, including precompiled code.tagcountviolates this (#714, #801).Changes
value(T, d),partials(T, d),partials(T, d, i),valtype(T, D)descend through layers with other tags and return identity/zero ifTis absent, independent of the order (R2).tagcountis replaced by a structural total order: a generated function returns a constant(rank, key...)ID per tag, and≺compares IDs (folded at compile time). The rank strictly increases under containment, soTag{F,V}is greater than all tags inFandV.DualMismatchErroris removed. The ID itself could be improved; what matters is that it is unique per tag, totally ordered and independent of the session, and that≺is folded to a constantBoolat compile time, astagcountcomparisons were.Dualare unique (R1 within a number). HenceHessianConfiguses a separate gradient tag (hessian: the two nested dual layers share a tag, so results depend on the code path and chunk size #845), and untaggedDuals andf = nothingconfigs useTag{Nothing,V}, so nesting them yields distinct tags by containment. Similar to the strict order, and for efficiency reasons, the constructor also requires a concrete value type (e.g.Dual{T,Real}is disallowed), so the type of aDualdetermines all its tags. Containment does not guarantee that the tag of a call is greater than the tags of its input (e.g. an abstract input type hides them), so seeding keeps layers of the input with greater tags outside. Elements of abstract input arrays are seeded with their own concrete types, in work buffers with element typeReal.Results (ForwardDiff 1.4.6 vs. this PR)
jacobianofgradient, #238, #267, constant inner derivative, nesting not ordered by containment, nested calls with abstract input typesCannot determine ordering of Dual tagshessian,hessian!,DiffResults, chunk sizes,SVectorjacobianofgradientor throwDualwith the same tag or a greater tag insideDualsDual{Nothing,Dual{Nothing}}, confusionInvalidTagException, correct withVal(false)derivative/gradient(rosen, 100)/hessian(rosen, 100)Comparison with other PRs
tagcountand raisesTAGCOUNTwhen aTagis constructed at runtime. It does not cover tags that are only constructed as types, and the order still depends on the session.tagcount. They still assume that the order matches the nesting, which containment does not guarantee: the review of Order nested Dual tags by provable containment (supersedes #807) #820 shows aDualbuffer in the differentiated function that makes correct code throw, and that≺is no longer transitive. Here containment only determines the rank in a total order, and extraction does not rely on the order matching the nesting (R2). The counterexamples from the reviews of Add tagdepth fast path to tag comparison #807 and Order nested Dual tags by provable containment (supersedes #807) #820 are correct on this branch.SmallTagtype for more compactDualtypes #748 (reverted) identified tags by hash, which is unsound since hashes of distinct types can collide. Here tags are still compared as types.Tag(see below) would conflict with it.Breaking changes
Duals andf = nothingconfigs have tagTag{Nothing,V}instead ofNothing.Dualin aDualwith the same or a greater tag throws an error.Duals with a non-concrete value type, such asDual{T,Real}, throw an error. Packages allocating their ownDualbuffers for abstract input arrays are affected.HessianConfigis parametrized on the type of its gradient config; work buffers for abstract input arrays have element typeReal;DualMismatchErroris removed; non-type tags throw an error.Not addressed
R1 across numbers:
Tag{F,V}is a static proxy for a fresh tag per call, so calls with the same function and input types share a tag.ScopedValuewould detect this on entry and could throw a descriptive error (or retag the inner call).Dualleaks from a finished call through mutable state (aCore.Box) into a later call with the same tag. The tag is no longer active, and types cannot distinguish the staleDualfrom a fresh one. Detecting this would require walking the state reachable fromfandx(incomplete, e.g. globals) or a per-call identity stored in eachDual(too costly).Another possible follow-up is restricting tags to
Tag(no custom≺). The same analysis applies to ReverseDiff#45; a small shared interface (active tags plus descending through wrappers of other packages) could let both packages interoperate.🤖 Generated with Claude Code