Skip to content

Commit 4322ecd

Browse files
KDr2yebai
andauthored
Deprecate TArray and TRef. (#152)
* deprecate `TArray` and `TRef`. * remote TArray and TRef * docs update * Update Project.toml Co-authored-by: Hong Ge <[email protected]>
1 parent 9579c93 commit 4322ecd

10 files changed

+135
-471
lines changed

Diff for: Project.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ uuid = "6f1fad26-d15e-5dc8-ae53-837a1d7b8c9f"
33
license = "MIT"
44
desc = "Tape based task copying in Turing"
55
repo = "https://github.com/TuringLang/Libtask.jl.git"
6-
version = "0.7.5"
6+
version = "0.8"
77

88
[deps]
99
FunctionWrappers = "069b7b12-0de2-55c6-9aab-29f3d0a68a2e"

Diff for: README.md

+15-18
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ a = copy(ttask)
3232
@show consume(ttask) # 3
3333
```
3434

35-
Heap allocated objects are shallow copied:
35+
Array and Ref objects are deep copied:
3636

3737
```julia
3838
using Libtask
@@ -50,46 +50,43 @@ ttask = TapedTask(f)
5050
@show consume(ttask) # 0
5151
@show consume(ttask) # 1
5252

53-
a = copy(t)
53+
a = copy(ttask)
5454
@show consume(a) # 2
5555
@show consume(a) # 3
5656

57-
@show consume(ttask) # 4
58-
@show consume(ttask) # 5
57+
@show consume(ttask) # 2
58+
@show consume(ttask) # 3
5959
```
6060

61-
In constrast to standard arrays, which are only shallow copied during
62-
task copying, `TArray`, an array data structure provided by Libtask,
63-
is deep copied during the copying process of a task:
61+
Others Heap allocated objects (e.g., `Dict`) are shallow copied:
6462

6563
```julia
6664
using Libtask
6765

6866
function f()
69-
t = TArray(Int, 1)
70-
t[1] = 0
71-
for _ in 1:10
67+
t = Dict(1=>10, 2=>20)
68+
while true
7269
produce(t[1])
7370
t[1] = 1 + t[1]
7471
end
7572
end
7673

7774
ttask = TapedTask(f)
7875

79-
@show consume(ttask) # 0
80-
@show consume(ttask) # 1
76+
@show consume(ttask) # 10
77+
@show consume(ttask) # 11
8178

8279
a = copy(ttask)
83-
@show consume(a) # 2
84-
@show consume(a) # 3
80+
@show consume(a) # 12
81+
@show consume(a) # 13
8582

86-
@show consume(ttask) # 2
87-
@show consume(ttask) # 3
83+
@show consume(ttask) # 14
84+
@show consume(ttask) # 15
8885
```
8986

90-
Notes:
87+
Notes:
9188

92-
- The [Turing](https://github.com/TuringLang/Turing.jl) probabilistic programming
89+
- The [Turing](https://github.com/TuringLang/Turing.jl) probabilistic programming
9390
language uses this task copying feature in
9491
an efficient implementation of the [particle
9592
filtering](https://en.wikipedia.org/wiki/Particle_filter) sampling

Diff for: src/Libtask.jl

+2-4
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,10 @@ using FunctionWrappers: FunctionWrapper
44
using LRUCache
55

66
export TapedTask, consume, produce
7-
export TArray, tzeros, tfill, TRef
7+
8+
export TArray, tzeros, tfill, TRef # legacy types back compat
89

910
include("tapedfunction.jl")
1011
include("tapedtask.jl")
1112

12-
include("tarray.jl")
13-
include("tref.jl")
14-
1513
end

Diff for: src/tapedfunction.jl

+62-42
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,37 @@
11
#=
22
3-
`TapedFunction` converts a Julia function to a friendly tape for user-specified interpreters.
4-
With this tape-like abstraction for functions, we gain some control over how the function is
5-
executed, like capturing continuations, caching variables, injecting additional control flows
6-
(i.e. produce/consume) between instructions on the tape, etc.
7-
8-
Under the hood, we firstly used Julia's compiler API to get the IR code of the original function.
9-
We use the unoptimised typed code in a non-strict SSA form. Then we convert each IR instruction
10-
to a Julia data structure (an object of a subtype of AbstractInstruction). All the operands
11-
(i.e., the variables) these instructions use are stored in a data structure called `Bindings`.
12-
This conversion/binding process is performed at compile-time / tape-recording time and is only
13-
done once for each function.
14-
3+
`TapedFunction` converts a Julia function to a friendly tape for user-specified interpreters.
4+
With this tape-like abstraction for functions, we gain some control over how the function is
5+
executed, like capturing continuations, caching variables, injecting additional control flows
6+
(i.e. produce/consume) between instructions on the tape, etc.
7+
8+
Under the hood, we firstly used Julia's compiler API to get the IR code of the original function.
9+
We use the unoptimised typed code in a non-strict SSA form. Then we convert each IR instruction
10+
to a Julia data structure (an object of a subtype of AbstractInstruction). All the operands
11+
(i.e., the variables) these instructions use are stored in a data structure called `Bindings`.
12+
This conversion/binding process is performed at compile-time / tape-recording time and is only
13+
done once for each function.
14+
1515
In a nutshell, there are two types of instructions (or primitives) on a tape:
1616
- Ordinary function call
1717
- Control-flow instruction: GotoInstruction and CondGotoInstruction, ReturnInstruction
18-
19-
Once the tape is recorded, we can run the tape just like calling the original function.
18+
19+
Once the tape is recorded, we can run the tape just like calling the original function.
2020
We first plugin the arguments, run each instruction on the tape, and stop after encountering
21-
a ReturnInstruction. We also provide a mechanism to add a callback after each instruction.
22-
This API allowed us to implement the `produce/consume` machanism in TapedTask. And exploiting
21+
a ReturnInstruction. We also provide a mechanism to add a callback after each instruction.
22+
This API allowed us to implement the `produce/consume` machanism in TapedTask. And exploiting
2323
these features, we implemented a fork mechanism for TapedTask.
24-
24+
2525
Some potentially sharp edges of this implementation:
2626
27-
1. GlobalRef is evaluated at the tape-recording time (compile-time). Most times,
28-
the value/object associated with a GlobalRef does not change at run time.
29-
So this works well. But, if you do something like `module A v=1 end; make tapedfunction; A.eval(:(v=2)); run tf;`,
27+
1. GlobalRef is evaluated at the tape-recording time (compile-time). Most times,
28+
the value/object associated with a GlobalRef does not change at run time.
29+
So this works well. But, if you do something like `module A v=1 end; make tapedfunction; A.eval(:(v=2)); run tf;`,
3030
The assignment won't work.
31-
2. QuoteNode is also evaluated at the tape-recording time (compile-time). Primarily
31+
2. QuoteNode is also evaluated at the tape-recording time (compile-time). Primarily
3232
the result of evaluating a QuoteNode is a Symbol, which works well most of the time.
33-
3. Each Instruction execution contains one unnecessary allocation at the moment.
34-
So writing a function with vectorised computation will be more performant,
33+
3. Each Instruction execution contains one unnecessary allocation at the moment.
34+
So writing a function with vectorised computation will be more performant,
3535
for example, using broadcasting instead of a loop.
3636
=#
3737

@@ -58,8 +58,9 @@ mutable struct TapedFunction{F, TapeType}
5858
binding_values::Bindings
5959
arg_binding_slots::Vector{Int} # arg indices in binding_values
6060
retval_binding_slot::Int # 0 indicates the function has not returned
61+
deepcopy_types::Vector{Any}
6162

62-
function TapedFunction{F, T}(f::F, args...; cache=false) where {F, T}
63+
function TapedFunction{F, T}(f::F, args...; cache=false, deepcopy_types=[]) where {F, T}
6364
args_type = _accurate_typeof.(args)
6465
cache_key = (f, args_type...)
6566

@@ -72,17 +73,17 @@ mutable struct TapedFunction{F, TapeType}
7273
ir = _infer(f, args_type)
7374
binding_values, slots, tape = translate!(RawTape(), ir)
7475

75-
tf = new{F, T}(f, length(args), ir, tape, 1, binding_values, slots, 0)
76+
tf = new{F, T}(f, length(args), ir, tape, 1, binding_values, slots, 0, deepcopy_types)
7677
TRCache[cache_key] = tf # set cache
7778
return tf
7879
end
7980

80-
TapedFunction(f, args...; cache=false) =
81-
TapedFunction{typeof(f), RawTape}(f, args...; cache=cache)
81+
TapedFunction(f, args...; cache=false, deepcopy_types=[]) =
82+
TapedFunction{typeof(f), RawTape}(f, args...; cache=cache, deepcopy_types=deepcopy_types)
8283

8384
function TapedFunction{F, T0}(tf::TapedFunction{F, T1}) where {F, T0, T1}
8485
new{F, T0}(tf.func, tf.arity, tf.ir, tf.tape,
85-
tf.counter, tf.binding_values, tf.arg_binding_slots, 0)
86+
tf.counter, tf.binding_values, tf.arg_binding_slots, 0, tf.deepcopy_types)
8687
end
8788

8889
TapedFunction(tf::TapedFunction{F, T}) where {F, T} = TapedFunction{F, T}(tf)
@@ -444,31 +445,50 @@ end
444445
## copy Bindings, TapedFunction
445446

446447
"""
447-
tape_copy(x)
448+
tape_shallowcopy(x)
449+
tape_deepcopy(x)
450+
451+
Function `tape_shallowcopy` and `tape_deepcopy` are used to copy data
452+
while copying a TapedFunction. A value in the bindings of a
453+
TapedFunction is either `tape_shallowcopy`ed or `tape_deepcopy`ed. For
454+
TapedFunction, all types are shallow copied by default, and you can
455+
specify some types to be deep copied by giving the `deepcopy_types`
456+
kwyword argument while constructing a TapedFunction.
457+
458+
The default behaviour of `tape_shallowcopy` is, we return its argument
459+
untouched, like `identity` does, i.e., `tape_copy(x) = x`. The default
460+
behaviour of `tape_deepcopy` is, we call `deepcopy` on its argument
461+
and return the result, `tape_deepcopy(x) = deepcopy(x)`. If one wants
462+
some kinds of data to be copied (shallowly or deeply) in a different
463+
way, one can overload these functions.
448464
449-
Function `tape_copy` is used to copy data while copying a
450-
TapedFunction, the default behaviour is: we perform share the data
451-
between tasks, i.e., `tape_copy(x) = x`. If one wants some kinds of
452-
data to be copied, or deeply copied, one can overload this function.
453465
"""
454-
function tape_copy end
455-
tape_copy(x) = x
466+
function tape_shallowcopy end, function tape_deepcopy end
467+
468+
tape_shallowcopy(x) = x
469+
tape_deepcopy(x) = deepcopy(x)
456470
# Core.Box is used as closure captured variable container, so we should tape_copy its contents
457-
tape_copy(x::Core.Box) = Core.Box(tape_copy(x.contents))
458-
# ?? should we deepcopy Array and Dict by default?
459-
# tape_copy(x::Array) = deepcopy(x)
460-
# tape_copy(x::Dict) = deepcopy(x)
471+
tape_shallowcopy(x::Core.Box) = Core.Box(tape_shallowcopy(x.contents))
472+
tape_deepcopy(x::Core.Box) = Core.Box(tape_deepcopy(x.contents))
473+
474+
function _tape_copy(v, deepcopy_types)
475+
if any(t -> isa(v, t), deepcopy_types)
476+
tape_deepcopy(v)
477+
else
478+
tape_shallowcopy(v)
479+
end
480+
end
461481

462-
function copy_bindings(old::Bindings)
482+
function copy_bindings(old::Bindings, deepcopy_types)
463483
newb = copy(old)
464484
for k in 1:length(old)
465-
isassigned(old, k) && (newb[k] = tape_copy(old[k]))
485+
newb[k] = _tape_copy(old[k], deepcopy_types)
466486
end
467487
return newb
468488
end
469489

470490
function Base.copy(tf::TapedFunction)
471491
new_tf = TapedFunction(tf)
472-
new_tf.binding_values = copy_bindings(tf.binding_values)
492+
new_tf.binding_values = copy_bindings(tf.binding_values, tf.deepcopy_types)
473493
return new_tf
474494
end

Diff for: src/tapedtask.jl

+19-3
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,8 @@ end
6565

6666
# NOTE: evaluating model without a trace, see
6767
# https://github.com/TuringLang/Turing.jl/pull/1757#diff-8d16dd13c316055e55f300cd24294bb2f73f46cbcb5a481f8936ff56939da7ceR329
68-
function TapedTask(f, args...)
69-
tf = TapedFunction(f, args...; cache=true)
68+
function TapedTask(f, args...; deepcopy_types=[Array, Ref]) # deepcoy Array and Ref by default.
69+
tf = TapedFunction(f, args...; cache=true, deepcopy_types=deepcopy_types)
7070
TapedTask(tf, args...)
7171
end
7272

@@ -178,7 +178,7 @@ function Base.copy(t::TapedTask; args=())
178178
end
179179
else
180180
# the task is not started yet, but no args is given
181-
tape_copy.(t.args)
181+
map(a -> _tape_copy(a, t.tf.deepcopy_types), t.args)
182182
end
183183
end
184184
new_t = TapedTask(tf, task_args...)
@@ -187,3 +187,19 @@ function Base.copy(t::TapedTask; args=())
187187
new_t.task.storage[:tapedtask] = new_t
188188
return new_t
189189
end
190+
191+
# TArray and TRef back-compat
192+
function TArray(args...)
193+
Base.depwarn("`TArray` is deprecated, please use `Array` instead.", :TArray)
194+
Array(args...)
195+
end
196+
function TArray(T::Type, dim)
197+
Base.depwarn("`TArray` is deprecated, please use `Array` instead.", :TArray)
198+
Array{T}(undef, dim)
199+
end
200+
tzeros, tfill = zeros, fill
201+
202+
function TRef(x)
203+
Base.depwarn("`TRef` is deprecated, please use `Ref` instead.", :TArray)
204+
Ref(x)
205+
end

0 commit comments

Comments
 (0)