Skip to content

Throw when broadcasting over structural tangents - #724

Open
devmotion wants to merge 5 commits into
mainfrom
dmw/tangent-broadcastable
Open

devmotion wants to merge 5 commits into
mainfrom
dmw/tangent-broadcastable

Conversation

@devmotion

@devmotion devmotion commented Oct 29, 2026 •

Copy link
Copy Markdown
Member

Tangent defines no Broadcast.broadcastable, so broadcasting over one falls back to collect, which iterates over the backing and produces a Vector. That is wrong in different ways depending on the backing:

julia> Tangent{Any}(1.0, [2.0]) .* 2          # tuple: element types are lost
2-element Vector{Any}:
 2.0
  [4.0]

julia> Tangent{Foo}(; y=2.0, x=1.0) .* 2      # struct: broadcasts over the fields, in keyword order
2-element Vector{Float64}:
 4.0
 2.0

julia> Tangent{Dict}(Dict(:a => 1.0)) .* 2     # Dict: broadcasts over the `Pair`s
ERROR: MethodError: no method matching *(::Pair{Symbol, Float64}, ::Int64)

This PR makes broadcasting over a StructuralTangent (Tangent and MutableTangent) throw an ArgumentError, like Base does for NamedTuples and Dicts:

julia> Tangent{Any}(1.0, 2.0) .* 2
ERROR: ArgumentError: broadcasting over structural tangents is not supported; use `map` or arithmetic

Why not broadcast like the primal?

An earlier version of this PR returned the backing for tuple-backed tangents and Ref(tangent) otherwise. No such rule is correct in general, because the shape a tangent should broadcast with depends on the broadcast, not on the tangent:

  • For Tangent{Any} the primal is unknown, and the constructor accepts any backing.
  • A tuple tangent is a container in a tuple broadcast, but a single element in a 0-dim broadcast with a tuple-valued output.
  • A structural tangent of an array type (e.g. Tangent{SVector}(; data=...)) belongs to a container. Treating it as a scalar made ChainRules' broadcast pullback for x -> x^3 silently return [NoTangent(), NoTangent()].

Only the code doing the broadcast knows which shape is meant. For example, ChainRules' broadcast rules know the output ys, so they should unwrap the cotangent there (as _no_tuple_tangent already does in mapreduce.jl).

Compatibility

Broadcasting over a structural tangent used to return a (mostly wrong) Vector; it now throws. In particular, ChainRules' generic broadcast pullbacks currently rely on this fallback when they get a Tangent{Any,<:Tuple} cotangent from Zygote for tuple broadcasts. They need to unwrap it before this is released. The version is therefore bumped to 1.27.0.

The unrelated fix for tuple projectors that was part of this PR is now in #725.

🤖 Generated with Claude Code

@codecov

codecov Bot commented Oct 29, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 93.56%. Comparing base (4e9c8aa) to head (755baf2).

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #724      +/-   ##
==========================================
+ Coverage   93.54%   93.56%   +0.01%     
==========================================
  Files          15       15              
  Lines        1022     1025       +3     
==========================================
+ Hits          956      959       +3     
  Misses         66       66              

☔ 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 devmotion changed the title Broadcast Tangent of tuples like their backing tuple Broadcast Tangents like their primal Oct 29, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
devmotion and others added 3 commits October 30, 2026 01:12
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Whether a structural tangent should broadcast as a container or as a
scalar depends on the broadcast and can't be determined from the tangent
alone (e.g. for `Tangent{Any}`), so throw an `ArgumentError` instead of
guessing. Move the unrelated tuple projector fix to a separate PR.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@devmotion devmotion changed the title Broadcast Tangents like their primal Throw when broadcasting over structural tangents Oct 30, 2026

This branch has not been deployed

No deployments
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.

1 participant