Summary
When a composite submodel is invoked with a multi-output ((a, b) ~ sub(...)) or
named-output ((a = x, b = y) ~ sub(...)) left-hand side, the child Context is
constructed without the NodeCreationOptions carried by the where { ... } clause.
As a result, inline where { constraints = ... } (e.g. a MeanField or custom factorization
constraint) is silently ignored for that submodel.
Impact
Silent wrong behaviour: inference runs with a different (unconstrained / different
factorization) recognition distribution than the user requested — no warning is emitted.
Root cause
src/model_macro.jl get_make_node_function:
- single-output LHS (line 838):
Context(__parent_context__, $ms_name, __options__) ✅
- Tuple LHS (line 860):
Context(__parent_context__, $ms_name) ❌
- NamedTuple LHS (line 882):
Context(__parent_context__, $ms_name) ❌
The constraint engine reads inline constraints from that field:
src/plugins/variational_constraints/variational_constraints_engine.jl:1003
inline_constraints = get(context_options(child), :constraints, nothing)
with context_options(context) = something(context.options, EmptyNodeCreationOptions)
(src/graph_engine.jl:668). When context.options === nothing it returns
EmptyNodeCreationOptions, so :constraints is never found.
Repro
using GraphPPL, Distributions
import GraphPPL: @model
@model function two_out(a, b, x)
a ~ Normal(x, 1.0)
b ~ Normal(a, 1.0)
end
@model function outer_multi(x)
(a, b) ~ two_out(x = x) where { constraints = GraphPPL.MeanField() }
end
model = GraphPPL.create_model(outer_multi()) do m, ctx
x = GraphPPL.datalabel(m, ctx, GraphPPL.NodeCreationOptions(kind=:data), :x, 1.0)
return (x = x,)
end
ctx = GraphPPL.getcontext(model)
for (fid, child) in GraphPPL.pairs(GraphPPL.children(ctx))
@show child.options # => nothing (single-output equivalent: NodeCreationOptions)
end
The equivalent single-output call honors it (see the existing "inline constraints on
submodel calls" test, test/plugins/variational_constraints/variational_constraints_tests.jl:1135).
Expected
Inline where { constraints = ... } behaves identically for multi-/named-output submodel
calls as for single-output calls.
Suggested fix
Pass __options__ in both multi-output paths (mirroring line 838) plus a regression test.
Summary
When a composite submodel is invoked with a multi-output (
(a, b) ~ sub(...)) ornamed-output (
(a = x, b = y) ~ sub(...)) left-hand side, the childContextisconstructed without the
NodeCreationOptionscarried by thewhere { ... }clause.As a result, inline
where { constraints = ... }(e.g. aMeanFieldor custom factorizationconstraint) is silently ignored for that submodel.
Impact
Silent wrong behaviour: inference runs with a different (unconstrained / different
factorization) recognition distribution than the user requested — no warning is emitted.
Root cause
src/model_macro.jlget_make_node_function:Context(__parent_context__, $ms_name, __options__)✅Context(__parent_context__, $ms_name)❌Context(__parent_context__, $ms_name)❌The constraint engine reads inline constraints from that field:
src/plugins/variational_constraints/variational_constraints_engine.jl:1003inline_constraints = get(context_options(child), :constraints, nothing)with
context_options(context) = something(context.options, EmptyNodeCreationOptions)(
src/graph_engine.jl:668). Whencontext.options === nothingit returnsEmptyNodeCreationOptions, so:constraintsis never found.Repro
The equivalent single-output call honors it (see the existing "inline constraints on
submodel calls" test,
test/plugins/variational_constraints/variational_constraints_tests.jl:1135).Expected
Inline
where { constraints = ... }behaves identically for multi-/named-output submodelcalls as for single-output calls.
Suggested fix
Pass
__options__in both multi-output paths (mirroring line 838) plus a regression test.