Skip to content

Correctness of Libtask with submodels depends on whether submodels' functions are inlined or not #2772

@penelopeysm

Description

@penelopeysm

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions