Skip to content

Commit 1295777

Browse files
ChrisRackauckas-ClaudeChrisRackauckasclaude
authored
Avoid reshape allocation in extract_jacobian! for Matrix results (#797)
* Avoid reshape allocation in extract_jacobian! for Matrix results `extract_jacobian!` called `reshape(result, length(ydual), n)` which allocates a 48-byte ReshapedArray wrapper. Under `--check-bounds=yes` (used by Pkg.test), this allocation cannot be elided by the compiler, causing 48 bytes per jacobian! call. For implicit ODE/SDE solvers that call jacobian! multiple times per step, this adds up (e.g. 144 bytes/step for SKenCarp with 3 NL solver iterations). Add `_maybe_reshape` that returns the array as-is when it already has the target shape, avoiding the wrapper allocation. Falls back to `reshape` when dimensions don't match. Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com> Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Use dispatch instead of conditional _maybe_reshape for type stability Address review feedback: replace the type-unstable _maybe_reshape helper with two dispatch-based extract_jacobian! methods: - AbstractMatrix: skip reshape entirely (zero-alloc, hot path for DiffEq) - AbstractArray: reshape unconditionally (type-stable for non-matrix inputs) Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com> Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fold AbstractMatrix optimization into general method as one-line change Address review: instead of a separate dispatch method for AbstractMatrix, use a conditional in the existing method to skip reshape when result is already a matrix. This minimizes the diff while preserving the allocation fix under --check-bounds=yes. Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com> Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Update Project.toml --------- Co-authored-by: ChrisRackauckas-Claude <accounts@chrisrackauckas.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent ff0d903 commit 1295777

File tree

3 files changed

+18
-2
lines changed

3 files changed

+18
-2
lines changed

Project.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
name = "ForwardDiff"
22
uuid = "f6369f11-7733-5829-9624-2563aa707210"
3-
version = "1.3.2"
3+
version = "1.3.3"
44

55
[deps]
66
CommonSubexpressions = "bbf7d656-a473-5ed7-a52c-81e309532950"

src/jacobian.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ jacobian(f, x::Real) = throw(DimensionMismatch("jacobian(f, x) expects that x is
9393
#####################
9494

9595
function extract_jacobian!(::Type{T}, result::AbstractArray, ydual::AbstractArray, n) where {T}
96-
out_reshaped = reshape(result, length(ydual), n)
96+
out_reshaped = result isa AbstractMatrix ? result : reshape(result, length(ydual), n)
9797
ydual_reshaped = vec(ydual)
9898
# Use closure to avoid GPU broadcasting with Type
9999
partials_wrap(ydual, nrange) = partials(T, ydual, nrange)

test/AllocationsTest.jl

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,20 @@ convert_test_574() = convert(ForwardDiff.Dual{Nothing,ForwardDiff.Dual{Nothing,F
2828
@test iszero(allocs_convert_test_574())
2929
end
3030

31+
@testset "Test jacobian! allocations" begin
32+
# jacobian! should not allocate when called with a pre-allocated result Matrix.
33+
# Previously, reshape() inside extract_jacobian! allocated a wrapper
34+
# object that could not be elided under --check-bounds=yes.
35+
function allocs_jacobian!()
36+
f!(y, x) = (y .= x .^ 2)
37+
x = [1.0, 2.0, 3.0]
38+
y = similar(x)
39+
result = zeros(3, 3)
40+
cfg = ForwardDiff.JacobianConfig(f!, y, x)
41+
ForwardDiff.jacobian!(result, f!, y, x, cfg) # warmup
42+
return @allocated ForwardDiff.jacobian!(result, f!, y, x, cfg)
43+
end
44+
@test iszero(allocs_jacobian!())
45+
end
46+
3147
end

0 commit comments

Comments
 (0)