Repository navigation
Instantiate per-partition Broadcasted in ArrayPartition copy/copyto!, use type-level homogeneity check - #670
Conversation
The tuple-recursion base case was missing @inline, forcing a runtime dispatch whenever an ArrayPartition broadcast mixes partition eltypes (e.g. Float32 and Float64 partitions). Homogeneous partitions were unaffected since the compiler could already fold the single-method recursion. Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com> Co-Authored-By: Claude <noreply@anthropic.com> Agent-Harness: Claude Code 2.1.294 Agent-Model: claude-sonnet-5 Agent-Session: https://claude.ai/code/session_01LPHREnnonfLg1VcE1EJovv Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LPHREnnonfLg1VcE1EJovv
… use type-level homogeneity check
unpack() drops a Broadcasted's axes to make recomputing them cheaper,
but the per-partition copyto!/copy never recomputed them before handing
the slice to the partition's own copyto!. On Julia 1.12 the resulting
Broadcasted{Style,Nothing,...} takes a slower, non-SIMD path in the
partition's own broadcast machinery. Wrap each per-partition unpack in
Broadcast.instantiate before copying/copying into it.
Also replace the runtime `all(x isa typeof(first(dest.x)) for x in dest.x)`
homogeneity check (which only looked at dest, allocated, and ran on every
call) with a type-level check over dest and every ArrayPartition argument
of the broadcast, so it constant-folds.
Builds on the @inline fix in the previous PR: without it, re-adding
Broadcast.instantiate on a mixed-partition-eltype broadcast makes the
existing unpack_args dispatch strictly worse (confirmed: AP3mixed
`b+c*d` 5203 -> 9390 ns, allocs 21 -> 42, with instantiate alone).
Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Agent-Harness: Claude Code 2.1.294
Agent-Model: claude-sonnet-5
Agent-Session: https://claude.ai/code/session_01LPHREnnonfLg1VcE1EJovv
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LPHREnnonfLg1VcE1EJovv
_homog_ap(::ArrayPartition{T,S}) called fieldtype(S, 1) without guarding
fieldcount(S) == 0, so an in-place broadcast on ArrayPartition() threw
BoundsError(Tuple{}, 1) instead of succeeding as it does on master (the
old all(...) check was vacuously true on an empty iterator). Guard the
empty case so it still constant-folds to true.
Also trims a changelog-style comment per review feedback.
Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Agent-Harness: Claude Code 2.1.294
Agent-Model: claude-sonnet-5
Agent-Session: https://claude.ai/code/session_01LPHREnnonfLg1VcE1EJovv
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LPHREnnonfLg1VcE1EJovv
Independent review (Devin CLI 3000.11.3, model fusion-claude-opus-5-5-high-sidekick-swe-2-medium): MERGE, risk medium. Full reviewVERDICT: MERGE Every Blocking findingsNone. Non-blocking findings
What I ranAll runs used
Differential probe (
Mixed-eltype in-place broadcasts still allocate 0 bytes, and OOP return types still infer concretely. Benchmarks (
What I did not verify
🤖 Posted by an AI agent — harness: Devin CLI 3000.11.3 (review), Claude Code 2.1.285 (posting) · model: fusion-claude-opus-5-5-high-sidekick-swe-2-medium |
Changes since round 1 review
A Cursor review (round 1) found a blocking bug:
_homog_ap(::ArrayPartition{T,S})called
fieldtype(S, 1)without guardingfieldcount(S) == 0, so an in-placebroadcast on the 0-partition
ArrayPartition()threwBoundsError(Tuple{}, 1)where master succeeds. Fixed by guarding the empty case
(
fieldcount(S) == 0 || S <: NTuple{...}), confirmed it still constant-foldsto
return truefor the empty case viacode_typed, added a Core test(
"Broadcast on 0-partition ArrayPartition") covering in-place broadcast,copy, OOP broadcast, andcopyto!(::ArrayPartition, ::ArrayPartition)onArrayPartition(), and confirmed it fails on the pre-fix code(
BoundsError) and passes after. Also corrected the AP2 row below, whichhad a copy-paste error (both columns showed the same number; the real
"before" is PR1-only ~92.5 ns, "after" is ~41.0 ns on this run — not
"unchanged" as originally written, per the review's non-blocking finding),
and trimmed a changelog-style comment on
_homog_apto a plain "why" note.Numbers, bitwise identity, and the earlier regression test are unchanged and
re-verified below. Re-ran Runic, typos, Core and QA on Julia 1.12.4 and
1.10.12: QA on 1.10.12 fails with 1 Aqua method-ambiguity
(
similar(::AbstractVectorOfArray, ...)vs.Base.similar), confirmedpre-existing on unmodified
origin/masterunder Julia 1.10 (same failure,same ambiguity, via
Test.detect_ambiguitieson a clean worktree) andunrelated to
vector_of_array.jl, which this PR does not touch; 1.12.4 QAis unaffected (
20 Pass) since this ambiguity apparently doesn't triggerunder 1.12's
similarsignature.Stacked on #666
This PR's branch (
perf/ap-broadcast-instantiate-homog) is branched offperf/ap-unpack-args-inline, the branch behind#666, so the diff below
includes both commits: #666's
@inlinefix and this PR's own commit. GitHubdoes not let a cross-fork PR's base be a branch that only exists on the fork,
so this is opened against
masterrather than against #666's branch directly-- please review only the second commit (
Instantiate per-partition Broadcasted...); the first is #666.This PR needs #666: applying only this PR's change (instantiate + type-level
homogeneity check) without the
@inlinefix makes the mixed-partition-eltypecase strictly worse than master, because
Broadcast.instantiateadds workon a path that still runtime-dispatches. Measured directly (Julia 1.12.4, 3000
samples), instantiate-only vs. master:
b+c*db+c*dWith PR1 applied first (this branch), the same cases improve (see below).
What and why
unpack(bc, i)drops aBroadcasted's axes "because it is easier torecompute" (
src/array_partition.jl:558comment), but the per-partitioncopyto!/copyinArrayPartition's own broadcast machinery(
src/array_partition.jl:503,516,521before this change) never recomputedthem — it handed each partition a
Broadcasted{Style, Nothing, ...}. OnJulia 1.12 that forces the partition's own broadcast implementation onto a
slower, non-SIMD path. This PR wraps each per-partition
unpackinBroadcast.instantiatebeforecopy/copyto!.It also replaces the runtime homogeneity check
all(x isa typeof(first(dest.x)) for x in dest.x)— which only inspectsdest(ignoring the broadcast's otherArrayPartitionarguments), andallocates an iterator on every call — with a type-level check
(
S <: NTuple{fieldcount(S), fieldtype(S,1)}) overdestand everyArrayPartitionargument of theBroadcastedtree, so it constant-foldsinstead of running at runtime.
Before/after (reused from the audit's
scratch/a1_bcfix2.jlandscratch/a1_ode.jl)Julia 1.12.4,
@benchmark3000 samples, in-place broadcast on this branch(PR1 + PR2) vs. PR1 alone:
b+c*d4argb+c*db+c*dJulia 1.10.12, same workload:
b+c*db+c*dTsit5
ArrayPartitionIIP ODE solve (scratch/a1_ode.jl,OrdinaryDiffEqTsit5),Julia 1.12.4:
step!(warmed)step!(warmed)solvesolveJulia 1.10.12:
step!step!Results are bitwise identical before/after. Checked with a seeded script
(
correctness2.jl) comparingcollect(a)for every broadcast case above and(sol.t, collect(sol.u[end]))for both Tsit5 ODE workloads, serialized fromthe unpatched tree and compared against this branch:
Failing-before / passing-after
Neither
@allocatednorJET.@report_optdiscriminates this fix: allocationsand runtime-dispatch-report counts for the homogeneous large-
ArrayPartitioncase are 0 both before and after (verified directly) — the regression this PR
fixes is a lost-SIMD timing effect, not an allocation or dispatch effect, so
those techniques used for PR1 don't apply here.
Instead, added a deterministic structural test,
test/Core/partitions_test.jl::"ArrayPartition broadcast instantiates partition Broadcasted", using a custom partition array type that records whether theBroadcastedit receives in its owncopyto!hasaxes !== nothing(i.e.was instantiated). This directly pins the mechanism (not a timing threshold,
so it isn't flaky in CI):
Before this fix (
git stashthesrc/array_partition.jlchange):After:
0-partition
ArrayPartition(the bug found in round-1 review): addedtest/Core/partitions_test.jl::"Broadcast on 0-partition ArrayPartition",covering in-place broadcast,
copy, OOP broadcast, andcopyto!(::ArrayPartition, ::ArrayPartition)onArrayPartition().Before this fix (
git stashthe_homog_apone-line change):After:
Also confirmed with
code_typedthat_homog_ap(ArrayPartition())stillconstant-folds to
return true(not a runtime branch), and spot-checked_homog_apon a 1-partition and a mixed-eltypeArrayPartition(trueandfalserespectively, as expected).Test results
GROUP=Core: all 12 Core testsets pass (Partitions Tests: 135 Pass, 0 Fail,includes this PR's two tests and PR1's).
GROUP=QA:QA: 20 Pass, 0 Fail— required adding:instantiatetotest/QA/qa.jl'sall_qualified_accesses_are_publicignore list next to theother legitimately-qualified
Base.Broadcastnames already there(
:Broadcasted,:flatten,:result_style, etc.), sinceBroadcast.instantiateis not itself declared public by Base.
Both re-run on Julia 1.12.4 and 1.10.12.
What was not verified
AllocCheck.jl-level static allocation checking was not used;@allocatedand
JET.@report_optwere, and neither showed a difference (expected, seeabove).
copy(vs.copyto!) on anArrayPartitionbroadcast with mixed partitiontypes was exercised by the existing test suite and the correctness script,
but no dedicated microbenchmark of mixed-type
copywas run (only theOOProws in the in-place-broadcast benchmark table above, which do exercisecopy).What a reviewer should push back on
The 0-partition case is now covered (see above); worth independently
re-checking that no other empty-
ArrayPartitionpath regressed beyond thefour covered by the new test (
copyto!(dest::AbstractArray, A::ArrayPartition)and
getindex/setindex!on an emptyArrayPartitiondon't go through_homog_apat all, so they were never at risk, but a second pair of eyes onthat claim is welcome).
Please ignore until reviewed by @ChrisRackauckas.
Risk assessment
src/array_partition.jl,Base.copy/Base.copyto!forArrayPartitionbroadcasts and the internal homogeneity check; no signatureor public API change. Affects every
ArrayPartitionbroadcast (homogeneousand mixed).
correctness check across 10 workloads including a Tsit5 ODE solve; Core and
QA test groups pass; a deterministic structural regression test.
ArrayPartitionbroadcast dispatch, so it should not auto-merge even if CIis green.
🤖 Generated with Claude Code
https://claude.ai/code/session_01LPHREnnonfLg1VcE1EJovv