Libtask

Libtask is best explained by the docstring for TapedTask:

Libtask.TapedTaskType
TapedTask(taped_globals::Any, f, args...; kwargs...)

Construct a TapedTask with the specified taped_globals, for function f, positional arguments args, and keyword argument kwargs.

Extended Help

There are three central features of a TapedTask, which we demonstrate via three examples.

Resumption

The function Libtask.produce has a special meaning in Libtask. You can insert it into regular Julia functions anywhere that you like. For example

julia> function f()
           for t in 1:2
               produce(t)
               t += 1
           end
           return nothing
       end
f (generic function with 1 method)

If you construct a TapedTask from f, and call Libtask.consume on it, you'll see

julia> t = TapedTask(nothing, f);

julia> consume(t)
1

The semantics of this are that Libtask.consume runs the function f until it reaches the call to Libtask.produce, at which point it will return the argument to Libtask.produce.

Subsequent calls to Libtask.produce will resume execution of f immediately after the last Libtask.produce statement that was hit.

julia> consume(t)
2

When there are no more Libtask.produce statements to hit, calling Libtask.consume will return nothing:

julia> consume(t)

Copying

TapedTasks can be copied. Doing so creates a completely independent object. For example:

julia> t2 = TapedTask(nothing, f);

julia> consume(t2)
1

If we make a copy and advance its state, it produces the same value that the original would have produced:

julia> t3 = copy(t2);

julia> consume(t3)
2

Moreover, advancing the state of the copy has not advanced the state of the original, because they are completely independent copies:

julia> consume(t2)
2

TapedTask-Specific Globals

It is often desirable to permit a copy of a task and the original to differ in very specific ways. For example, in the context of Sequential Monte Carlo, you might want the only difference between two copies to be their random number generator.

A generic mechanism is available to achieve this. Libtask.get_taped_globals and Libtask.set_taped_globals! let you set and retrieve a variable which is specific to a given Libtask.TapedTask. The former can be called inside a function:

julia> function f()
           produce(get_taped_globals(Int))
           produce(get_taped_globals(Int))
           return nothing
       end
f (generic function with 1 method)

The first argument to Libtask.TapedTask is the value that Libtask.get_taped_globals will return:

julia> t = TapedTask(1, f);

julia> consume(t)
1

The value that it returns can be changed between Libtask.consume calls:

julia> set_taped_globals!(t, 2)

julia> consume(t)
2

Ints have been used here, but it is permissible to set the value returned by Libtask.get_taped_globals to anything you like.

Implementation Notes

Under the hood, we implement a TapedTask by obtaining the IRCode associated to the original function, transforming it so that it implements the semantics required by the produce / consume interface, and placing it inside a MistyClosure to make it possible to execute.

There are two main considerations when transforming the IRCode. The first is to ensure that the "state" of a TapedTask can be copied, so that a TapedTask can be copied, and resumed later. The complete state of a TapedTask is given by its arguments, and the value associated to each ssa (these are initially undefined). To make it possible to copy the state of the ssa values, we place Base.RefValue{T}s into the captures of the MistyClosure which implements the TapedTask, one for each ssa in the IR (T is the type inferred for that ssa). A call is replaced by reading in values of ssas from these refs, applying the original operation, and writing the result to the ref associated to the instruction. For example, if the original snippet of IRCode is something like

%5 = f(%3, _1)

the transformed IR would be something like

%5 = ref_for_%3[]
%6 = f(%5, _1)
ref_for_%5[] = %6

Setting things up in this manner ensures that an independent copy is made by simply copying all of the refs. A deepcopy is required for correctness as, while the refs do not alias one another (by construction), their contents might. For example, two refs may contain the same Array, and in general the behaviour of a function depends on this relationship.

The second component of the transformation is implementing the produce mechanism, and the ability to resume computation from where we produced. Roughly speaking, the IRCode must be modified to ensure that whenever a produce call in encountered, the MistyClosure returns the argument to produce, and that subsequent calls resume computation immediately after the produce statement. This resumption is achieved by setting the value of a counter prior to returning following a produce statement – a sequence of comparisons against this counter, and GotoIfNot statement are inserted at the top of the IR. These are used to jump to the point in the code from which computation should resume. These are set up such that, when the TapedTask is first run, computation start froms the first statement. Observe that this is also facilitated by the ref mechanism discussed above, as it ensures that the state persists between calls to a MistyClosure.

The above gives the broad outline of how TapedTasks are implemented. We refer interested readers to the code, which is extensively commented to explain implementation details.

source

The functions discussed in the above docstring (in addition to TapedTask itself) form the public interface of Libtask.jl. They divide neatly into two kinds of functions: those which are used to manipulate TapedTasks, and those which are intended to be used inside a TapedTask.

Manipulation of TapedTasks:

Libtask.consumeFunction
consume(t::TapedTask)

Run t until it makes a call to produce. If this is the first time that t has been called, it starts execution from the entry point. If consume has previously been called on t, it will resume from the last produce call. If there are no more produce calls, nothing will be returned.

source
Base.copyMethod
Base.copy(t::TapedTask)

Makes a completely independent copy of t. consume can be applied to either the copy of t or the original without advancing the state of the other.

source
Libtask.set_taped_globals!Function
set_taped_globals!(t::TapedTask, new_taped_globals)::Nothing

Set the taped_globals of t to new_taped_globals. Any calls to get_taped_globals in future calls to consume(t) (either directly, or implicitly via iteration) will see this new value.

source

Functions for use inside a TapedTasks:

Libtask.get_taped_globalsFunction
get_taped_globals(T::Type)

When called from inside a call to a TapedTask, this will return whatever is contained in its taped_globals field.

The type T is required for optimal performance. If you know that the result of this operation must return a specific type, specify T. If you do not know what type it will return, pass Any – this will typically yield type instabilities, but will run correctly.

See also set_taped_globals!.

source

An opt-in mechanism marks functions that might contain Libtask.produce statements.

Libtask.might_produceMethod
might_produce(sig::Type{<:Tuple})::Bool

true if a call to method with signature sig is permitted to contain Libtask.produce statements.

This is an opt-in mechanism. the fallback method of this function returns false indicating that, by default, we assume that calls do not contain Libtask.produce statements.

source