Repository navigation
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:
|
|
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>
|
Generally I like it, but isn't the following part sufficient to solve the tagcount problems?
Of course the other points might be useful, but this above seems to be the key aspect. |
|
I'd argue it's not. In theory, you only need R1 and R2. In practice, you need something that is a safe and efficient implementation of both conditions. With tagless partial or value extraction it's unsafe (point 1). Without strictly monotonically ordered nested duals any operations on duals will be inefficient (point 4). An order that often does not put the currently active level of differentiation on the top level of the duals will be inefficient as well (point 3). But the exact definition of the order is arguably secondary to the proposal. I'm still not convinced the proposed definition is a good choice, it's actually the only point I'm still unsure about. It still feels too much of a hack to me. |
Which part exactly? To be fair I only have a limited understanding of which changes this PR is proposing. I only meant to comment that the general idea looked reasonable to me at a glance. ;) |
|
Sorry, that was a bit unclear. I was referring to the choice of
|
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