Skip to content

Commit 8d48c30

Browse files
ChrisRackauckas-ClaudehersleChrisRackauckas
authored
Use separate cache flags for A and b in ForwardDiff extension (rebased #1054 + test) (#1062)
* Use separate cache flags for A and b (faster when only one changes) * Test separate A/b partials validity flags in DualLinearCache Verify that mutating only A or only b invalidates that side's partials list independently, that solving revalidates both, and that the result stays correct across one-sided updates. Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com> --------- Co-authored-by: Herman Sletmoen <hermansletmoen@gmail.com> Co-authored-by: ChrisRackauckas-Claude <accounts@chrisrackauckas.com>
1 parent 804d330 commit 8d48c30

2 files changed

Lines changed: 57 additions & 24 deletions

File tree

ext/LinearSolveForwardDiffExt.jl

Lines changed: 22 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,9 @@ LinearSolve.@concrete mutable struct DualLinearCache{DT}
5656
primal_u_cache
5757
primal_b_cache
5858

59-
# Cache validity flag for RHS precalculation optimization
60-
rhs_cache_valid
59+
# Cache validity flags for when partials of A or b changes
60+
A_partials_valid
61+
b_partials_valid
6162

6263
dual_A
6364
dual_b
@@ -94,15 +95,13 @@ function linearsolve_forwarddiff_solve!(cache::DualLinearCache, alg, args...; kw
9495

9596
rhs_list = cache.rhs_list
9697

97-
# Update cached partials lists if cache is invalid
98-
if !cache.rhs_cache_valid
99-
if !isnothing(∂_A)
100-
update_partials_list!(∂_A, cache.partials_A_list)
101-
end
102-
if !isnothing(∂_b)
103-
update_partials_list!(∂_b, cache.partials_b_list)
104-
end
105-
cache.rhs_cache_valid = true
98+
if !cache.A_partials_valid && !isnothing(∂_A)
99+
update_partials_list!(∂_A, cache.partials_A_list)
100+
cache.A_partials_valid = true
101+
end
102+
if !cache.b_partials_valid && !isnothing(∂_b)
103+
update_partials_list!(∂_b, cache.partials_b_list)
104+
cache.b_partials_valid = true
106105
end
107106

108107
A_list = cache.partials_A_list
@@ -177,11 +176,13 @@ function xp_linsolve_rhs!(
177176
∂_b::Union{<:Partials, <:AbstractArray{<:Partials}}, cache::DualLinearCache
178177
)
179178

180-
# Update cached partials lists if cache is invalid
181-
if !cache.rhs_cache_valid
179+
if !cache.A_partials_valid
182180
update_partials_list!(∂_A, cache.partials_A_list)
181+
cache.A_partials_valid = true
182+
end
183+
if !cache.b_partials_valid
183184
update_partials_list!(∂_b, cache.partials_b_list)
184-
cache.rhs_cache_valid = true
185+
cache.b_partials_valid = true
185186
end
186187

187188
A_list = cache.partials_A_list
@@ -201,10 +202,9 @@ function xp_linsolve_rhs!(
201202
∂_b::Nothing, cache::DualLinearCache
202203
)
203204

204-
# Update cached partials list for A if cache is invalid
205-
if !cache.rhs_cache_valid
205+
if !cache.A_partials_valid
206206
update_partials_list!(∂_A, cache.partials_A_list)
207-
cache.rhs_cache_valid = true
207+
cache.A_partials_valid = true
208208
end
209209

210210
A_list = cache.partials_A_list
@@ -222,10 +222,9 @@ function xp_linsolve_rhs!(
222222
cache::DualLinearCache
223223
)
224224

225-
# Update cached partials list for b if cache is invalid
226-
if !cache.rhs_cache_valid
225+
if !cache.b_partials_valid
227226
update_partials_list!(∂_b, cache.partials_b_list)
228-
cache.rhs_cache_valid = true
227+
cache.b_partials_valid = true
229228
end
230229

231230
b_list = cache.partials_b_list
@@ -380,6 +379,7 @@ function __dual_init(
380379
similar(non_partial_cache.u), # primal_u_cache
381380
similar(new_b), # primal_b_cache
382381
true, # Cache is initially valid
382+
true,
383383
A,
384384
b,
385385
dual_u_init
@@ -495,8 +495,7 @@ function setA!(dc::DualLinearCache, A)
495495
partial_vals!(getfield(dc, :partials_A), A)
496496
end
497497

498-
# Invalidate cache (if setting A or b)
499-
return setfield!(dc, :rhs_cache_valid, false)
498+
return setfield!(dc, :A_partials_valid, false)
500499
end
501500
function setb!(dc::DualLinearCache, b)
502501
# Put the Dual-stripped versions in the LinearCache
@@ -510,8 +509,7 @@ function setb!(dc::DualLinearCache, b)
510509
partial_vals!(getfield(dc, :partials_b), b)
511510
end
512511

513-
# Invalidate cache (if setting A or b)
514-
return setfield!(dc, :rhs_cache_valid, false)
512+
return setfield!(dc, :b_partials_valid, false)
515513
end
516514
function setu!(dc::DualLinearCache{DT}, u) where {DT}
517515
# Put the Dual-stripped versions in the LinearCache

test/Core/forwarddiff_overloads.jl

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -571,3 +571,38 @@ end
571571
extract_partials(v) = [collect(ForwardDiff.partials(x)) for x in v]
572572
@test all(((a, b),) -> a b, zip(extract_partials(sol.u), extract_partials(ref)))
573573
end
574+
575+
# The DualLinearCache tracks partials-list validity for A and b independently,
576+
# so mutating only one side does not force the other's partials to be recomputed
577+
# (relevant e.g. in an ODE where A is fixed while b changes, and vice versa).
578+
@testset "DualLinearCache separate A/b partials validity" begin
579+
A, b = h([ForwardDiff.Dual(10.0, 1.0, 0.0), ForwardDiff.Dual(10.0, 0.0, 1.0)])
580+
cache = init(LinearProblem(A, b), LUFactorization())
581+
582+
# Both lists start valid (populated lazily on first solve).
583+
@test getfield(cache, :A_partials_valid)
584+
@test getfield(cache, :b_partials_valid)
585+
586+
# Mutating only b invalidates b's list and leaves A's untouched.
587+
_, new_b = h([ForwardDiff.Dual(5.0, 1.0, 0.0), ForwardDiff.Dual(5.0, 0.0, 1.0)])
588+
cache.b = new_b
589+
@test getfield(cache, :A_partials_valid)
590+
@test !getfield(cache, :b_partials_valid)
591+
592+
# Solving revalidates both, and the result still matches the reference.
593+
x_p = solve!(cache)
594+
@test getfield(cache, :A_partials_valid)
595+
@test getfield(cache, :b_partials_valid)
596+
@test (x_p, A \ new_b, rtol = 1.0e-9)
597+
598+
# Symmetrically, mutating only A invalidates A's list and leaves b's untouched.
599+
new_A, _ = h([ForwardDiff.Dual(2.0, 1.0, 0.0), ForwardDiff.Dual(2.0, 0.0, 1.0)])
600+
cache.A = new_A
601+
@test !getfield(cache, :A_partials_valid)
602+
@test getfield(cache, :b_partials_valid)
603+
604+
x_p = solve!(cache)
605+
@test getfield(cache, :A_partials_valid)
606+
@test getfield(cache, :b_partials_valid)
607+
@test (x_p, new_A \ new_b, rtol = 1.0e-9)
608+
end

0 commit comments

Comments
 (0)