feat(cccl): integrate missing CCCL directories — python/, ci/, .agent/, docs/, test/

Sparse-checkout from NVIDIA/cccl main branch to complete cccl_upstream:

Added:
- python/cuda_cccl/ (226 files) — Python bindings for device-level algorithms
  Critical for muh toolchain: cuda.compute.reduce_into, scan, radix_sort, etc.
  Includes 204 .py files with full test coverage for all 27 algorithms
- ci/ (163 files) — Build/test infrastructure
  build_cub.sh, test_cub.sh, build_and_test_targets.sh, matrix.yaml
  Directly maps to our [INFRA-CI] and [INFRA-BUILD] items
- .agent/skills/ (7 files) — NVIDIA's own agent skills for CCCL
  cccl-style/SKILL.md, cccl-test/SKILL.md, sass-diff/SKILL.md
- docs/ (491 files) — Official CCCL documentation
  CI references, CMake guides, Python compute docs, libcudacxx PTX docs
- test/ (12 files) — Top-level integration tests (cuda_smoke, stdpar)
- Root configs: .clang-format, .clang-tidy, CONTRIBUTING.md, pyproject.toml
- CLAUDE.md symlink → AGENTS.md (NVIDIA's standard)

cccl_upstream now mirrors full NVIDIA/cccl structure:
  Before: 42M (cub + thrust + libcudacxx + cudax + c + examples + benchmarks)
  After:  53M (+python +ci +docs +.agent +test +configs)

This completes the CCCL base needed for:
- [muh-bench] items: ci/util/build_and_test_targets.sh for targeted builds
- [CCCL-verify] items: python/cuda_cccl/tests/ as reference implementations
- [CCCL-test] items: ci/test_cub.sh, ci/test_thrust.sh
- Agent workflow: .agent/skills/ for consistent style and test patterns
This commit is contained in:
muh-bot
2026-08-07 02:34:33 +00:00
parent 3f97dca7ad
commit 2a7ca101d7
908 changed files with 121615 additions and 0 deletions

View File

@@ -0,0 +1,330 @@
.. _stf_custom_data_interface:
CUDASTF offers an extensible API that allows users to implement their
own data interface.
Let us for example go through the different steps to implement a data
interface for a very simple simple implementation of a matrix class.
For the sake of simplicity, we here only consider the CUDA stream
backend, but adding support for the CUDA graph backend simply require
some extra steps which use the CUDA graph API.
Implementation of the ``matrix`` class
======================================
For the sake of simplicity, we consider a very simple representation of
matrix, only defined by the dimensions m and n, and by the base address
of the matrix which we assume to be contiguous.
.. code:: c++
template <typename T>
class matrix {
public:
matrix(size_t m, size_t n, T* base) : m(m), n(n), base(base) {}
__host__ __device__ T& operator()(size_t i, size_t j) { return base[i + j * m]; }
__host__ __device__ const T& operator()(size_t i, size_t j) const { return base[i + j * m]; }
size_t m, n;
T* base;
};
Defining the shape of a matrix
==============================
The first step consists in defining what is the *shape* of a matrix. The
shape of a matrix should be a class that defines all parameters which
are the same for all data instances, ``m`` and ``n``. On the other hand,
the base address should not be part of this shape class, because each
data instance will have its own base address.
To define what is the shape of a matrix, we need to specialize the
``cudastf::shape_of`` trait class.
.. code:: c++
template <typename T>
class cudastf::shape_of<matrix<T>> {
public:
/**
* @brief The default constructor.
*
* All `shape_of` specializations must define this constructor.
*/
shape_of() = default;
explicit shape_of(size_t m, size_t n) : m(m), n(n) {}
/**
* @name Copies a shape.
*
* All `shape_of` specializations must define this constructor.
*/
shape_of(const shape_of&) = default;
/**
* @brief Extracts the shape from a matrix
*
* @param M matrix to get the shape from
*
* All `shape_of` specializations must define this constructor.
*/
shape_of(const matrix<T>& M) : shape_of<matrix<T>>(M.m, M.n) {}
/// Mandatory method : defined the total number of elements in the shape
size_t size() const { return m * n; }
size_t m;
size_t n;
};
We here see that ``shape_of<matrix<T>>`` contains two ``size_t`` fields
``m`` and ``n``.
In addition, we need to define a default constructor and a copy
constructors.
To implement the ``.shape()`` member of the ``logical_data`` class, we
need to define a constructor which takes a const reference to a matrix.
Finally, if the ``ctx.parallel_for`` construct is needed, we must define
a ``size_t size() const`` method which computes the total number of
elements in a shape.
Hash of a matrix
================
For internal needs, such as using (unordered) maps of data instances,
CUDASTF need to have specialized forms of the ``std::hash`` trait class.
The ``()`` operator of this class should compute a unique identifier
associated to the description of the data instance. This typically means
computing a hash of the matrix sizes, and of the base address. Note that
this hash *does not* depend on the actual content of the matrix.
In code snippet, we are using the ``cudastf::hash_combine`` helper which
updates a hash value with another value. This function is available from
the ``cudastf/utility/hash.h`` header.
.. code:: c++
template <typename T>
struct std::hash<matrix<T>> {
std::size_t operator()(matrix<T> const& m) const noexcept {
// Combine hashes from the base address and sizes
return cudastf::hash_all(m.m, m.n, m.base);
}
};
Defining a data interface
=========================
We can now implement the actual data interface for a matrix class, which
defines the basic operations that CUDASTF need to perform on a matrix.
The ``matrix_stream_interface`` class inherits from the
``data_interface`` class, but to implement a data interface using APIs
based on CUDA streams, ``matrix_stream_interface`` inherits from
``stream_data_interface_simple<matrix<T>>`` which contains pure virtual
functions that need to be implemented.
.. code:: c++
template <typename T>
class matrix_stream_interface : public stream_data_interface_simple<matrix<T>> {
public:
using base = stream_data_interface_simple<matrix<T>>;
using base::shape_t;
/// Initialize from an existing matrix
matrix_stream_interface(matrix<T> m) : base(std::move(m)) {}
/// Initialize from a shape of matrix
matrix_stream_interface(shape_t s) : base(s) {}
/// Copy the content of an instance to another instance
///
/// This implementation assumes that we have registered memory if one of the data place is the host
void stream_data_copy(const data_place& dst_memory_node, instance_id_t dst_instance_id,
const data_place& src_memory_node, instance_id_t src_instance_id, cudaStream_t stream) override {
assert(src_memory_node != dst_memory_node);
cudaMemcpyKind kind = cudaMemcpyDeviceToDevice;
if (src_memory_node == data_place::host) {
kind = cudaMemcpyHostToDevice;
}
if (dst_memory_node == data_place::host) {
kind = cudaMemcpyDeviceToHost;
}
const matrix<T>& src_instance = this->instance(src_instance_id);
const matrix<T>& dst_instance = this->instance(dst_instance_id);
size_t sz = src_instance.m * src_instance.n * sizeof(T);
cuda_safe_call(cudaMemcpyAsync((void*) dst_instance.base, (void*) src_instance.base, sz, kind, stream));
}
/// allocate an instance on a specific data place
///
/// setting *s to a negative value informs CUDASTF that the allocation
/// failed, and that a memory reclaiming mechanism need to be performed.
void stream_data_allocate(backend_ctx_untyped& ctx, const data_place& memory_node, instance_id_t instance_id, ssize_t& s,
void** extra_args, cudaStream_t stream) override {
matrix<T>& instance = this->instance(instance_id);
size_t sz = instance.m * instance.n * sizeof(T);
T* base_ptr;
if (memory_node == data_place::host) {
// Fallback to a synchronous method as there is no asynchronous host allocation API
cuda_safe_call(cudaStreamSynchronize(stream));
cuda_safe_call(cudaHostAlloc(&base_ptr, sz, cudaHostAllocMapped));
} else {
cuda_safe_call(cudaMallocAsync(&base_ptr, sz, stream));
}
// By filling a positive number, we notify that the allocation was successful
*s = sz;
instance.base = base_ptr;
}
/// deallocate an instance
void stream_data_deallocate(backend_ctx_untyped& ctx, const data_place& memory_node, instance_id_t instance_id, void* extra_args,
cudaStream_t stream) override {
matrix<T>& instance = this->instance(instance_id);
if (memory_node == data_place::host) {
// Fallback to a synchronous method as there is no asynchronous host deallocation API
cuda_safe_call(cudaStreamSynchronize(stream));
cuda_safe_call(cudaFreeHost(instance.base));
} else {
cuda_safe_call(cudaFreeAsync(instance.base, stream));
}
}
/// Register the host memory associated to an instance of matrix
///
/// Note that this pin_host_memory method is not mandatory, but then it is
/// the responsibility of the user to only passed memory that is already
/// registered, and the allocation method on the host must allocate
/// registered memory too. Otherwise, copy methods need to be synchronous.
bool pin_host_memory(instance_id_t instance_id) override {
matrix<T>& instance = this->instance(instance_id);
if (!instance.base) {
return false;
}
cuda_safe_call(pin_memory(instance.base, instance.m * instance.n * sizeof(T)));
return true;
}
/// Unregister memory pinned by pin_host_memory
void unpin_host_memory(instance_id_t instance_id) override {
matrix<T>& instance = this->instance(instance_id);
unpin_memory(instance.base);
}
};
``matrix_stream_interface`` must meet the following requirements so that
they can be used in the CUDA stream backend : - It must provide
constructors which take either a matrix, or a shape of matrix as
arguments. - It must implement the ``stream_data_copy``,
``stream_data_allocate`` and ``stream_data_deallocate`` virtual methods,
which respectively define how to copy an instance into another instance,
how to allocate an instance, and how to deallocate an instance. - It may
implement the ``pin_host_memory`` and ``unpin_host_memory`` virtual
methods which respectively register and unregister the memory associated
to an instance allocated on the host. These two methods are not
mandatory, but it is the responsibility of the user to either only pass
and allocate registered host buffers, or to ensure that the copy method
does not require such memory pinning. Similarly, accessing an instance
located in host memory from a device typically requires to access
registered memory.
Associating a data interface with the CUDA stream backend
=========================================================
To ensure that we can initialize a logical data from a matrix, or from
the shape of a matrix with ``stream_ctx::logical_data``, we then need to
specialize the ``cudastf::streamed_interface_of`` trait class.
The resulting class must simply define a type named ``type`` which is
the type of the data interface for the CUDA stream backend.
.. code:: c++
template <typename T>
class cudastf::streamed_interface_of<matrix<T>> {
public:
using type = matrix_stream_interface<T>;
};
Once we have defined this trait class, it is for example possible to
initialize a logical data from a matrix, or from a matrix shape :
.. code:: c++
std::vector<int> v(m * n, 0);
matrix M(m, n, &v[0]);
// Initialize from a matrix
auto lM = ctx.logical_data(M);
// Initialize from a shape
auto lM2 = ctx.logical_data(shape_of<matrix<int>>(m, n));
Example of code using the ``matrix`` data interface
===================================================
We can now use the ``matrix`` class in CUDASTF, and access it from
tasks. In this code, we first initialize a matrix on the host, we then
apply a task which will update its content on the current device. We
finally check that the content is correct, by the means of the
write-back mechanism that automatically updates the reference data
instance of a logical data when calling ``ctx.sync()``.
.. code:: c++
template <typename T>
__global__ void kernel(matrix<T> M) {
int tid_x = blockIdx.x * blockDim.x + threadIdx.x;
int nthreads_x = gridDim.x * blockDim.x;
int tid_y = blockIdx.y * blockDim.y + threadIdx.y;
int nthreads_y = gridDim.y * blockDim.y;
for (int x = tid_x; x < M.m; x += nthreads_x)
for (int y = tid_y; y < M.n; y += nthreads_y) {
M(x, y) += -x + 7 * y;
}
}
int main() {
stream_ctx ctx;
const size_t m = 8;
const size_t n = 10;
std::vector<int> v(m * n);
for (size_t j = 0; j < n; j++)
for (size_t i = 0; i < m; i++) {
v[i + j * m] = 17 * i + 23 * j;
}
matrix<int> M(m, n, &v[0]);
auto lM = ctx.logical_data(M);
// M(i,j) += -i + 7*i
ctx.task(lM.rw())->*[](cudaStream_t s, auto dM) { kernel<<<dim3(8, 8), dim3(8, 8), 0, s>>>(dM); };
ctx.sync();
for (size_t j = 0; j < n; j++)
for (size_t i = 0; i < m; i++) {
assert(v[i + j * m] == (17 * i + 23 * j) + (-i + 7*i));
}
}

View File

@@ -0,0 +1,5 @@
digraph {
"NODE_23" [style="filled" fillcolor="red" label="task fence"]
"NODE_3" -> "NODE_23"
"NODE_3" [style="filled" fillcolor="white" label="foo"]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

View File

@@ -0,0 +1,14 @@
digraph {
"NODE_23" [style="filled" fillcolor="red" label="task fence"]
subgraph cluster_section_1 {
color=black;
style=dashed
label="foo"
"NODE_13"
"NODE_3"
} // end subgraph cluster_section_1
"NODE_13" -> "NODE_23"
"NODE_3" -> "NODE_13"
"NODE_13" [style="filled" fillcolor="white" label="bar"]
"NODE_3" [style="filled" fillcolor="white" label="bar"]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -0,0 +1,36 @@
digraph {
"NODE_23" [style="filled" fillcolor="red" label="task fence"]
subgraph cluster_section_1 {
subgraph cluster_section_2 {
color=black;
style=dashed
label="bar"
"NODE_9"
"NODE_5"
"NODE_3"
} // end subgraph cluster_section_2
subgraph cluster_section_5 {
color=black;
style=dashed
label="bar"
"NODE_19"
"NODE_15"
"NODE_13"
} // end subgraph cluster_section_5
color=black;
style=dashed
label="foo"
} // end subgraph cluster_section_1
"NODE_19" -> "NODE_23"
"NODE_3" -> "NODE_5"
"NODE_5" -> "NODE_9"
"NODE_15" -> "NODE_19"
"NODE_9" -> "NODE_13"
"NODE_13" -> "NODE_15"
"NODE_19" [style="filled" fillcolor="white" label="baz"]
"NODE_15" [style="filled" fillcolor="white" label="baz"]
"NODE_13" [style="filled" fillcolor="white" label="t1\nA(read)(0) \nB(rw)(0) "]
"NODE_9" [style="filled" fillcolor="white" label="baz"]
"NODE_5" [style="filled" fillcolor="white" label="baz"]
"NODE_3" [style="filled" fillcolor="white" label="t1\nA(read)(0) \nB(rw)(0) "]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

View File

@@ -0,0 +1,69 @@
digraph {
"NODE_23" [style="filled" fillcolor="red" label="task fence"]
subgraph cluster_section_1 {
subgraph cluster_section_2 {
subgraph cluster_section_3 {
color=black;
style=dashed
label="baz"
"NODE_7"
"NODE_5"
} // end subgraph cluster_section_3
subgraph cluster_section_4 {
color=black;
style=dashed
label="baz"
"NODE_11"
"NODE_9"
} // end subgraph cluster_section_4
color=black;
style=dashed
label="bar"
"NODE_3"
} // end subgraph cluster_section_2
subgraph cluster_section_5 {
subgraph cluster_section_6 {
color=black;
style=dashed
label="baz"
"NODE_17"
"NODE_15"
} // end subgraph cluster_section_6
subgraph cluster_section_7 {
color=black;
style=dashed
label="baz"
"NODE_21"
"NODE_19"
} // end subgraph cluster_section_7
color=black;
style=dashed
label="bar"
"NODE_13"
} // end subgraph cluster_section_5
color=black;
style=dashed
label="foo"
} // end subgraph cluster_section_1
"NODE_15" -> "NODE_17"
"NODE_9" -> "NODE_11"
"NODE_11" -> "NODE_15"
"NODE_13" -> "NODE_17"
"NODE_19" -> "NODE_21"
"NODE_11" -> "NODE_13"
"NODE_7" -> "NODE_9"
"NODE_5" -> "NODE_7"
"NODE_17" -> "NODE_19"
"NODE_3" -> "NODE_7"
"NODE_21" -> "NODE_23"
"NODE_21" [style="filled" fillcolor="white" label="t3\nA(rw)(0) \nB(read)(0) \nC(read)(0) "]
"NODE_19" [style="filled" fillcolor="white" label="t2\nA(read)(0) \nC(rw)(0) "]
"NODE_17" [style="filled" fillcolor="white" label="t3\nA(rw)(0) \nB(read)(0) \nC(read)(0) "]
"NODE_15" [style="filled" fillcolor="white" label="t2\nA(read)(0) \nC(rw)(0) "]
"NODE_13" [style="filled" fillcolor="white" label="t1\nA(read)(0) \nB(rw)(0) "]
"NODE_11" [style="filled" fillcolor="white" label="t3\nA(rw)(0) \nB(read)(0) \nC(read)(0) "]
"NODE_9" [style="filled" fillcolor="white" label="t2\nA(read)(0) \nC(rw)(0) "]
"NODE_7" [style="filled" fillcolor="white" label="t3\nA(rw)(0) \nB(read)(0) \nC(read)(0) "]
"NODE_5" [style="filled" fillcolor="white" label="t2\nA(read)(0) \nC(rw)(0) "]
"NODE_3" [style="filled" fillcolor="white" label="t1\nA(read)(0) \nB(rw)(0) "]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

View File

@@ -0,0 +1,10 @@
digraph {
T_1 [label="T1\nX(rw)"];
T_2 [label="T2\nX(read)\nY(rw)"];
T_3 [label="T3\nX(read)\nZ(rw)"];
T_4 [label="T4\nY(read)\nZ(rw)"];
T_1 -> T_2;
T_1 -> T_3;
T_2 -> T_4;
T_3 -> T_4;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View File

@@ -0,0 +1,16 @@
digraph {
subgraph cluster_0 {
label="device 0";
T_1 [label="T_1(A^W)"];
T_2 [label="T_2(A^R, B^W)"];
}
subgraph cluster_1 {
label="device 1";
T_3 [label="T_3(A^R, C^W)"];
T_4 [label="T_4(B^R, C^R, D^W)"];
}
T_1 -> T_2 [label="A"];
T_1 -> T_3 [label="A"];
T_2 -> T_4 [label="B"];
T_3 -> T_4 [label ="C"];
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 171 KiB

View File

@@ -0,0 +1,26 @@
digraph {
compound=true;
subgraph cluster_0 {
label="T1";
K1 [label="K1"];
K2 [label="K2"];
}
subgraph cluster_1 {
label="T2";
K3 [label="K3"];
}
subgraph cluster_2 {
label="T3";
K4 [label="K4"];
}
subgraph cluster_3 {
label="T4";
cb [label="callback"];
}
K1 -> K2;
K2 -> K3 [ltail=cluster_0,lhead=cluster_1,minlen=2];
K2 -> K4 [ltail=cluster_0,lhead=cluster_2,minlen=2];
K3 -> cb [ltail=cluster_1,lhead=cluster_3,minlen=2];
K4 -> cb [ltail=cluster_2,lhead=cluster_3,minlen=2];
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

View File

@@ -0,0 +1,68 @@
digraph {
compound=true;
subgraph cluster_00 {
label="";
AA [label="Allocate A"];
}
subgraph cluster_01 {
label="";
CA [label="Copy A H->D"];
}
subgraph cluster_10 {
label="";
AB [label="Allocate B"];
}
subgraph cluster_11 {
label="";
CB [label="Copy B H->D"];
}
subgraph cluster_0 {
label="T1";
K1 [label="K1"];
K2 [label="K2"];
}
CA -> K1 [ltail=cluster_01,lhead=cluster_0,minlen=2];
AA -> CA [ltail=cluster_00,lhead=cluster_01,minlen=2];
CB -> K1 [ltail=cluster_11,lhead=cluster_0,minlen=2];
AB -> CB [ltail=cluster_10,lhead=cluster_11,minlen=2];
subgraph cluster_1 {
label="T2";
K3 [label="K3"];
}
subgraph cluster_2 {
label="T3";
K4 [label="K4"];
}
K1 -> K2;
K2 -> K3 [ltail=cluster_0,lhead=cluster_1,minlen=2];
K2 -> K4 [ltail=cluster_0,lhead=cluster_2,minlen=2];
subgraph cluster_02 {
label="";
CA2 [label="Copy A D->A"];
}
subgraph cluster_12 {
label="";
CB2 [label="Copy B D->A"];
}
subgraph cluster_3 {
label="T4";
cb [label="callback"];
}
K3 -> CA2 [ltail=cluster_1,lhead=cluster_02,minlen=2];
K4 -> CB2 [ltail=cluster_2,lhead=cluster_12,minlen=2];
CA2 -> cb [ltail=cluster_02,lhead=cluster_3,minlen=2]
CB2 -> cb [ltail=cluster_12,lhead=cluster_3,minlen=2]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

View File

@@ -0,0 +1,114 @@
.. _stf_lower_level_api:
Lower-level API
===============
In some situations, the use of ``operator->*()`` on the object returned
by ``ctx.task()`` (where ``ctx`` is a stream or graph context) may not
be suitable, for example when the number of parameters is not known
statically. To address such situations, CUDASTF provides a lower-level
interface for creating tasks, which is described below.
.. code:: cpp
#include "cudastf/stf.h"
#include "cudastf/__stf/stream/stream_ctx.h"
using namespace cudastf;
template <typename T>
__global__ void axpy(int n, T a, T* x, T* y) {
int tid = blockIdx.x * blockDim.x + threadIdx.x;
int nthreads = gridDim.x * blockDim.x;
for (int ind = tid; ind < n; ind += nthreads) {
y[ind] += a * x[ind];
}
}
int main(int argc, char** argv) {
stream_ctx ctx;
const size_t N = 16;
double X[N], Y[N];
for (size_t ind = 0; ind < N; ind++) {
X[ind] = sin(double(ind));
Y[ind] = cos(double(ind));
}
auto lX = ctx.logical_data(X);
auto lY = ctx.logical_data(Y);
double alpha = 3.14;
/* Compute Y = Y + alpha X */
auto t = ctx.task(lX.read(), lY.rw());
t.start();
slice<double> sX = t.get<0>();
slice<double> sY = t.get<1>();
axpy<<<16, 128, 0, t.get_stream()>>>(sX.size(), alpha, sX.data_handle(), sY.data_handle());
t.end();
ctx.sync();
}
The ``ctx.task()`` call returns a task object. This object provides
access to the local description of the data associated with the task and
a CUDA stream that can be used to submit work asynchronously. The
beginning of the task body and its end are delimited by the ``.start()``
and ``.end()`` calls. Failing to call either of these methods or calling
them more than once or in the wrong order results in undefined behavior.
Asynchrony is achieved by using the CUDA stream, which provides a
mechanism to submit work on the execution place (here, implicitly the
current CUDA device). CUDA ensures that all kernels synchronized with
this CUDA stream will only be executed once all prerequisites have been
fulfilled (e.g., preceding tasks, data transfers, etc.). In addition,
CUDASTF performs all the necessary synchronization so that future tasks
will be properly synchronized with the operations enqueued in the CUDA
stream associated with this task after calling ``.start()`` and before
calling ``.end()``.
Compatibility with CUDA graphs
==============================
Similarly to the CUDA stream backend with a context of type
``stream_ctx``, the CUDA graph backend ``graph_ctx`` also provides a
low-level interface.
.. code:: cpp
graph_ctx ctx;
double X[1024], Y[1024];
auto lX = ctx.logical_data(X);
auto lY = ctx.logical_data(Y);
for (int k = 0; k < 10; k++) {
graph_task t = ctx.task();
t.add_deps(handle_X.rw());
t.start();
cudaGraphNode_t n;
cuda_safe_call(cudaGraphAddEmptyNode(&n, t.get_graph(), nullptr, 0));
t.end();
}
graph_task t2 = ctx.task();
t2.add_deps(lX.read(), lY.rw());
t2.start();
cudaGraphNode_t n2;
cuda_safe_call(cudaGraphAddEmptyNode(&n2, t2.get_graph(), nullptr, 0));
t2.end();
ctx.sync();
A task in the CUDA graph backend corresponds to a *child graph*
automatically inserted into the CUDA graph associated to a ``graph_ctx``
context. The example above creates 10 tasks that modify logical data
``lX``, followed by a task that reads ``lX`` and modifies ``lY``. The
code illustrates how one can add dependencies to a task by using the
``add_deps`` method.
Similarly to the CUDA stream backend, a task is outlined by a pair of
calls to the ``start()``/``end()`` member functions.