Skip to content

Commit eb6547c

Browse files
authored
Add array output support (#45)
* Add array output support * Fix format * cosmetic * Add test
1 parent a4c8fab commit eb6547c

7 files changed

Lines changed: 221 additions & 8 deletions

File tree

src/evaluator.jl

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,20 @@ function MOI.eval_constraint(evaluator::Evaluator, g, x)
6060
return
6161
end
6262

63+
function eval_residual!(evaluator::Evaluator, F, x)
64+
return eval_residual!(evaluator.backend, F, x)
65+
end
66+
67+
function eval_residual_jtprod!(evaluator::Evaluator, Jtv, x, v)
68+
return eval_residual_jtprod!(evaluator.backend, Jtv, x, v)
69+
end
70+
71+
function eval_residual_jprod!(evaluator::Evaluator, Jv, x, v)
72+
return eval_residual_jprod!(evaluator.backend, Jv, x, v)
73+
end
74+
75+
residual_dimension(evaluator::Evaluator) = residual_dimension(evaluator.backend)
76+
6377
function MOI.jacobian_structure(evaluator::Evaluator)
6478
return MOI.jacobian_structure(evaluator.backend)
6579
end

src/mathoptinterface_api.jl

Lines changed: 136 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ function MOI.initialize(d::NLPEvaluator, requested_features::Vector{Symbol})
3636
largest_user_input_dimension = max(largest_user_input_dimension, op.N)
3737
end
3838
d.objective = nothing
39+
d.residual = nothing
3940
d.user_output_buffer = zeros(largest_user_input_dimension)
4041
d.jac_storage = zeros(max(N, largest_user_input_dimension))
4142
d.constraints = _FunctionStorage[]
@@ -47,6 +48,9 @@ function MOI.initialize(d::NLPEvaluator, requested_features::Vector{Symbol})
4748
max_expr_with_sub_length = 0
4849
#
4950
main_expressions = [c.expression.nodes for (_, c) in d.data.constraints]
51+
if d.data.residual !== nothing
52+
pushfirst!(main_expressions, something(d.data.residual).nodes)
53+
end
5054
if d.data.objective !== nothing
5155
pushfirst!(main_expressions, something(d.data.objective).nodes)
5256
end
@@ -102,7 +106,9 @@ function MOI.initialize(d::NLPEvaluator, requested_features::Vector{Symbol})
102106
end
103107
max_chunk = 1
104108
shared_partials_storage_ϵ = Float64[]
109+
main_offset = 0
105110
if d.data.objective !== nothing
111+
main_offset += 1
106112
expr = something(d.data.objective)
107113
subexpr, linearity = _subexpression_and_linearity(
108114
expr,
@@ -116,7 +122,7 @@ function MOI.initialize(d::NLPEvaluator, requested_features::Vector{Symbol})
116122
coloring_storage,
117123
d.want_hess,
118124
d.subexpressions,
119-
individual_order[1],
125+
individual_order[main_offset],
120126
subexpression_edgelist,
121127
subexpression_variables,
122128
linearity,
@@ -125,8 +131,32 @@ function MOI.initialize(d::NLPEvaluator, requested_features::Vector{Symbol})
125131
max_chunk = max(max_chunk, size(objective.seed_matrix, 2))
126132
d.objective = objective
127133
end
134+
if d.data.residual !== nothing
135+
main_offset += 1
136+
expr = something(d.data.residual)
137+
subexpr, linearity = _subexpression_and_linearity(
138+
expr,
139+
moi_index_to_consecutive_index,
140+
shared_partials_storage_ϵ,
141+
d,
142+
)
143+
residual = _FunctionStorage(
144+
subexpr,
145+
N,
146+
coloring_storage,
147+
d.want_hess,
148+
d.subexpressions,
149+
individual_order[main_offset],
150+
subexpression_edgelist,
151+
subexpression_variables,
152+
linearity,
153+
)
154+
max_expr_length = max(max_expr_length, length(expr.nodes))
155+
max_chunk = max(max_chunk, size(residual.seed_matrix, 2))
156+
d.residual = residual
157+
end
128158
for (k, (_, constraint)) in enumerate(d.data.constraints)
129-
idx = d.data.objective !== nothing ? k + 1 : k
159+
idx = main_offset + k
130160
expr = constraint.expression
131161
subexpr, linearity = _subexpression_and_linearity(
132162
expr,
@@ -211,6 +241,110 @@ function MOI.eval_constraint(d::NLPEvaluator, g, x)
211241
return
212242
end
213243

244+
# Forward-evaluate subexpressions and the residual at `x`.
245+
function _forward_pass_residual!(d::NLPEvaluator, x)
246+
for k in d.subexpression_order
247+
d.subexpression_forward_values[k] =
248+
_forward_eval(d.subexpressions[k], d, x)
249+
end
250+
_forward_eval(something(d.residual).expr, d, x)
251+
return
252+
end
253+
254+
# Read the residual values from forward storage into `F`.
255+
function _read_residual!(F::AbstractVector, d::NLPEvaluator)
256+
res = something(d.residual)
257+
range = _storage_range(res.expr.sizes, 1)
258+
@assert length(F) == length(range)
259+
for (i, j) in enumerate(range)
260+
F[i] = res.expr.forward_storage[j]
261+
end
262+
return
263+
end
264+
265+
"""
266+
eval_residual!(d::NLPEvaluator, F, x)
267+
268+
Forward-evaluate the residual at `x` and write the values into `F`.
269+
"""
270+
function eval_residual!(d::NLPEvaluator, F::AbstractVector, x::AbstractVector)
271+
@assert !isnothing(d.residual)
272+
_forward_pass_residual!(d, x)
273+
_read_residual!(F, d)
274+
return F
275+
end
276+
277+
"""
278+
eval_residual_jtprod!(d::NLPEvaluator, Jtv, x, v)
279+
280+
Compute `J(x)' * v` for the residual `F`, writing the result into `Jtv`.
281+
This is one reverse-mode pass with `v` as the seed at the residual root.
282+
"""
283+
function eval_residual_jtprod!(
284+
d::NLPEvaluator,
285+
Jtv::AbstractVector,
286+
x::AbstractVector,
287+
v::AbstractVector,
288+
)
289+
@assert !isnothing(d.residual)
290+
res = something(d.residual)
291+
_forward_pass_residual!(d, x)
292+
for k in d.subexpression_order
293+
_reverse_eval(d.subexpressions[k])
294+
end
295+
_reverse_eval(res.expr, v)
296+
fill!(Jtv, 0.0)
297+
_extract_reverse_pass(Jtv, d, res)
298+
return Jtv
299+
end
300+
301+
"""
302+
residual_dimension(d::NLPEvaluator) -> Int
303+
304+
Number of residual components (i.e. the length of the residual vector).
305+
"""
306+
function residual_dimension(d::NLPEvaluator)
307+
return length(_storage_range(something(d.residual).expr.sizes, 1))
308+
end
309+
310+
"""
311+
eval_residual_jprod!(d::NLPEvaluator, Jv, x, v)
312+
313+
Compute `J(x) * v` for the residual `F`. ArrayDiff doesn't yet have a
314+
forward-mode JVP, so this materializes the Jacobian via `nresid` reverse-mode
315+
passes (each with a unit-basis seed) and then takes `J * v`.
316+
"""
317+
function eval_residual_jprod!(
318+
d::NLPEvaluator,
319+
Jv::AbstractVector,
320+
x::AbstractVector,
321+
v::AbstractVector,
322+
)
323+
@assert !isnothing(d.residual)
324+
res = something(d.residual)
325+
nresid = residual_dimension(d)
326+
_forward_pass_residual!(d, x)
327+
for k in d.subexpression_order
328+
_reverse_eval(d.subexpressions[k])
329+
end
330+
seed = zeros(Float64, nresid)
331+
row = zeros(Float64, length(v))
332+
fill!(Jv, 0.0)
333+
for i in 1:nresid
334+
fill!(seed, 0.0)
335+
seed[i] = 1.0
336+
_reverse_eval(res.expr, seed)
337+
fill!(row, 0.0)
338+
_extract_reverse_pass(row, d, res)
339+
s = 0.0
340+
for j in eachindex(v)
341+
s += row[j] * v[j]
342+
end
343+
Jv[i] = s
344+
end
345+
return Jv
346+
end
347+
214348
function MOI.jacobian_structure(d::NLPEvaluator)
215349
J = Tuple{Int64,Int64}[]
216350
for (row, constraint) in enumerate(d.constraints)

src/model.jl

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@ function set_objective(model::Model, obj)
66
return
77
end
88

9+
function set_residual!(model::Model, residual)
10+
model.residual = parse_expression(model, residual)
11+
return
12+
end
13+
914
function add_constraint(
1015
model::Model{T},
1116
func,

src/reverse_mode.jl

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -568,7 +568,9 @@ function _forward_eval(
568568
f.partials_storage[rhs] = zero(T)
569569
end
570570
end
571-
@assert f.sizes.ndims[1] == 0 "Final result must be scalar, got ndims = $(f.sizes.ndims[1])"
571+
# Caller is responsible for reading the right range of `f.forward_storage`
572+
# for vector-valued roots (use `_storage_range(f.sizes, 1)`); the scalar
573+
# return is only meaningful when the root is scalar.
572574
return f.forward_storage[1]
573575
end
574576

@@ -580,15 +582,26 @@ Reverse-mode evaluation of an expression tree given in `f`.
580582
* This function assumes `f.partials_storage` is already updated.
581583
* This function assumes that `f.reverse_storage` has been initialized with 0.0.
582584
"""
583-
function _reverse_eval(f::_SubexpressionStorage)
585+
function _reverse_eval(
586+
f::_SubexpressionStorage,
587+
seed::Union{Nothing,AbstractVector{Float64}} = nothing,
588+
)
584589
@assert length(f.reverse_storage) >= _length(f.sizes)
585590
@assert length(f.partials_storage) >= _length(f.sizes)
586591
# f.nodes is already in order such that parents always appear before
587592
# children so a forward pass through nodes is a backwards pass through the
588593
# tree.
589594
children_arr = SparseArrays.rowvals(f.adj)
590-
for i in _storage_range(f.sizes, 1)
591-
f.reverse_storage[i] = one(Float64)
595+
root_range = _storage_range(f.sizes, 1)
596+
if seed === nothing
597+
for i in root_range
598+
f.reverse_storage[i] = one(Float64)
599+
end
600+
else
601+
@assert length(seed) == length(root_range)
602+
for (j, i) in enumerate(root_range)
603+
f.reverse_storage[i] = seed[j]
604+
end
592605
end
593606
for k in 1:length(f.nodes)
594607
node = f.nodes[k]

src/types.jl

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,9 @@ It has the following fields:
194194
"""
195195
mutable struct Model{T}
196196
objective::Union{Nothing,Expression{T}}
197+
# Vector residual for nonlinear least-squares objectives. When set, callers
198+
# can query its value, `J*v`, `J'*v`, etc. via the evaluator.
199+
residual::Union{Nothing,Expression{T}}
197200
expressions::Vector{Expression{T}}
198201
constraints::OrderedCollections.OrderedDict{ConstraintIndex,Constraint{T}}
199202
parameters::Vector{T}
@@ -202,6 +205,7 @@ mutable struct Model{T}
202205
last_constraint_index::Int64
203206
function Model{T}() where {T}
204207
return new{T}(
208+
nothing,
205209
nothing,
206210
Expression{T}[],
207211
OrderedCollections.OrderedDict{ConstraintIndex,Constraint{T}}(),
@@ -294,6 +298,7 @@ mutable struct NLPEvaluator <: MOI.AbstractNLPEvaluator
294298
ordered_variables::Vector{MOI.VariableIndex}
295299

296300
objective::Union{Nothing,_FunctionStorage}
301+
residual::Union{Nothing,_FunctionStorage}
297302
constraints::Vector{_FunctionStorage}
298303
subexpressions::Vector{_SubexpressionStorage}
299304
subexpression_order::Vector{Int}

test/ArrayDiff.jl

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -787,6 +787,36 @@ function test_model_typed_float32_evaluator_runs()
787787
return
788788
end
789789

790+
function test_residual_with_subexpression()
791+
# Residual references a subexpression `e = x1 * x2`, so the evaluator's
792+
# subexpression-iteration loops in `_forward_pass_residual!` and
793+
# `eval_residual_jprod!` are exercised.
794+
model = ArrayDiff.Model()
795+
x1 = MOI.VariableIndex(1)
796+
x2 = MOI.VariableIndex(2)
797+
e = ArrayDiff.add_expression(model, :($x1 * $x2))
798+
# F = [x1 + e, x2 - e]
799+
ArrayDiff.set_residual!(model, :([$x1 + $e, $x2 - $e]))
800+
evaluator = ArrayDiff.Evaluator(model, ArrayDiff.Mode(), [x1, x2])
801+
MOI.initialize(evaluator, [:Grad, :Jac, :JacVec])
802+
@test ArrayDiff.residual_dimension(evaluator) == 2
803+
x = [3.0, 4.0]
804+
# e = 12, F = [3 + 12, 4 - 12] = [15, -8]
805+
F = zeros(2)
806+
ArrayDiff.eval_residual!(evaluator, F, x)
807+
@test F == [15.0, -8.0]
808+
# J = [1+x2 x1 ; -x2 1-x1] = [5 3 ; -4 -2]
809+
Jtv = zeros(2)
810+
ArrayDiff.eval_residual_jtprod!(evaluator, Jtv, x, [1.0, 1.0])
811+
@test Jtv == [1.0, 1.0]
812+
Jv = zeros(2)
813+
ArrayDiff.eval_residual_jprod!(evaluator, Jv, x, [1.0, 0.0])
814+
@test Jv == [5.0, -4.0]
815+
ArrayDiff.eval_residual_jprod!(evaluator, Jv, x, [0.0, 1.0])
816+
@test Jv == [3.0, -2.0]
817+
return
818+
end
819+
790820
end # module
791821

792822
TestArrayDiff.runtests()

test/NLPModelsJuMP.jl

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,12 @@ function runtests()
1919
return
2020
end
2121

22-
function test_neural_nlpmodels_jump()
22+
function _test_neural_nlpmodels_jump(solver)
2323
n = 2
2424
X = [1.0 0.5; 0.3 0.8]
2525
target = [0.5 0.2; 0.1 0.7]
2626
model = Model(NLPModelsJuMP.Optimizer)
27-
set_attribute(model, "solver", JSOSolvers.LBFGSSolver)
27+
set_attribute(model, "solver", solver)
2828
set_attribute(
2929
model,
3030
MOI.AutomaticDifferentiationBackend(),
@@ -48,6 +48,18 @@ function test_neural_nlpmodels_jump()
4848
return
4949
end
5050

51+
function test_neural_lbfgs()
52+
return _test_neural_nlpmodels_jump(JSOSolvers.LBFGSSolver)
53+
end
54+
55+
function test_neural_trunkls()
56+
return _test_neural_nlpmodels_jump(JSOSolvers.TrunkSolverNLS)
57+
end
58+
59+
function test_neural_tronls()
60+
return _test_neural_nlpmodels_jump(JSOSolvers.TronSolverNLS)
61+
end
62+
5163
end
5264

5365
TestWithNLPModelsJuMP.runtests()

0 commit comments

Comments
 (0)