GPUTaskingcudaFlowCapturer

# GPU Tasking (cudaFlowCapturer)

Include the Header
GPUTaskingcudaFlowCapturer_1GPUTaskingcudaFlowCapturerIncludeTheHeader
Capture a cudaFlow
GPUTaskingcudaFlowCapturer_1Capture_a_cudaFlow
Common Capture Methods
GPUTaskingcudaFlowCapturer_1CommonCaptureMethods
Create a Capturer on a Specific GPU
GPUTaskingcudaFlowCapturer_1CreateACapturerOnASpecificGPU
Create a Capturer from a cudaFlow
GPUTaskingcudaFlowCapturer_1CreateACapturerWithinAcudaFlow
Offload a cudaFlow Capturer
GPUTaskingcudaFlowCapturer_1OffloadAcudaFlowCapturer
Update a cudaFlow Capturer
GPUTaskingcudaFlowCapturer_1UpdateAcudaFlowCapturer
Integrate a cudaFlow Capturer into Taskflow
GPUTaskingcudaFlowCapturer_1IntegrateCudaFlowCapturerIntoTaskflow

You can create a cudaFlow through stream capture, which allows you to implicitly capture a CUDA graph using stream-based interface. Compared to explicit CUDA Graph construction (tf::cudaFlow), implicit CUDA Graph capturing (tf::cudaFlowCapturer) is more flexible in building GPU task graphs.

# Include the Header

You need to include the header file, `taskflow/cuda/cudaflow.hpp`, for capturing a GPU task graph using tf::cudaFlowCapturer.

    #include<taskflow/cuda/cudaflow.hpp>

# Capture a cudaFlow

When your program has no access to direct kernel calls but can only invoke them through a stream-based interface (e.g., cuBLAS and cuDNN library functions), you can use tf::cudaFlowCapturer to capture the hidden GPU operations into a CUDA graph. A cudaFlowCapturer is similar to a cudaFlow except it constructs a GPU task graph through stream capture. You use the method tf::cudaFlowCapturer::on to capture a sequence of asynchronous GPU operations through the given stream. The following example creates a CUDA graph that captures two kernel tasks, `task_1` (`my_kernel_1`) and `task_2` (`my_kernel_2`) , where `task_1` runs before `task_2`.

    //createacudaFlowcapturertorunaCUDAgraphusingstreamcapturing
    tf::cudaFlowCapturercapturer;

    //capturemy_kernel_1throughastreammanagedbycapturer
    tf::cudaTasktask_1=capturer.on([&](cudaStream_tstream){
    my_kernel_1<<<grid_1,block_1,shm_size_1,stream>>>(my_parameters_1);
    }).name("my_kernel_1");

    //capturemy_kernel_2throughastreammanagedbycapturer
    tf::cudaTasktask_2=capturer.on([&](cudaStream_tstream){
    my_kernel_2<<<grid_2,block_2,shm_size_2,stream>>>(my_parameters_2);
    }).name("my_kernel_2");

    //my_kernel_1runsbeforemy_kernel_2
    task_1.precede(task_2);

    //offloadcapturedGPUtasksusingtheCUDAGraphexecutionmodel
    tf::cudaStreamstream;
    capturer.run(stream);
    stream.synchronize();

    //dumpthecudaFlowtoaDOTformatthroughstd::cout
    capturer.dump(std::cout)

Inside tf::cudaFlowCapturer::on, you should NOT modify the properties of the stream argument but only use it to capture asynchronous GPU operations (e.g., `kernel`, `cudaMemcpyAsync`). The stream argument is internal to the capturer use only.

# Common Capture Methods

tf::cudaFlowCapturer defines a set of methods for capturing common GPU operations, such as tf::cudaFlowCapturer::kernel, tf::cudaFlowCapturer::memcpy, tf::cudaFlowCapturer::memset, and so on. For example, the following code snippet uses these pre-defined methods to construct a GPU task graph of one host-to-device copy, kernel, and one device-to-host copy, in this order of their dependencies.

    tf::cudaFlowCapturercapturer;

    //copydatafromhost_datatogpu_data
    tf::cudaTaskh2d=capturer.memcpy(gpu_data,host_data,bytes)
    .name("h2d");

    //capturemy_kerneltodocomputationongpu_data
    tf::cudaTaskkernel=capturer.kernel(grid,block,shm_size,kernel,kernel_args);
    .name("my_kernel");

    //copydatafromgpu_datatohost_data
    tf::cudaTaskd2h=capturer.memcpy(host_data,gpu_data,bytes)
    .name("d2h");

    //buildtaskdependencies
    h2d.precede(kernel);
    kernel.precede(d2h);

# Create a Capturer on a Specific GPU

You can run a cudaFlow capturer on a specific GPU by switching to the context of that GPU using tf::cudaScopedDevice, following the CUDA convention of multi-GPU programming. The example below creates a cudaFlow capturer and runs it on GPU `2`:

    {
    //createanRAII-styledswitchertothecontextofGPU2
    tf::cudaScopedDevicecontext(2);

    //createacudaFlowcapturerunderGPU2
    tf::cudaFlowCapturercapturer;
    //...

    //createastreamunderGPU2andoffloadthecapturertothatGPU
    tf::cudaStreamstream;
    capturer.run(stream);
    stream.synchronize();
    }

tf::cudaScopedDevice is an RAII-styled wrapper to perform scoped switch to the given GPU context. When the scope is destroyed, it switches back to the original context.

By default, a cudaFlow capturer runs on the current GPU associated with the caller, which is typically `0`.

# Create a Capturer from a cudaFlow

Within a parent cudaFlow, you can capture a cudaFlow to form a subflow that eventually becomes a child node in the underlying CUDA task graph. The following example defines a captured flow `task2` of two dependent tasks, `task2_1` and `task2_2`, and `task2` runs after `task1`.

    tf::cudaFlowcudaflow;

    tf::cudaTasktask1=cudaflow.kernel(grid,block,shm,my_kernel,args...)
    .name("kernel");

    //task2formsasubflowasachildnodeintheunderlyingCUDAgraph
    tf::cudaTasktask2=cudaflow.capture([&](tf::cudaFlowCapturer&capturer){

    //capturekernel_1usingthegivenstream
    tf::cudaTasktask2_1=capturer.on([&](cudaStream_tstream){
    kernel_2<<<grid1,block1,shm_size1,stream>>>(args1...);
    }).name("kernel_1");

    //capturekernel_2usingthegivenstream
    tf::cudaTasktask2_2=capturer.on([&](cudaStream_tstream){
    kernel_2<<<grid2,block2,shm_size2,stream>>>(args2...);
    }).name("kernel_2");

    //kernel_1runsbeforekernel_2
    task2_1.precede(task2_2);
    }).name("capturer");

    task1.precede(task2);

# Offload a cudaFlow Capturer

When you offload a cudaFlow capturer using tf::cudaFlowCapturer::run, the runtime transforms that capturer (i.e., application GPU task graph) into a native CUDA graph and an executable instance both optimized for maximum kernel concurrency. Depending on the optimization algorithm, the application GPU task graph may be different from the actual executable graph submitted to the CUDA runtime.

    tf::cudaStreamstream;
    //launchacudaflowcapturerasynchronouslythroughastream
    capturer.run(stream);
    //waitforthecudaflowtofinish
    stream.synchronize();

# Update a cudaFlow Capturer

Between successive offloads (i.e., executions of a cudaFlow capturer), you can update the captured task with a different set of parameters. Every task-creation method in tf::cudaFlowCapturer has an overload to update the parameters of a created task by that method. The following example creates a kernel task and updates its parameter between successive runs:

    tf::cudaStreamstream;
    tf::cudaFlowCapturercf;

    //createakerneltask
    tf::cudaTasktask=cf.kernel(grid1,block1,shm1,kernel,kernel_args_1);
    cf.run(stream);
    stream.synchronize();

    //updatethecreatedkerneltaskwithdifferentparameters
    cf.kernel(task,grid2,block2,shm2,kernel,kernel_args_2);
    cf.run(stream);
    stream.synchronize();

When you run a updated cudaFlow capturer, Taskflow will try to update the underlying executable with the newly captured graph first. If that update is unsuccessful, Taskflow will destroy the executable graph and re-instantiate a new one from the newly captured graph.

# Integrate a cudaFlow Capturer into Taskflow

You can create a task to enclose a cudaFlow capturer and run it from a worker thread. The usage of the capturer remains the same except that the capturer is run by a worker thread from a taskflow task. The following example runs a cudaFlow capturer from a static task:

    tf::Executorexecutor;
    tf::Taskflowtaskflow;

    taskflow.emplace([](){
    //createacudaFlowcapturerinsideastatictask
    tf::cudaFlowCapturercapturer;

    //...captureaGPUtaskgraph
    capturer.kernel(...);

    //runthecapturerthroughastream
    tf::cudaStreamstream;
    capturer.run(stream);
    stream.synchronize();
    });