Skip to content

Commit 7479a2a

Browse files
ChrisRackauckasclaudecodex
committed
Seed dense dual arrays without scalar indexing
ForwardDiff scalar-indexes GPU arrays while seeding Jacobian chunks, and the chunk-tail zeroing added in 1.4.4 introduced the same failure in seed_zero_partials!. Use broadcast and map! over contiguous views for dense arrays with isbits values while preserving the structural fallback for wrappers, offset axes, and non-isbits values. Add JLArray regressions for both seeding paths, including the 1.4.4 tail clear, vector and chunk modes, mutating Jacobians, matrices, and views. Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Co-Authored-By: Codex <noreply@openai.com> Original-Agent-Harness: Claude Code (version unknown) Original-Agent-Model: Claude Fable 5 Original-Agent-Session: https://claude.ai/code/session_01Vx7zQ96NYk4VV4ML2s3kAC Agent-Harness: Codex CLI 0.150.1 Agent-Model: unknown Agent-Session: 01a04603-0ac8-7570-9713-851acf5b8f5d
1 parent b742809 commit 7479a2a

4 files changed

Lines changed: 106 additions & 4 deletions

File tree

Project.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ DiffRules = "1.4"
2828
DiffTests = "0.1"
2929
IrrationalConstants = "0.1, 0.2"
3030
JET = "0.9, 0.12"
31+
JLArrays = "0.1, 0.2"
3132
LogExpFunctions = "0.3, 1"
3233
NaNMath = "1"
3334
Preferences = "1"
@@ -41,9 +42,10 @@ DiffTests = "de460e47-3fe3-5279-bb4a-814414816d5d"
4142
InteractiveUtils = "b77e0a4c-d291-57a0-90e8-8db25a27a240"
4243
IrrationalConstants = "92d709cd-6900-40b7-9082-c6be49f344b6"
4344
JET = "c3a54625-cd67-489e-a8e7-0a5a0ff4e31b"
45+
JLArrays = "27aeb0d3-9eb9-45fb-866b-73c2ecf80fcb"
4446
SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf"
4547
StaticArrays = "90137ffa-7385-5640-81b9-e52037218182"
4648
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
4749

4850
[targets]
49-
test = ["Calculus", "DiffTests", "IrrationalConstants", "JET", "SparseArrays", "StaticArrays", "Test", "InteractiveUtils"]
51+
test = ["Calculus", "DiffTests", "IrrationalConstants", "JET", "JLArrays", "SparseArrays", "StaticArrays", "Test", "InteractiveUtils"]

src/apiutils.jl

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,18 +70,36 @@ function structural_eachindex(x::Diagonal, y::AbstractArray)
7070
return diagind(x)
7171
end
7272

73+
@inline function dense_seedable(duals, x, ::Type{V}) where {V}
74+
return duals isa DenseArray && isbitstype(V) && !Base.has_offset_axes(duals, x)
75+
end
76+
7377
# Copies the values of `x` into `duals` with zero partials. Used both to remove seeds `duals` is
7478
# currently carrying and to initialize a freshly allocated work buffer, whose elements must all be
7579
# written before the target function reads them.
76-
seed_zero_partials!(duals::AbstractArray{Dual{T,V,N}}, x) where {T,V,N} =
77-
_seed_zero_partials!(duals, x, structural_eachindex(duals, x))
80+
function seed_zero_partials!(duals::AbstractArray{Dual{T,V,N}}, x) where {T,V,N}
81+
seed = zero(Partials{N,V})
82+
if dense_seedable(duals, x, V) && axes(duals) == axes(x)
83+
duals .= Dual{T,V,N}.(x, Ref(seed))
84+
return duals
85+
end
86+
return _seed_zero_partials!(duals, x, structural_eachindex(duals, x))
87+
end
7888

7989
# Zeroes the partials of `count` elements starting at structural position `index`. Chunk mode only
8090
# needs to clear the chunk it just seeded, so writing through to the end of the array would be O(n)
8191
# redundant work per chunk, i.e. O(n^2/N) per sweep. `count` mirrors the `chunksize` argument of
8292
# `seed!(duals, x, index, seeds, chunksize)`.
8393
function seed_zero_partials!(duals::AbstractArray{Dual{T,V,N}}, x, index,
8494
count = N) where {T,V,N}
95+
if dense_seedable(duals, x, V)
96+
length(duals) == length(x) || throw(DimensionMismatch())
97+
last_index = min(index + count - 1, length(duals))
98+
dual_inds = index:last_index
99+
seed = zero(Partials{N,V})
100+
map!(xi -> Dual{T,V,N}(xi, seed), view(duals, dual_inds), view(x, dual_inds))
101+
return duals
102+
end
85103
idxs = Iterators.take(Iterators.drop(structural_eachindex(duals, x), index - 1), count)
86104
return _seed_zero_partials!(duals, x, idxs)
87105
end
@@ -106,7 +124,12 @@ end
106124

107125
function seed!(duals::AbstractArray{Dual{T,V,N}}, x,
108126
seeds::NTuple{N,Partials{N,V}}) where {T,V,N}
109-
if isbitstype(V)
127+
if dense_seedable(duals, x, V)
128+
length(duals) == length(x) || throw(DimensionMismatch())
129+
dual_inds = 1:min(N, length(duals))
130+
map!((xi, i) -> Dual{T,V,N}(xi, seeds[i]),
131+
view(duals, dual_inds), view(x, dual_inds), dual_inds)
132+
elseif isbitstype(V)
110133
for (i, idx) in zip(1:N, structural_eachindex(duals, x))
111134
duals[idx] = Dual{T,V,N}(x[idx], seeds[i])
112135
end
@@ -124,6 +147,14 @@ end
124147

125148
function seed!(duals::AbstractArray{Dual{T,V,N}}, x, index,
126149
seeds::NTuple{N,Partials{N,V}}, chunksize = N) where {T,V,N}
150+
if dense_seedable(duals, x, V)
151+
length(duals) == length(x) || throw(DimensionMismatch())
152+
shift = index - 1
153+
dual_inds = (1 + shift):min(shift + chunksize, length(duals))
154+
map!((xi, i) -> Dual{T,V,N}(xi, seeds[i - shift]),
155+
view(duals, dual_inds), view(x, dual_inds), dual_inds)
156+
return duals
157+
end
127158
offset = index - 1
128159
idxs = Iterators.drop(structural_eachindex(duals, x), offset)
129160
if isbitstype(V)

test/GPUArraysTest.jl

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
module GPUArraysTest
2+
3+
using ForwardDiff, Test
4+
using JLArrays
5+
6+
# Exercise GPU array semantics, including the scalar-indexing ban, without physical GPU hardware.
7+
JLArrays.allowscalar(false)
8+
9+
@testset "ForwardDiff seeding on GPU arrays" begin
10+
f(x) = x .^ 2 .+ 2 .* x
11+
12+
@testset "zero chunk tail" begin
13+
values = collect(Float64, 1:20)
14+
x = JLArray(values)
15+
duals = JLArray([ForwardDiff.Dual{Nothing}(xi, 1.0) for xi in values])
16+
ForwardDiff.seed_zero_partials!(duals, x, 5, 12)
17+
result = Array(duals)
18+
@test ForwardDiff.value.(result) == values
19+
@test [ForwardDiff.partials(d)[1] for d in result] ==
20+
[ones(4); zeros(12); ones(4)]
21+
end
22+
23+
@testset "jacobian, vector mode (length $n)" for n in (1, 4, 8)
24+
x = collect(Float64, 1:n)
25+
@test Array(ForwardDiff.jacobian(f, JLArray(x))) == ForwardDiff.jacobian(f, x)
26+
end
27+
28+
# lengths above the chunk size exercise the chunked `seed!` methods
29+
@testset "jacobian, chunk mode (length $n, chunk $c)" for n in (16, 20, 27), c in (4, 8)
30+
x = collect(Float64, 1:n)
31+
cfg = ForwardDiff.JacobianConfig(f, JLArray(x), ForwardDiff.Chunk{c}())
32+
@test Array(ForwardDiff.jacobian(f, JLArray(x), cfg)) == ForwardDiff.jacobian(f, x)
33+
end
34+
35+
@testset "jacobian! into a GPU array (length $n)" for n in (4, 16)
36+
x = collect(Float64, 1:n)
37+
out = JLArray(zeros(n, n))
38+
ForwardDiff.jacobian!(out, f, JLArray(x))
39+
@test Array(out) == ForwardDiff.jacobian(f, x)
40+
end
41+
42+
@testset "jacobian of f! with GPU input and output" begin
43+
f!(y, x) = (y .= x .^ 2 .+ 2 .* x; nothing)
44+
x = collect(Float64, 1:8)
45+
y = zeros(8)
46+
J = ForwardDiff.jacobian(f!, JLArray(y), JLArray(x))
47+
@test Array(J) == ForwardDiff.jacobian(f!, y, x)
48+
end
49+
50+
@testset "jacobian with matrix input (chunk $c)" for c in (3, 6)
51+
X = reshape(collect(Float64, 1:12), 4, 3)
52+
g(x) = x .* sum(x)
53+
cfg = ForwardDiff.JacobianConfig(g, JLArray(X), ForwardDiff.Chunk{c}())
54+
@test Array(ForwardDiff.jacobian(g, JLArray(X), cfg)) ForwardDiff.jacobian(g, X)
55+
end
56+
57+
@testset "jacobian with view input" begin
58+
X = JLArray(reshape(collect(Float64, 1:18), 6, 3))
59+
xv = view(X, :, 2)
60+
@test Array(ForwardDiff.jacobian(f, xv)) == ForwardDiff.jacobian(f, Array(xv))
61+
end
62+
end
63+
64+
end # module

test/runtests.jl

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,11 @@ Random.seed!(SEED)
4343
t = @elapsed include("ConfusionTest.jl")
4444
println("##### done (took $t seconds).")
4545
end
46+
@testset "GPUArrays" begin
47+
println("##### Testing seeding on GPU arrays...")
48+
t = @elapsed include("GPUArraysTest.jl")
49+
println("##### done (took $t seconds).")
50+
end
4651
@testset "Miscellaneous" begin
4752
println("##### Testing miscellaneous functionality...")
4853
t = @elapsed include("MiscTest.jl")

0 commit comments

Comments
 (0)