|
1 | 1 | # Task Affinity |
2 | 2 |
|
3 | | -Dagger.jl's `@spawn` macro allows precise control over task execution and result access using `scope`, `compute_scope`, and `result_scope`. |
| 3 | +Dagger.jl's `@spawn` macro allows precise control over task execution and result accessibility using `scope`, `compute_scope`, and `result_scope`, which specify various chunk scopes of the task. |
| 4 | + |
| 5 | +For more information on how these scopes work, see [Scopes](scopes.md#Scopes). |
4 | 6 |
|
5 | 7 | --- |
6 | 8 |
|
7 | 9 | ## Key Terms |
8 | 10 |
|
9 | 11 | ### Scope |
10 | | -`scope` defines the general set of locations where a Dagger task can execute. If `compute_scope` and `result_scope` are not explicitly set, the task's `compute_scope` defaults to its `scope`, and its `result_scope` defaults to `AnyScope()`, meaning the result can be accessed by any processor. Execution occurs on any processor within the defined scope. |
| 12 | +`scope` defines the general set of locations where a Dagger task can execute. If `scope` is not explicitly set, the task runs within the `compute_scope`. If both `scope` and `compute_scope` both are unspecified, the task falls back to `DefaultScope()`, allowing it to run wherever execution is possible. Execution occurs on any worker within the defined scope. |
11 | 13 |
|
12 | 14 | **Example:** |
13 | 15 | ```julia |
14 | | -g = Dagger.@spawn scope=ExactScope(Dagger.OSProc(3)) f(x,y) |
| 16 | +g = Dagger.@spawn scope=Dagger.scope(worker=3) f(x,y) |
15 | 17 | ``` |
16 | | -Task `g` executes only on Processor 3. Its result can be accessed by any processor. |
| 18 | +Task `g` executes only on worker 3. Its result can be accessed by any worker. |
17 | 19 |
|
18 | 20 | --- |
19 | 21 |
|
20 | 22 | ### Compute Scope |
21 | | -`compute_scope` also specifies where a Dagger task can execute. The key difference is if both `compute_scope` and `scope` are provided, `compute_scope` takes precedence over `scope` for execution placement. If `result_scope` isn't specified, it defaults to `AnyScope()`, allowing the result to be accessed by any processor. |
| 23 | +Like `scope`,`compute_scope` also specifies where a Dagger task can execute. The key difference is if both `compute_scope` and `scope` are provided, `compute_scope` takes precedence over `scope` for execution placement. If neither is specified, the they default to `DefaultScope()`. |
22 | 24 |
|
23 | 25 | **Example:** |
24 | 26 | ```julia |
25 | | -g1 = Dagger.@spawn scope=ExactScope(Dagger.ThreadProc(2, 3)) compute_scope=Dagger.UnionScope(ExactScope(Dagger.ThreadProc(1, 2)), ExactScope(Dagger.ThreadProc(3, 1))) f(x,y) |
26 | | -g2 = Dagger.@spawn compute_scope=Dagger.UnionScope(ExactScope(Dagger.ThreadProc(1, 2)), ExactScope(Dagger.ThreadProc(3, 1))) f(x,y) |
| 27 | +g1 = Dagger.@spawn scope=Dagger.scope(worker=2,thread=3) compute_scope=Dagger.scope((worker=1, thread=2), (worker=3, thread=1)) f(x,y) |
| 28 | +g2 = Dagger.@spawn compute_scope=Dagger.scope((worker=1, thread=2), (worker=3, thread=1)) f(x,y) |
27 | 29 | ``` |
28 | | -Task `g1` and `g2` execute on either thread 2 of processor 1, or thread 1 of processor 3. Their result can be accessed by any processor. |
| 30 | +Tasks `g1` and `g2` execute on either thread 2 of worker 1, or thread 1 of worker 3. The `scope` argument to `g1` is ignored. Their result can be accessed by any worker. |
29 | 31 |
|
30 | 32 | --- |
31 | 33 |
|
32 | 34 | ### Result Scope |
33 | 35 |
|
34 | | -`result_scope` restricts where a task's result can be fetched or moved. This is crucial for managing data locality and minimizing transfers. If only `result_scope` is specified, the `compute_scope` defaults to `Dagger.DefaultScope()`, meaning computation may happen on any processor. |
| 36 | +The result_scope limits the workers from which a task's result can be accessed. This is crucial for managing data locality and minimizing transfers. If `result_scope` is not specified, it defaults to `AnyScope()`, meaning the result can be accessed by any worker. |
35 | 37 |
|
36 | 38 | **Example:** |
37 | 39 | ```julia |
38 | | -g = Dagger.@spawn result_scope=ExactScope(Dagger.OSProc(3)) f(x,y) |
| 40 | +g = Dagger.@spawn result_scope=Dagger.scope(worker=3, threads=[1,3, 4]) f(x,y) |
39 | 41 | ``` |
40 | | -The result of `g` is accessible only from worker process 3. The task's execution may happen anywhere. |
| 42 | +The result of `g` is accessible only from threads 1, 3 and 4 of worker process 3. The task's execution may happen anywhere on threads 1, 3 and 4 of worker 3. |
41 | 43 |
|
42 | 44 | --- |
43 | 45 |
|
44 | | -## Interaction of compute_scope and result_scope |
| 46 | +## Interaction of `compute_scope` and `result_scope` |
45 | 47 |
|
46 | | -When `scope`, `compute_scope`, and `result_scope` are all used, the scheduler executes the task on the intersection of the effective compute scope (which will be `compute_scope` if provided, otherwise `scope`) and the `result_scope`. If intersection does not exist then Scheduler throws Exception error. |
| 48 | +When `scope`, `compute_scope`, and `result_scope` are all used, the scheduler executes the task on the intersection of the effective compute scope (which will be `compute_scope` if provided, otherwise `scope`) and the `result_scope`. If the intersection is empty then the scheduler throws a `Dagger.Sch.SchedulerException` error. |
47 | 49 |
|
48 | 50 | **Example:** |
49 | 51 | ```julia |
50 | | -g = Dagger.@spawn scope=ExactScope(Dagger.ThreadProc(3, 2)) compute_scope=Dagger.ProcessScope(2) result_scope=Dagger.UnionScope(ExactScope(Dagger.ThreadProc(2, 2)), ExactScope(Dagger.ThreadProc(4, 2))) f(x,y) |
| 52 | +g = Dagger.@spawn scope=Dagger.scope(worker=3,thread=2) compute_scope=Dagger.scope(worker=2) result_scope=Dagger.scope((worker=2, thread=2), (worker=4, thread=2)) f(x,y) |
51 | 53 | ``` |
52 | | -The task `g` computes on `Dagger.ThreadProc(2, 2)` (as it's the intersection of compute and result scopes), and its result access is also restricted to `Dagger.ThreadProc(2, 2)`. |
| 54 | +The task `g` computes on thread 2 of worker 2 (as it's the intersection of compute and result scopes), and its result access is also restricted to thread 2 of worker 2. |
53 | 55 |
|
54 | 56 | --- |
55 | 57 |
|
56 | 58 | ## Chunk Inputs to Tasks |
57 | 59 |
|
58 | | -This section explains how `scope`, `compute_scope`, and `result_scope` affect tasks when a `Chunk` is the primary input to `@spawn` (e.g., `Dagger.tochunk(...)`). |
| 60 | +This section explains how `scope`, `compute_scope`, and `result_scope` affect tasks when a `Chunk` is the primary input to `@spawn` (e.g. created via `Dagger.tochunk(...)` or by calling `fetch(task; raw=true)` on a task). |
59 | 61 |
|
60 | | -Assume `g` is some function, e.g., `g(x, y) = x * 2 + y * 3` and . `chunk_proc` is the chunk's processor, and `chunk_scope` is its defined accessibility. |
| 62 | +Assume `g` is some function, e.g. `g(x, y) = x * 2 + y * 3`, `chunk_proc` is the chunk's processor, and `chunk_scope` is its defined accessibility. |
61 | 63 |
|
62 | 64 | When `Dagger.tochunk(...)` is directly spawned: |
63 | 65 | - The task executes on `chunk_proc`. |
64 | 66 | - The result is accessible only within `chunk_scope`. |
65 | 67 | - This behavior occurs irrespective of the `scope`, `compute_scope`, and `result_scope` values provided in the `@spawn` macro. |
66 | | -- Dagger validates that there is an intersection between the effective `compute_scope` (derived from `@spawn`'s `compute_scope` or `scope`) and the `result_scope`. If no intersection exists, the Scheduler throws an exception. |
| 68 | +- Dagger validates that there is an intersection between the effective `compute_scope` (derived from `@spawn`'s `compute_scope` or `scope`) and the `result_scope`. If no intersection exists, the scheduler throws an exception. |
| 69 | + |
| 70 | +!!! info While `chunk_proc` is currently required when constructing a chunk, it is largely unused in actual scheduling logic. It exists primarily for backward compatibility and may be deprecated in the future. |
67 | 71 |
|
68 | 72 | **Usage:** |
69 | 73 | ```julia |
70 | | -h1 = Dagger.@spawn scope=ExactScope(Dagger.OSProc(3)) Dagger.tochunk(g(10, 11), chunk_proc, chunk_scope) |
71 | | -h2 = Dagger.@spawn compute_scope=Dagger.UnionScope(ExactScope(Dagger.ThreadProc(1, 2)), ExactScope(Dagger.ThreadProc(3, 1))) Dagger.tochunk(g(20, 21), chunk_proc, chunk_scope) |
72 | | -h3 = Dagger.@spawn scope=ExactScope(Dagger.ThreadProc(2, 3)) compute_scope=Dagger.UnionScope(ExactScope(Dagger.ThreadProc(1, 2)), ExactScope(Dagger.ThreadProc(3, 1))) Dagger.tochunk(g(30, 31), chunk_proc, chunk_scope) |
73 | | -h4 = Dagger.@spawn result_scope=ExactScope(Dagger.OSProc(3)) Dagger.tochunk(g(40, 41), chunk_proc, chunk_scope) |
74 | | -h5 = Dagger.@spawn scope=ExactScope(Dagger.ThreadProc(3, 2)) compute_scope=Dagger.ProcessScope(2) result_scope=Dagger.UnionScope(ExactScope(Dagger.ThreadProc(2, 2)), ExactScope(Dagger.ThreadProc(4, 2))) Dagger.tochunk(g(50, 51), chunk_proc, chunk_scope) |
| 74 | +h1 = Dagger.@spawn scope=Dagger.scope(worker=3) Dagger.tochunk(g(10, 11), chunk_proc, chunk_scope) |
| 75 | +h2 = Dagger.@spawn compute_scope=Dagger.scope((worker=1, thread=2), (worker=3, thread=1)) Dagger.tochunk(g(20, 21), chunk_proc, chunk_scope) |
| 76 | +h3 = Dagger.@spawn scope=Dagger.scope(worker=2,thread=3) compute_scope=Dagger.scope((worker=1, thread=2), (worker=3, thread=1)) Dagger.tochunk(g(30, 31), chunk_proc, chunk_scope) |
| 77 | +h4 = Dagger.@spawn result_scope=Dagger.scope(worker=3) Dagger.tochunk(g(40, 41), chunk_proc, chunk_scope) |
| 78 | +h5 = Dagger.@spawn scope=Dagger.scope(worker=3,thread=2) compute_scope=Dagger.ProcessScope(2) result_scope=Dagger.scope(worker=2,threads=[2,3]) Dagger.tochunk(g(50, 51), chunk_proc, chunk_scope) |
75 | 79 | ``` |
76 | | -In all these cases (`h1` through `h5`), the task gets executed on `chunk_proc`, and its result is accessible only within `chunk_scope`. |
| 80 | +In all these cases (`h1` through `h5`), the tasks get executed on processor `chunk_proc` of chunk, and its result is accessible only within `chunk_scope`. |
77 | 81 |
|
78 | 82 | --- |
79 | 83 |
|
80 | | -## Function with Chunked Arguments as Tasks |
| 84 | +## Function with Chunk Arguments as Tasks |
81 | 85 |
|
82 | | -This section details behavior when `scope`, `compute_scope`, and `result_scope` are used with tasks where a function is the input, and its arguments include `Chunks`. |
| 86 | +This section details behavior when `scope`, `compute_scope`, and `result_scope` are used with tasks where a function is the input, and its arguments include `Chunk`s. |
83 | 87 |
|
84 | | -Assume `g(x, y) = x * 2 + y * 3` is a function, and `arg = Dagger.tochunk(g(1, 2), arg_proc, arg_scope)` is a chunked argument, where `arg_proc` is the chunk's processor and `arg_scope` is its defined scope. |
| 88 | +Assume `g(x, y) = x * 2 + y * 3` is a function, and `arg = Dagger.tochunk(g(1, 2), arg_proc, arg_scope)` is a chunk argument, where `arg_proc` is the chunk's processor and `arg_scope` is its defined scope. |
85 | 89 |
|
86 | 90 | ### Scope |
87 | | -If `arg_scope` and `scope` do not intersect, the Scheduler throws an exception. Otherwise, `compute_scope` defaults to `scope`, and `result_scope` defaults to `AnyScope()`. Execution occurs on the intersection of `scope` and `arg_scope`. |
| 91 | +If `arg_scope` and `scope` do not intersect, the scheduler throws an exception. Execution occurs on the intersection of `scope` and `arg_scope`. |
88 | 92 |
|
89 | 93 | ```julia |
90 | | -h = Dagger.@spawn scope=ExactScope(Dagger.OSProc(3)) g(arg, 11) |
| 94 | +h = Dagger.@spawn scope=Dagger.scope(worker=3) g(arg, 11) |
91 | 95 | ``` |
92 | | -Task `h` executes on any processor within the intersection of `scope` and `arg_scope`. The result is stored and accessible from anywhere. |
| 96 | +Task `h` executes on any worker within the intersection of `scope` and `arg_scope`. The result is accessible from any worker. |
93 | 97 |
|
94 | 98 | --- |
95 | 99 |
|
96 | | -### Compute Scope |
97 | | -If `arg_scope` and `compute_scope` do not intersect, the Scheduler throws an exception. Otherwise, execution happens on the intersection of the effective compute scope (which will be `compute_scope` if provided, otherwise `scope`) and `arg_scope`. `result_scope` defaults to `AnyScope()`. |
| 100 | +### Compute scope and Chunk argument scopes interaction |
| 101 | +If `arg_scope` and `compute_scope` do not intersect, the scheduler throws an exception. Otherwise, execution happens on the intersection of the effective compute scope (which will be `compute_scope` if provided, otherwise `scope`) and `arg_scope`. `result_scope` defaults to `AnyScope()`. |
98 | 102 |
|
99 | 103 | ```julia |
100 | | -h1 = Dagger.@spawn compute_scope=Dagger.UnionScope(ExactScope(Dagger.ThreadProc(1, 2)), ExactScope(Dagger.ThreadProc(3, 1))) g(arg, 11) |
101 | | -h2 = Dagger.@spawn scope=ExactScope(Dagger.ThreadProc(2, 3)) compute_scope=Dagger.UnionScope(ExactScope(Dagger.ThreadProc(1, 2)), ExactScope(Dagger.ThreadProc(3, 1))) g(arg, 21) |
| 104 | +h1 = Dagger.@spawn compute_scope=Dagger.scope((worker=1, thread=2), (worker=3, thread=1)) g(arg, 11) |
| 105 | +h2 = Dagger.@spawn scope=Dagger.scope(worker=2,thread=3) compute_scope=Dagger.scope((worker=1, thread=2), (worker=3, thread=1)) g(arg, 21) |
102 | 106 | ``` |
103 | | -Task `h1` and `h2` execute on any processor within the intersection of the `compute_scope` and `arg_scope`. `scope` is ignored if `compute_scope` is specified. The result is stored and accessible from anywhere. |
| 107 | +Tasks `h1` and `h2` execute on any worker within the intersection of the `compute_scope` and `arg_scope`. `scope` is ignored if `compute_scope` is specified. The result is stored and accessible from anywhere. |
104 | 108 |
|
105 | 109 | --- |
106 | 110 |
|
107 | | -### Result Scope |
108 | | -If only `result_scope` is specified, computation happens on any processor within `arg_scope`, and the result is only accessible from `result_scope`. |
| 111 | +### Result scope and Chunk argument scopes interaction |
| 112 | +If only `result_scope` is specified, computation happens on any worker within `arg_scope`, and the result is only accessible from `result_scope`. |
109 | 113 |
|
110 | 114 | ```julia |
111 | | -h = Dagger.@spawn result_scope=ExactScope(Dagger.OSProc(3)) g(arg, 11) |
| 115 | +h = Dagger.@spawn result_scope=Dagger.scope(worker=3) g(arg, 11) |
112 | 116 | ``` |
113 | | -Task `h` executes on any processor within `arg_scope`. The result is accessible from `result_scope`. |
| 117 | +Task `h` executes on any worker within `arg_scope`. The result is accessible from `result_scope`. |
114 | 118 |
|
115 | 119 | --- |
116 | 120 |
|
117 | | -### Compute and Result Scope |
118 | | -When `scope`, `compute_scope`, and `result_scope` are all used, the scheduler executes the task on the intersection of `arg_scope`, the effective compute scope (which is `compute_scope` if provided, otherwise `scope`), and `result_scope`. If no intersection exists, the Scheduler throws an exception. |
| 121 | +### Compute, result, and chunk argument scopes interaction |
| 122 | +When `scope`, `compute_scope`, and `result_scope` are all used, the scheduler executes the task on the intersection of `arg_scope`, the effective compute scope (which is `compute_scope` if provided, otherwise `scope`), and `result_scope`. If no intersection exists, the scheduler throws an exception. |
119 | 123 |
|
120 | 124 | ```julia |
121 | | -h = Dagger.@spawn scope=ExactScope(Dagger.ThreadProc(3, 2)) compute_scope=Dagger.ProcessScope(2) result_scope=Dagger.UnionScope(ExactScope(Dagger.ThreadProc(2, 2)), ExactScope(Dagger.ThreadProc(4, 2))) g(arg, 31) |
| 125 | +h = Dagger.@spawn scope=Dagger.scope(worker=3,thread=2) compute_scope=Dagger.ProcessScope(2) result_scope=Dagger.scope((worker=2, thread=2), (worker=4, thread=2)) g(arg, 31) |
122 | 126 | ``` |
123 | | -Task `h` computes on `Dagger.ThreadProc(2, 2)` (as it's the intersection of `arg`, `compute`, and `result` scopes), and its result access is also restricted to `Dagger.ThreadProc(2, 2)`. |
| 127 | +Task `h` computes on thread 2 of worker 2 (as it's the intersection of `arg`, `compute`, and `result` scopes), and its result access is also restricted to thread 2 of worker 2. |
0 commit comments