From 0b0989f6d6be540833fa75b51b6a4238904a1b25 Mon Sep 17 00:00:00 2001 From: Cody Tapscott Date: Wed, 23 Apr 2025 01:42:38 -0400 Subject: [PATCH 1/2] Add `SmallTag` type This is an alternative to `Tag` that provides largely the same functionality, but carries around only the hash of the function / array types instead of the full types themselves. This can make these types much less bulky to print and easier to visually scan for. --- src/config.jl | 97 +++++++++++++++++++++++++++++++++++++++++--------- src/prelude.jl | 5 +++ 2 files changed, 85 insertions(+), 17 deletions(-) diff --git a/src/config.jl b/src/config.jl index d561b4fd..c8d2791a 100644 --- a/src/config.jl +++ b/src/config.jl @@ -20,11 +20,50 @@ end Tag(::Nothing, ::Type{V}) where {V} = nothing - @inline function ≺(::Type{Tag{F1,V1}}, ::Type{Tag{F2,V2}}) where {F1,V1,F2,V2} tagcount(Tag{F1,V1}) < tagcount(Tag{F2,V2}) end +""" + HashTag{Hash} + +HashTag is similar to a Tag, but carries just a small UInt64 hash, +instead of the full type, which makes stacktraces / types easier to +read while still providing good resilience to perturbation confusion. +""" +struct HashTag{H} +end + +@generated function tagcount(::Type{HashTag{H}}) where {H} + :($(Threads.atomic_add!(TAGCOUNT, UInt(1)))) +end + +function HashTag(f::F, ::Type{V}) where {F,V} + H = if F <: Tuple + # no easy way to check Jacobian tag used with Hessians as multiple functions may be used + # see checktag(::Type{Tag{FT,VT}}, f::F, x::AbstractArray{V}) where {FT<:Tuple,VT,F,V} + nothing + else + hash(F) ⊻ hash(V) + end + tagcount(HashTag{H}) # trigger generated function + HashTag{H}() +end + +HashTag(::Nothing, ::Type{V}) where {V} = nothing + +@inline function ≺(::Type{HashTag{H1}}, ::Type{Tag{F2,V2}}) where {H1,F2,V2} + tagcount(HashTag{H1}) < tagcount(Tag{F2,V2}) +end + +@inline function ≺(::Type{Tag{F1,V1}}, ::Type{HashTag{H2}}) where {F1,V1,H2} + tagcount(Tag{F1,V1}) < tagcount(HashTag{H2}) +end + +@inline function ≺(::Type{HashTag{H1}}, ::Type{HashTag{H2}}) where {H1,H2} + tagcount(HashTag{H1}) < tagcount(HashTag{H2}) +end + struct InvalidTagException{E,O} <: Exception end @@ -36,13 +75,22 @@ checktag(::Type{Tag{FT,VT}}, f::F, x::AbstractArray{V}) where {FT,VT,F,V} = checktag(::Type{Tag{F,V}}, f::F, x::AbstractArray{V}) where {F,V} = true +# HashTag is a smaller tag, that only confirms the hash +function checktag(::Type{HashTag{HT}}, f::F, x::AbstractArray{V}) where {HT,F,V} + H = hash(F) ⊻ hash(V) + if HT == H || HT === nothing + true + else + throw(InvalidTagException{HashTag{H},HashTag{HT}}()) + end +end + # no easy way to check Jacobian tag used with Hessians as multiple functions may be used checktag(::Type{Tag{FT,VT}}, f::F, x::AbstractArray{V}) where {FT<:Tuple,VT,F,V} = true # custom tag: you're on your own. checktag(z, f, x) = true - ################## # AbstractConfig # ################## @@ -55,6 +103,21 @@ Base.eltype(cfg::AbstractConfig) = eltype(typeof(cfg)) @inline (chunksize(::AbstractConfig{N})::Int) where {N} = N +@inline function maketag(f, X; style::Union{Symbol,Nothing} = nothing) + if style === :hash + return HashTag(f, X) + elseif style === :type + return Tag(f, X) + elseif style === nothing + if HASHTAG_MODE_ENABLED + return HashTag(f, X) + else + return Tag(f, X) + end + end + error("unexpected tag style: $(style)") +end + #################### # DerivativeConfig # #################### @@ -108,9 +171,9 @@ vector `x`. The returned `GradientConfig` instance contains all the work buffers required by `ForwardDiff.gradient` and `ForwardDiff.gradient!`. -If `f` is `nothing` instead of the actual target function, then the returned instance can -be used with any target function. However, this will reduce ForwardDiff's ability to catch -and prevent perturbation confusion (see https://github.com/JuliaDiff/ForwardDiff.jl/issues/83). +If `f` or `tag` is `nothing`, then the returned instance can be used with any target function. +However, this will reduce ForwardDiff's ability to catch and prevent perturbation confusion +(see https://github.com/JuliaDiff/ForwardDiff.jl/issues/83). This constructor does not store/modify `x`. """ @@ -145,9 +208,9 @@ The returned `JacobianConfig` instance contains all the work buffers required by `ForwardDiff.jacobian` and `ForwardDiff.jacobian!` when the target function takes the form `f(x)`. -If `f` is `nothing` instead of the actual target function, then the returned instance can -be used with any target function. However, this will reduce ForwardDiff's ability to catch -and prevent perturbation confusion (see https://github.com/JuliaDiff/ForwardDiff.jl/issues/83). +If `f` or `tag` is `nothing`, then the returned instance can be used with any target function. +However, this will reduce ForwardDiff's ability to catch and prevent perturbation confusion +(see https://github.com/JuliaDiff/ForwardDiff.jl/issues/83). This constructor does not store/modify `x`. """ @@ -170,9 +233,9 @@ The returned `JacobianConfig` instance contains all the work buffers required by `ForwardDiff.jacobian` and `ForwardDiff.jacobian!` when the target function takes the form `f!(y, x)`. -If `f!` is `nothing` instead of the actual target function, then the returned instance can -be used with any target function. However, this will reduce ForwardDiff's ability to catch -and prevent perturbation confusion (see https://github.com/JuliaDiff/ForwardDiff.jl/issues/83). +If `f!` or `tag` is `nothing`, then the returned instance can be used with any target function. +However, this will reduce ForwardDiff's ability to catch and prevent perturbation confusion +(see https://github.com/JuliaDiff/ForwardDiff.jl/issues/83). This constructor does not store/modify `y` or `x`. """ @@ -212,9 +275,9 @@ configured for the case where the `result` argument is an `AbstractArray`. If it is a `DiffResult`, the `HessianConfig` should instead be constructed via `ForwardDiff.HessianConfig(f, result, x, chunk)`. -If `f` is `nothing` instead of the actual target function, then the returned instance can -be used with any target function. However, this will reduce ForwardDiff's ability to catch -and prevent perturbation confusion (see https://github.com/JuliaDiff/ForwardDiff.jl/issues/83). +If `f` or `tag` is `nothing`, then the returned instance can be used with any target function. +However, this will reduce ForwardDiff's ability to catch and prevent perturbation confusion +(see https://github.com/JuliaDiff/ForwardDiff.jl/issues/83). This constructor does not store/modify `x`. """ @@ -236,9 +299,9 @@ type/shape of the input vector `x`. The returned `HessianConfig` instance contains all the work buffers required by `ForwardDiff.hessian!` for the case where the `result` argument is an `DiffResult`. -If `f` is `nothing` instead of the actual target function, then the returned instance can -be used with any target function. However, this will reduce ForwardDiff's ability to catch -and prevent perturbation confusion (see https://github.com/JuliaDiff/ForwardDiff.jl/issues/83). +If `f` or `tag` is `nothing`, then the returned instance can be used with any target function. +However, this will reduce ForwardDiff's ability to catch and prevent perturbation confusion +(see https://github.com/JuliaDiff/ForwardDiff.jl/issues/83). This constructor does not store/modify `x`. """ diff --git a/src/prelude.jl b/src/prelude.jl index 9e037afa..81441f3a 100644 --- a/src/prelude.jl +++ b/src/prelude.jl @@ -1,6 +1,11 @@ const NANSAFE_MODE_ENABLED = @load_preference("nansafe_mode", false) const DEFAULT_CHUNK_THRESHOLD = @load_preference("default_chunk_threshold", 12) +# On ≤1.10, the hash of a type cannot be computed at compile-time, +# making `HashTag(...)` type-unstable, so `Tag(...)` is left as +# as the default. +const HASHTAG_MODE_ENABLED = @load_preference("hashtag_mode", VERSION ≥ v"1.11") + const AMBIGUOUS_TYPES = (AbstractFloat, Irrational, Integer, Rational, Real, RoundingMode) const UNARY_PREDICATES = Symbol[:isinf, :isnan, :isfinite, :iseven, :isodd, :isreal, :isinteger] From 6da76e6caff76eef09604727b99b327c21424d88 Mon Sep 17 00:00:00 2001 From: Cody Tapscott Date: Fri, 8 Aug 2025 11:30:34 -0400 Subject: [PATCH 2/2] Remove "hashtag_mode" preference As requested by @oscardssmith. --- src/config.jl | 2 +- src/prelude.jl | 5 ----- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/src/config.jl b/src/config.jl index c8d2791a..579079a5 100644 --- a/src/config.jl +++ b/src/config.jl @@ -109,7 +109,7 @@ Base.eltype(cfg::AbstractConfig) = eltype(typeof(cfg)) elseif style === :type return Tag(f, X) elseif style === nothing - if HASHTAG_MODE_ENABLED + if VERSION ≥ v"1.11" return HashTag(f, X) else return Tag(f, X) diff --git a/src/prelude.jl b/src/prelude.jl index 81441f3a..9e037afa 100644 --- a/src/prelude.jl +++ b/src/prelude.jl @@ -1,11 +1,6 @@ const NANSAFE_MODE_ENABLED = @load_preference("nansafe_mode", false) const DEFAULT_CHUNK_THRESHOLD = @load_preference("default_chunk_threshold", 12) -# On ≤1.10, the hash of a type cannot be computed at compile-time, -# making `HashTag(...)` type-unstable, so `Tag(...)` is left as -# as the default. -const HASHTAG_MODE_ENABLED = @load_preference("hashtag_mode", VERSION ≥ v"1.11") - const AMBIGUOUS_TYPES = (AbstractFloat, Irrational, Integer, Rational, Real, RoundingMode) const UNARY_PREDICATES = Symbol[:isinf, :isnan, :isfinite, :iseven, :isodd, :isreal, :isinteger]