1
1
#=
2
2
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
+
15
15
In a nutshell, there are two types of instructions (or primitives) on a tape:
16
16
- Ordinary function call
17
17
- 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.
20
20
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
23
23
these features, we implemented a fork mechanism for TapedTask.
24
-
24
+
25
25
Some potentially sharp edges of this implementation:
26
26
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;`,
30
30
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
32
32
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,
35
35
for example, using broadcasting instead of a loop.
36
36
=#
37
37
@@ -58,8 +58,9 @@ mutable struct TapedFunction{F, TapeType}
58
58
binding_values:: Bindings
59
59
arg_binding_slots:: Vector{Int} # arg indices in binding_values
60
60
retval_binding_slot:: Int # 0 indicates the function has not returned
61
+ deepcopy_types:: Vector{Any}
61
62
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}
63
64
args_type = _accurate_typeof .(args)
64
65
cache_key = (f, args_type... )
65
66
@@ -72,17 +73,17 @@ mutable struct TapedFunction{F, TapeType}
72
73
ir = _infer (f, args_type)
73
74
binding_values, slots, tape = translate! (RawTape (), ir)
74
75
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 )
76
77
TRCache[cache_key] = tf # set cache
77
78
return tf
78
79
end
79
80
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 )
82
83
83
84
function TapedFunction {F, T0} (tf:: TapedFunction{F, T1} ) where {F, T0, T1}
84
85
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 )
86
87
end
87
88
88
89
TapedFunction (tf:: TapedFunction{F, T} ) where {F, T} = TapedFunction {F, T} (tf)
@@ -444,31 +445,50 @@ end
444
445
# # copy Bindings, TapedFunction
445
446
446
447
"""
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.
448
464
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.
453
465
"""
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)
456
470
# 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
461
481
462
- function copy_bindings (old:: Bindings )
482
+ function copy_bindings (old:: Bindings , deepcopy_types )
463
483
newb = copy (old)
464
484
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 )
466
486
end
467
487
return newb
468
488
end
469
489
470
490
function Base. copy (tf:: TapedFunction )
471
491
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 )
473
493
return new_tf
474
494
end
0 commit comments