@@ -4,24 +4,103 @@ __v::Int = 5
4
4
return nothing
5
5
end
6
6
7
- mutable struct CopyableTask {Tmc<: MistyClosure ,Targs}
7
+ mutable struct TapedTask {Tmc<: MistyClosure ,Targs}
8
8
const mc:: Tmc
9
9
args:: Targs
10
10
const position:: Base.RefValue{Int32}
11
+ const deepcopy_types:: Type
11
12
end
12
13
13
- @inline consume (t:: CopyableTask ) = t. mc (t. args... )
14
+ """
15
+ Base.copy(t::TapedTask)
16
+
17
+ Makes a copy of `t` which can be run. For the most part, calls to [`consume`](@ref) on the
18
+ copied task will give the same results as the original. There are, however, substantial
19
+ limitations to this, detailed in the extended help.
20
+
21
+ # Extended Help
22
+
23
+ We call a copy of a `TapedTask` _consistent_ with the original if the call to `==` in the
24
+ loop below always returns `true`:
25
+ ```julia
26
+ t = <some_TapedTask>
27
+ tc = copy(t)
28
+ for (v, vc) in zip(t, tc)
29
+ v == vc
30
+ end
31
+ ```
32
+ (provided that `==` is implemented for all `v` that are produced). Convesely, we refer to a
33
+ copy as _inconsistent_ if this property doesn't hold. In order to ensure
34
+ consistency, we need to ensure that independent copies are made of anything which might be
35
+ mutated by the task or its copy during subsequent `consume` calls. Failure to do this can
36
+ cause problems if, for example, a task reads-to and writes-from some memory.
37
+ If we call `consume` on the original task, and then on a copy of it, any changes made by the
38
+ original will be visible to the copy, potentially causing its behaviour to differ. This can
39
+ manifest itself as a race condition if the task and its copies are run concurrently.
40
+
41
+ To understand a bit more about when a task is / is not consistent, we need to dig into the
42
+ rather specific semantics of `copy`. Calling `copy` on a `TapedTask` does the following:
43
+ 1. `copy` the `position` field,
44
+ 2. `map`s `_tape_copy` over the `args` field, and
45
+ 3. `map`s `_tape_copy` over the all of the data closed over in the `OpaqueClosure` which
46
+ implements the task (specifically the values _inside_ the `Ref`s) -- call these the
47
+ `captures`. Except the last elements of this data, because this is `===` to the
48
+ `position` field -- for this element we use the copy we made in step 1.
49
+
50
+ `_tape_copy` doesn't actually make a copy of the object at all if it is not either an
51
+ `Array`, a `Ref`, or an instance of one of the types listed in the task's `deepcopy_type`
52
+ field. If it is an instance of one of these types then `_tape_copy` just calls `deepcopy`.
53
+
54
+ This behaviour is plainly entirely acceptable if the argument to `_tape_copy` is a bits
55
+ type. For any `mutable struct`s which aren't flagged for `deepcopy`ing, we have an immediate
56
+ risk of inconsistency. Similarly, for any `struct` types which aren't bits types (e.g.
57
+ those which contain an `Array`, `Ref`, or some other `mutable struct` either directly as one
58
+ of their fields, or as a field of a field, etc), we have an inconsistency risk.
59
+
60
+ Furthermore, for anything which _is_ `deepcopy`ed we introduce inconsistency risks. If, for
61
+ example, two elements of the data closed over by the task alias one another, calling
62
+ `deepcopy` on them separately will cause the copies to _not_ alias one another.
63
+ The same thing can happen if one element is `deepcopy`ed and the other not. For example, if
64
+ we have both an `Array` `x` and `view(x, inds)` stored in separate elements of `captures`,
65
+ `x` will be `deepcopy`ed, while `view(x, inds)` will not. In the copy of `captures`, the
66
+ `view` will still be a view into the original `x`, not the `deepcopy`ed version. Again, this
67
+ introduces inconsistency.
68
+
69
+ Why do we have these semantics? We have them because Libtask has always had them, and at the
70
+ time of writing we're unsure whether AdvancedPS.jl, and by extension Turing.jl rely on this
71
+ behaviour.
72
+
73
+ What other options do we have? Simply calling `deepcopy` on a `TapedTask` works fine, and
74
+ should reliably result in consistent behaviour between a `TapedTask` and any copies of it.
75
+ This would, therefore, be a preferable implementation. We should try to determine whether
76
+ this is a viable option.
77
+ """
78
+ function Base. copy (t:: T ) where {T<: TapedTask }
79
+ captures = t. mc. oc. captures
80
+ new_captures = map (Base. Fix2 (_tape_copy, t. deepcopy_types), captures)
81
+ new_position = new_captures[end ] # baked in later on.
82
+ new_args = map (Base. Fix2 (_tape_copy, t. deepcopy_types), t. args)
83
+ new_mc = Mooncake. replace_captures (t. mc, new_captures)
84
+ return T (new_mc, new_args, new_position, t. deepcopy_types)
85
+ end
86
+
87
+ _tape_copy (v, deepcopy_types:: Type ) = v isa deepcopy_types ? deepcopy (v) : v
88
+
89
+ # Not sure that we need this in the new implementation.
90
+ _tape_copy (box:: Core.Box , deepcopy_types:: Type ) = error (" Found a box" )
91
+
92
+ @inline consume (t:: TapedTask ) = t. mc (t. args... )
14
93
15
- function initialise! (t:: CopyableTask , args:: Vararg{Any,N} ):: Nothing where {N}
94
+ function initialise! (t:: TapedTask , args:: Vararg{Any,N} ):: Nothing where {N}
16
95
t. position[] = - 1
17
96
t. args = args
18
97
return nothing
19
98
end
20
99
21
- function CopyableTask (fargs... )
100
+ function TapedTask (fargs... ; deepcopy_types :: Type = Union{} )
22
101
sig = typeof (fargs)
23
102
mc, count_ref = build_callable (Base. code_ircode_by_type (sig)[1 ][1 ])
24
- return CopyableTask (mc, fargs[2 : end ], count_ref)
103
+ return TapedTask (mc, fargs[2 : end ], count_ref, Union{deepcopy_types, Array, Ref} )
25
104
end
26
105
27
106
function build_callable (ir:: IRCode )
36
115
might_produce(sig::Type{<:Tuple})::Bool
37
116
38
117
`true` if a call to method with signature `sig` is permitted to contain
39
- `CopyableTasks .produce` statements.
118
+ `Libtask .produce` statements.
40
119
41
120
This is an opt-in mechanism. the fallback method of this function returns `false` indicating
42
- that, by default, we assume that calls do not contain `CopyableTasks .produce` statements.
121
+ that, by default, we assume that calls do not contain `Libtask .produce` statements.
43
122
"""
44
123
might_produce (:: Type{<:Tuple} ) = false
45
124
382
461
@inline deref_phi (:: R , x) where {R<: Tuple } = x
383
462
384
463
# Implement iterator interface.
385
- function Base. iterate (t:: CopyableTask , state:: Nothing = nothing )
464
+ function Base. iterate (t:: TapedTask , state:: Nothing = nothing )
386
465
v = consume (t)
387
466
return v === nothing ? nothing : (v, nothing )
388
467
end
389
- Base. IteratorSize (:: Type{<:CopyableTask } ) = Base. SizeUnknown ()
390
- Base. IteratorEltype (:: Type{<:CopyableTask } ) = Base. EltypeUnknown ()
468
+ Base. IteratorSize (:: Type{<:TapedTask } ) = Base. SizeUnknown ()
469
+ Base. IteratorEltype (:: Type{<:TapedTask } ) = Base. EltypeUnknown ()
0 commit comments