Consider the following models, which are entirely equivalent:
using Turing, Random
@model function inner(y, x)
y ~ Normal(x)
end
@model function nested(y)
x ~ Normal()
a ~ to_submodel(inner(y, x))
end
m1 = nested(1.0)
mean(sample(Xoshiro(468), m1, PG(10), 1000))
@model function single(y)
x ~ Normal()
y ~ Normal(x)
end
m2 = single(1.0)
mean(sample(Xoshiro(468), m2, PG(10), 1000))
It turns out that (at least on my system) both of these sample correctly with PG (giving a mean x of 0.46).
If you think about it carefully though, there's no reason why the submodel one should work. The call to Libtask.produce is inside the y ~ Normal(x) statement, which is in turn inside the inner submodel. So, in order to get all the way to the produce call, we need to mark everything along the call stack with Libtask.might_produce. This is all basic Libtask functionality.
The problem is that the method inner(model, varinfo, ...) itself has never been marked as produce-able! So how does this sampling actually work correctly, then?
The answer is that if the submodel is small enough, then the method inner(model, varinfo, ...) will just be inlined.
We can demonstrate what happens when this fails to inline, by changing the output of the DynamicPPL compiler. If we were to apply this patch on the current release of DPPL (v0.39.13)
diff --git a/src/compiler.jl b/src/compiler.jl
index 84a9a485..604e0b32 100644
--- a/src/compiler.jl
+++ b/src/compiler.jl
@@ -710,6 +710,7 @@ function build_output(modeldef, linenumbernode)
# element in the returned value is always the most up-to-date `__varinfo__`.
# See the docstrings of `replace_returns` for more info.
evaluatordef[:body] = MacroTools.@q begin
+ @noinline
$(linenumbernode)
$(replace_returns(add_return_to_last_statment(modeldef[:body])))
end
and rerun the PG sampling for the submodel, we get a mean x of 0.01, which is consistent with the call to produce never actually being triggered.
So essentially, we are pretty much at the mercy of the Julia compiler here. If it decides to inline the submodel, we are good. If it doesn't, we will silently give wrong results.
Consider the following models, which are entirely equivalent:
It turns out that (at least on my system) both of these sample correctly with PG (giving a mean
xof 0.46).If you think about it carefully though, there's no reason why the submodel one should work. The call to
Libtask.produceis inside they ~ Normal(x)statement, which is in turn inside theinnersubmodel. So, in order to get all the way to the produce call, we need to mark everything along the call stack withLibtask.might_produce. This is all basic Libtask functionality.The problem is that the method
inner(model, varinfo, ...)itself has never been marked as produce-able! So how does this sampling actually work correctly, then?The answer is that if the submodel is small enough, then the method
inner(model, varinfo, ...)will just be inlined.We can demonstrate what happens when this fails to inline, by changing the output of the DynamicPPL compiler. If we were to apply this patch on the current release of DPPL (v0.39.13)
and rerun the PG sampling for the submodel, we get a mean
xof 0.01, which is consistent with the call toproducenever actually being triggered.So essentially, we are pretty much at the mercy of the Julia compiler here. If it decides to inline the submodel, we are good. If it doesn't, we will silently give wrong results.