• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Function Flow Runtime Task Graph (C)
2
3## Overview
4
5The FFRT task graph supports task dependency and data dependency. Each node in the task graph indicates a task, and each edge indicates the dependency between tasks. Task dependency is classified into input dependency (`in_deps`) and output dependency (`out_deps`).
6
7You can use either of the following ways to build a task graph:
8
9- Use the task dependency to build a task graph. The task `handle` is used to indicate a task object.
10- Use the data dependency to build a task graph. The data object is abstracted as a data signature, and each data signature uniquely indicates a data object.
11
12### Task dependency
13
14> **NOTE**
15>
16> When a task handle appears in `in_deps`, the corresponding task is the previous task. When a task handle appears in `out_deps`, the corresponding task is the subsequent task.
17
18Task dependency applies to scenarios where tasks have specific sequence or logical process requirements. For example:
19
20- Tasks with sequence. For example, a data preprocessing task is executed before a model training task.
21- Logic process control. For example, in a typical commodity transaction process, orders are placed, followed by production and then logistics transportation.
22- Multi-level chain: For example, during video processing, you can perform tasks such as transcoding, generating thumbnails, adding watermarks, and releasing the final video.
23
24### Data Dependency
25
26> **NOTE**
27>
28> When the signature of a data object appears in `in_deps` of a task, the task is referred to as a consumer task that executes without modifying the original input data object.
29> When the signature of a data object appears in `out_deps` of a task, the task is referred to as a producer task that updates the output data object's content to create a new version.
30
31Data dependency applies to scenarios where tasks are triggered by data production and consumption relationships.
32
33A data object may have multiple versions. Each version corresponds to one producer task and zero, one, or more consumer tasks. A sequence of the data object versions and the version-specific producer task and consumer tasks are defined according to the delivery sequence of the producer task and consumer tasks.
34
35When all producer tasks and consumer tasks of the data object of all the available versions are executed, the data dependency is removed. In this case, the task enters the ready state and can be scheduled for execution.
36
37FFRT can dynamically build producer/consumer-based data dependencies between tasks at runtime and perform scheduling based on the task data dependency status, including:
38
39- Producer-Consumer dependency
40
41  A dependency formed between the producer task of a data object of a specific version and a consumer task of the data object of the same version. It is also referred to as a read-after-write dependency.
42
43- Consumer-Producer dependency
44
45  A dependency formed between a consumer task of a data object of a specific version and the producer task of the data object of the next version. It is also referred to as a write-after-read dependency.
46
47- Producer-Producer dependency
48
49  A dependency formed between the producer task of a data object of a specific version and a producer task of the data object of the next version. It is also referred to as a write-after-write dependency.
50
51For example, the relationship between a group of tasks and data A is expressed as follows:
52
53```cpp
54task1(OUT A);
55task2(IN A);
56task3(IN A);
57task4(OUT A);
58task5(OUT A);
59```
60
61![image](figures/ffrt_figure3.png)
62
63For ease of description, circles are used to represent tasks and squares are used to represent data.
64
65The following conclusions can be drawn:
66
67- task1 and task2/task3 form a producer-consumer dependency. This means that task2/task3 can read data A only after task1 writes data A.
68- task2/task3 and task4 form a consumer-producer dependency. This means that task4 can write data A only after task2/task3 reads data A.
69- task 4 and task 5 form a producer-producer dependency. This means that task 5 can write data A only after task 4 writes data A.
70
71## Example: Streaming Video Processing
72
73A user uploads a video to the platform. The processing steps include: parsing, transcoding, generating a thumbnail, adding a watermark, and releasing the video. Transcoding and thumbnail generation can occur simultaneously. The following figure shows the task process.
74
75![image](figures/ffrt_figure1.png)
76
77The FFRT provides task graph that can describe the task dependency and parallelize the preceding video processing process. The code is as follows:
78
79```c
80#include <stdio.h>
81#include "ffrt/task.h"
82
83static inline void ffrt_submit_c(ffrt_function_t func, const ffrt_function_t after_func,
84    void* arg, const ffrt_deps_t* in_deps, const ffrt_deps_t* out_deps, const ffrt_task_attr_t* attr)
85{
86    ffrt_submit_base(ffrt_create_function_wrapper(func, after_func, arg), in_deps, out_deps, attr);
87}
88
89static inline ffrt_task_handle_t ffrt_submit_h_c(ffrt_function_t func, const ffrt_function_t after_func,
90    void* arg, const ffrt_deps_t* in_deps, const ffrt_deps_t* out_deps, const ffrt_task_attr_t* attr)
91{
92    return ffrt_submit_h_base(ffrt_create_function_wrapper(func, after_func, arg), in_deps, out_deps, attr);
93}
94
95void func_TaskA(void* arg)
96{
97    printf("Parse\n");
98}
99
100void func_TaskB(void* arg)
101{
102    printf("Transcode\n");
103}
104
105void func_TaskC(void* arg)
106{
107    printf("Generate a thumbnail\n");
108}
109
110void func_TaskD(void* arg)
111{
112    printf("Add watermark\n");
113}
114
115void func_TaskE(void* arg)
116{
117    printf("Release\n");
118}
119
120int main()
121{
122    // Submit task A.
123    ffrt_task_handle_t hTaskA = ffrt_submit_h_c(func_TaskA, NULL, NULL, NULL, NULL, NULL);
124
125    // Submit tasks B and C.
126    ffrt_dependence_t taskA_deps[] = {{ffrt_dependence_task, hTaskA}};
127    ffrt_deps_t dTaskA = {1, taskA_deps};
128    ffrt_task_handle_t hTaskB = ffrt_submit_h_c(func_TaskB, NULL, NULL, &dTaskA, NULL, NULL);
129    ffrt_task_handle_t hTaskC = ffrt_submit_h_c(func_TaskC, NULL, NULL, &dTaskA, NULL, NULL);
130
131    // Submit task D.
132    ffrt_dependence_t taskBC_deps[] = {{ffrt_dependence_task, hTaskB}, {ffrt_dependence_task, hTaskC}};
133    ffrt_deps_t dTaskBC = {2, taskBC_deps};
134    ffrt_task_handle_t hTaskD = ffrt_submit_h_c(func_TaskD, NULL, NULL, &dTaskBC, NULL, NULL);
135
136    // Submit task E.
137    ffrt_dependence_t taskD_deps[] = {{ffrt_dependence_task, hTaskD}};
138    ffrt_deps_t dTaskD = {1, taskD_deps};
139    ffrt_submit_c(func_TaskE, NULL, NULL, &dTaskD, NULL, NULL);
140
141    // Wait until all tasks are complete.
142    ffrt_wait();
143    return 0;
144}
145```
146
147C-style FFRT construction requires additional encapsulation using common code and is irrelevant to specific service scenarios.
148
149```c
150typedef struct {
151    ffrt_function_header_t header;
152    ffrt_function_t func;
153    ffrt_function_t after_func;
154    void* arg;
155} c_function_t;
156
157static inline void ffrt_exec_function_wrapper(void* t)
158{
159    c_function_t* f = (c_function_t *)t;
160    if (f->func) {
161        f->func(f->arg);
162    }
163}
164
165static inline void ffrt_destroy_function_wrapper(void* t)
166{
167    c_function_t* f = (c_function_t *)t;
168    if (f->after_func) {
169        f->after_func(f->arg);
170    }
171}
172
173#define FFRT_STATIC_ASSERT(cond, msg) int x(int static_assertion_##msg[(cond) ? 1 : -1])
174static inline ffrt_function_header_t *ffrt_create_function_wrapper(const ffrt_function_t func,
175    const ffrt_function_t after_func, void *arg)
176{
177    FFRT_STATIC_ASSERT(sizeof(c_function_t) <= ffrt_auto_managed_function_storage_size,
178        size_of_function_must_be_less_than_ffrt_auto_managed_function_storage_size);
179
180    c_function_t* f = (c_function_t *)ffrt_alloc_auto_managed_function_storage_base(ffrt_function_kind_general);
181    f->header.exec = ffrt_exec_function_wrapper;
182    f->header.destroy = ffrt_destroy_function_wrapper;
183    f->func = func;
184    f->after_func = after_func;
185    f->arg = arg;
186    return (ffrt_function_header_t *)f;
187}
188```
189
190The expected output may be as follows:
191
192```plain
193Video parsing
194Video transcoding
195Thumbnails generation
196Watermark adding
197Video release
198```
199
200## Example: Fibonacci Sequence
201
202Each number in the Fibonacci sequence is the sum of the first two numbers. The process of calculating the Fibonacci number can well express the task dependency through the data object. The code for calculating the Fibonacci number using the FFRT framework is as follows:
203
204```c
205#include <stdio.h>
206#include "ffrt/task.h"
207
208typedef struct {
209    int x;
210    int* y;
211} fib_ffrt_s;
212
213static inline void ffrt_submit_c(ffrt_function_t func, const ffrt_function_t after_func,
214    void* arg, const ffrt_deps_t* in_deps, const ffrt_deps_t* out_deps, const ffrt_task_attr_t* attr)
215{
216    ffrt_submit_base(ffrt_create_function_wrapper(func, after_func, arg), in_deps, out_deps, attr);
217}
218
219void fib_ffrt(void* arg)
220{
221    fib_ffrt_s* p = (fib_ffrt_s*)arg;
222    int x = p->x;
223    int* y = p->y;
224
225    if (x <= 1) {
226        *y = x;
227    } else {
228        int y1, y2;
229        fib_ffrt_s s1 = {x - 1, &y1};
230        fib_ffrt_s s2 = {x - 2, &y2};
231
232        // Build data dependencies.
233        ffrt_dependence_t dx_deps[] = {{ffrt_dependence_data, &x}};
234        ffrt_deps_t dx = {1, dx_deps};
235        ffrt_dependence_t dy1_deps[] = {{ffrt_dependence_data, &y1}};
236        ffrt_deps_t dy1 = {1, dy1_deps};
237        ffrt_dependence_t dy2_deps[] = {{ffrt_dependence_data, &y2}};
238        ffrt_deps_t dy2 = {1, dy2_deps};
239        ffrt_dependence_t dy12_deps[] = {{ffrt_dependence_data, &y1}, {ffrt_dependence_data, &y2}};
240        ffrt_deps_t dy12 = {2, dy12_deps};
241
242        // Submit tasks separately.
243        ffrt_submit_c(fib_ffrt, NULL, &s1, &dx, &dy1, NULL);
244        ffrt_submit_c(fib_ffrt, NULL, &s2, &dx, &dy2, NULL);
245
246        // Wait until the task is complete.
247        ffrt_wait_deps(&dy12);
248        *y = y1 + y2;
249    }
250}
251
252int main()
253{
254    int r;
255    fib_ffrt_s s = {5, &r};
256    ffrt_dependence_t dr_deps[] = {{ffrt_dependence_data, &r}};
257    ffrt_deps_t dr = {1, dr_deps};
258    ffrt_submit_c(fib_ffrt, NULL, &s, NULL, &dr, NULL);
259
260    // Wait until the task is complete.
261    ffrt_wait_deps(&dr);
262    printf("Fibonacci(5) is %d\n", r);
263    return 0;
264}
265```
266
267Expected output:
268
269```plain
270Fibonacci(5) is 5
271```
272
273In the example, `fibonacci(x-1)` and `fibonacci(x-2)` are submitted to FFRT as two tasks. After the two tasks are complete, the results are accumulated. Although a single task is split into two subtasks, the subtasks can be further split. Therefore, the concurrency of the entire computational graph is very high.
274
275Each task forms a call tree in the FFRT.
276
277![image](figures/ffrt_figure2.png)
278
279## Available APIs
280
281The main FFRT APIs involved in the preceding example are as follows:
282
283| Name                                                            | Description                                  |
284| ---------------------------------------------------------------- | -------------------------------------- |
285| [ffrt_submit_base](ffrt-api-guideline-c.md#ffrt_submit_base)     | Submits a task.                    |
286| [ffrt_submit_h_base](ffrt-api-guideline-c.md#ffrt_submit_h_base) | Submits a task, and obtains the task handle.      |
287| [ffrt_wait_deps](ffrt-api-guideline-c.md#ffrt_wait_deps)         | Waits until the dependent tasks are complete.|
288
289## Constraints
290
291- For `ffrt_submit_base`, the total number of input dependencies and output dependencies of each task cannot exceed 8.
292- For `ffrt_submit_h_base`, the total number of input dependencies and output dependencies of each task cannot exceed 7.
293- When a parameter is used as both an input dependency and an output dependency, it is counted as one dependency. For example, if the input dependency is `{&x}` and the output dependency is also `{&x}`, then the number of dependencies is 1.
294