Skip to content

Commit 0a1499f

Browse files
authored
Fix type-inference failure for nested submodels (#1427)
Evaluating a submodel used to recurse through the shared `_evaluate!!(::Model, ::AbstractVarInfo)` method. Each submodel level constructs a contextualized `Model{..., Ctx}` whose `Ctx` parameter gains another context layer. In nested submodels, Julia's recursion limiter (`limit_type_size`) sees the same `_evaluate!!(::Model, ...)` method recurring with a more complex `Model{..., Ctx}` call signature, widens the `Model` argument to abstract `Model`, and inference can no longer resolve `model.f` precisely. The model return type then collapses to `Any`, evaluation falls back to runtime dispatch, and primal/gradient evaluation slows down. Submodel evaluation now calls the generated model function directly instead of going through `_evaluate!!(::Model, ...)`. This removes the recursive `_evaluate!!(::Model, ...)` call edge while leaving the recursive `_evaluate!!(::Submodel, ...)` edge intact; the remaining submodel edge is not widened by the limiter. As a result, inference stays type stable for nested submodels. Adds type-stability (`@inferred`) and allocation (`iszero(allocs)`) regression tests at both the evaluation and `LogDensityFunction` levels, covering several nesting depths under `UnlinkAll`/`LinkAll`. The tests were verified to fail on the unfixed code. Also bumps the version to 0.42.1 and adds a HISTORY.md entry. See TuringLang/Turing.jl#2844 Above is written by Claude/Codex, lightly touched by me.
1 parent b310eec commit 0a1499f

5 files changed

Lines changed: 94 additions & 6 deletions

File tree

HISTORY.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
# 0.42.1
2+
3+
Fixed a type-inference failure that made nested submodels (a `~ to_submodel(...)` statement inside a model that is itself evaluated as a submodel) very slow.
4+
5+
Previously, evaluating a submodel recursed through the shared `_evaluate!!(::Model, ::AbstractVarInfo)` method. Each level of nesting adds another context layer to the `Model` type, which tripped Julia's type-inference recursion limit: from the first level of nesting onwards the return type was inferred as `Any` and evaluation fell back to runtime dispatch, slowing down both primal and gradient evaluation (the primal slowdown was roughly 15x, growing with nesting depth). Submodel evaluation now calls the model function directly, keeping nested submodels type-stable. See [Turing.jl#2844](https://github.com/TuringLang/Turing.jl/issues/2844).
6+
17
# 0.42.0
28

39
`LogDensityFunction` now performs AD preparation through AbstractPPL's `prepare` / `value_and_gradient!!` interface instead of calling DifferentiationInterface directly. Internally this removes the `_use_closure` heuristic and the explicit `DI.Constant` plumbing; the choice between closure and constants now lives in AbstractPPL.

Project.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
name = "DynamicPPL"
22
uuid = "366bfd00-2699-11ea-058f-f148b4cae6d8"
3-
version = "0.42"
3+
version = "0.42.1"
44

55
[deps]
66
ADTypes = "47edcb42-4c32-4615-8424-f2b9edc5f35b"

src/submodel.jl

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -181,9 +181,17 @@ function _evaluate!!(
181181
# (4) Finally, we need to store that context inside the submodel.
182182
model = contextualize(submodel.model, eval_context)
183183

184-
# Once that's all set up nicely, we can just _evaluate!! the wrapped model. This
185-
# returns a tuple of submodel.model's return value and the new varinfo.
186-
return _evaluate!!(model, vi)
184+
# Evaluate the wrapped model. These two lines are a verbatim copy of the body of
185+
# `_evaluate!!(model::Model, ::AbstractVarInfo)` (in `model.jl`), and the duplication is
186+
# deliberate: DO NOT replace them with `return _evaluate!!(model, vi)`. Each level of
187+
# submodel nesting grows the contextualised `Model`'s context type, and routing the
188+
# recursion through the shared `_evaluate!!(::Model, ...)` method trips Julia's recursion
189+
# limiter, which widens the `Model` argument to abstract and collapses the return type to
190+
# `Any`. Calling `model.f` directly avoids that. See
191+
# https://github.com/TuringLang/DynamicPPL.jl/pull/1427 and
192+
# https://github.com/TuringLang/Turing.jl/issues/2844 for the full explanation.
193+
args, kwargs = make_evaluate_args_and_kwargs(model, vi)
194+
return model.f(args...; kwargs...)
187195
end
188196

189197
function tilde_observe!!(

test/logdensityfunction.jl

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,30 @@ using DifferentiationInterface: DifferentiationInterface
1717
using ForwardDiff: ForwardDiff
1818
using Mooncake: Mooncake
1919

20+
@model function issue_2844_nested_inner()
21+
p ~ Normal()
22+
return (; p)
23+
end
24+
@model function issue_2844_nested_middle()
25+
inner ~ to_submodel(issue_2844_nested_inner())
26+
return (; p=inner.p)
27+
end
28+
@model function issue_2844_nested_outer()
29+
middle ~ to_submodel(issue_2844_nested_middle())
30+
return 0.0 ~ Normal(middle.p)
31+
end
32+
# One submodel boundary deeper than `issue_2844_nested_outer`, so the inference test
33+
# guards against a regression that merely pushes the recursion limit out by one level
34+
# rather than removing it.
35+
@model function issue_2844_nested_middle2()
36+
middle ~ to_submodel(issue_2844_nested_middle())
37+
return (; p=middle.p)
38+
end
39+
@model function issue_2844_nested_outer_deep()
40+
middle2 ~ to_submodel(issue_2844_nested_middle2())
41+
return 0.0 ~ Normal(middle2.p)
42+
end
43+
2044
@testset "LogDensityFunction: constructors" begin
2145
dist = Beta(2, 2)
2246
@model f() = x ~ dist
@@ -420,6 +444,21 @@ end
420444
end skip = skip
421445
end
422446
end
447+
448+
# See https://github.com/TuringLang/DynamicPPL.jl/pull/1427.
449+
@testset "nested to_submodel return values" begin
450+
@testset "$(nameof(model.f))" for model in (
451+
issue_2844_nested_outer(), issue_2844_nested_outer_deep()
452+
)
453+
@testset "$tfm_strategy" for tfm_strategy in (UnlinkAll(), LinkAll())
454+
ldf = DynamicPPL.LogDensityFunction(
455+
model, DynamicPPL.getlogjoint_internal, tfm_strategy
456+
)
457+
x = rand(ldf)
458+
@test @inferred(LogDensityProblems.logdensity(ldf, x)) isa Float64
459+
end
460+
end
461+
end
423462
end
424463

425464
@testset "LogDensityFunction: performance" begin
@@ -442,8 +481,18 @@ end
442481
y ~ Normal(params.m, params.s)
443482
return 1.0 ~ Normal(y)
444483
end
445-
@testset for model in
446-
(f(), submodel_inner() | (; s=0.0), submodel_outer(submodel_inner()))
484+
# A submodel nested inside another submodel; see
485+
# https://github.com/TuringLang/DynamicPPL.jl/pull/1427.
486+
@model function submodel_middle(inner)
487+
mid ~ to_submodel(inner)
488+
return (m=mid.m, s=mid.s)
489+
end
490+
@testset for model in (
491+
f(),
492+
submodel_inner() | (; s=0.0),
493+
submodel_outer(submodel_inner()),
494+
submodel_outer(submodel_middle(submodel_inner())),
495+
)
447496
@testset for tfm_strategy in (UnlinkAll(), LinkAll())
448497
ldf = DynamicPPL.LogDensityFunction(model, getlogjoint_internal, tfm_strategy)
449498
x = rand(ldf)

test/submodels.jl

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,15 @@ function get_logp_and_rawval_accs(model::Model)
1717
return accs
1818
end
1919

20+
# Models for the nested-submodel type-stability tests; see
21+
# https://github.com/TuringLang/DynamicPPL.jl/pull/1427. Each level wraps the previous one in
22+
# a `to_submodel`. They must be defined at module scope: a model defined in local (testset)
23+
# scope is not type-inferrable, which would mask the property under test.
24+
@model t2844_inner() = (x ~ Normal(); return (; x))
25+
@model t2844_middle() = (a ~ to_submodel(t2844_inner()); return (; x=a.x))
26+
@model t2844_outer() = (b ~ to_submodel(t2844_middle()); return (; x=b.x))
27+
@model t2844_deeper() = (c ~ to_submodel(t2844_outer()); return (; x=c.x))
28+
2029
@testset "submodels.jl" begin
2130
@testset "$op with AbstractPPL API" for op in [condition, fix]
2231
x_val = 1.0
@@ -338,6 +347,22 @@ end
338347
@test vnt.data.a.data.b.data.x.data isa Matrix{Float64}
339348
@test size(vnt.data.a.data.b.data.x.data) == (2, 2)
340349
end
350+
351+
@testset "type stability of nested submodels (issue #2844)" begin
352+
# See https://github.com/TuringLang/DynamicPPL.jl/pull/1427.
353+
@testset "$(nameof(model.f))" for model in (
354+
t2844_inner(), t2844_middle(), t2844_outer(), t2844_deeper()
355+
)
356+
# The fast evaluation path: `init!!` into an `OnlyAccsVarInfo`, under both
357+
# transform strategies.
358+
@testset "$tfm" for tfm in (UnlinkAll(), LinkAll())
359+
accs = setacc!!(OnlyAccsVarInfo(), LogPriorAccumulator())
360+
@test @inferred(init!!(model, accs, InitFromPrior(), tfm)) isa Tuple
361+
end
362+
# Evaluating a pre-populated `VarInfo` must also stay type stable.
363+
@test @inferred(DynamicPPL.evaluate_nowarn!!(model, VarInfo(model))) isa Tuple
364+
end
365+
end
341366
end
342367

343368
end

0 commit comments

Comments
 (0)