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,185 @@
# Doxyfile for CUDAX
PROJECT_NAME = CUDAX
OUTPUT_DIRECTORY = ../_build/doxygen/cudax
CREATE_SUBDIRS = NO
GENERATE_HTML = NO
GENERATE_LATEX = NO
GENERATE_XML = YES
XML_OUTPUT = xml
XML_PROGRAMLISTING = YES
# Input directory - specific paths for cudax
INPUT = ../../cudax/include/cuda/experimental/__copy_bytes \
../../cudax/include/cuda/experimental/__fill_bytes \
../../cudax/include/cuda/experimental/__container \
../../cudax/include/cuda/experimental/__device \
../../cudax/include/cuda/experimental/graph.cuh \
../../cudax/include/cuda/experimental/__graph \
../../cudax/include/cuda/experimental/__group \
../../cudax/include/cuda/experimental/__launch \
../../cudax/include/cuda/experimental/__places \
../../cudax/include/cuda/experimental/__places/exec \
../../cudax/include/cuda/experimental/__stream \
../../cudax/include/cuda/experimental/stf.cuh \
../../cudax/include/cuda/experimental/__stf \
../../cudax/include/cuda/experimental/__stf/internal \
../../cudax/include/cuda/experimental/__stf/utility \
../../cudax/include/cuda/experimental/__stf/localization \
../../cudax/include/cuda/experimental/__stf/allocators \
../../cudax/include/cuda/experimental/__stf/graph \
../../cudax/include/cuda/experimental/__stf/graph/internal \
../../cudax/include/cuda/experimental/__stf/graph/interfaces \
../../cudax/include/cuda/experimental/__stf/places \
../../cudax/include/cuda/experimental/__stf/places/exec/host \
../../cudax/include/cuda/experimental/__stf/stream \
../../cudax/include/cuda/experimental/__stf/stream/internal \
../../cudax/include/cuda/experimental/__stf/stream/interfaces
RECURSIVE = YES
# Exclude patterns from repo.toml
EXCLUDE = ../../cudax/include/cuda/experimental/__graph/fwd.cuh
EXCLUDE_PATTERNS = */detail/* */test/* */tests/* */examples/* */__detail/*
# Exclude symbols from repo.toml
EXCLUDE_SYMBOLS = *detail* *RESERVED* *reserved* *__* _A* _B* _C* _D* _E* _F* _G* _H* _I* _J* _K* _L* _M* _N* _O* _P* _Q* _R* _S* _T* _U* _V* _W* _X* _Y* _Z* UNITTEST
# Path for @snippet references to test/example files
EXAMPLE_PATH = ../../cudax/include/cuda/experimental/__stf/utility \
../../cudax/test/multi_gpu/algorithms \
../../cudax/test/coop/reduce
FILE_PATTERNS = *.h *.hpp *.cuh
EXTENSION_MAPPING = cuh=C++ cu=C++
# Documentation extraction settings
EXTRACT_ALL = YES
EXTRACT_PRIVATE = NO
EXTRACT_STATIC = YES
EXTRACT_LOCAL_CLASSES = YES
HIDE_UNDOC_MEMBERS = NO
HIDE_UNDOC_CLASSES = NO
SHOW_INCLUDE_FILES = YES
INLINE_INHERITED_MEMB = YES
FULL_PATH_NAMES = YES
STRIP_FROM_PATH = ../../cudax
SHORT_NAMES = NO
# Parsing settings
JAVADOC_AUTOBRIEF = YES
QT_AUTOBRIEF = NO
MULTILINE_CPP_IS_BRIEF = NO
INHERIT_DOCS = YES
SEPARATE_MEMBER_PAGES = NO
TAB_SIZE = 4
BUILTIN_STL_SUPPORT = YES
# Preprocessing
ENABLE_PREPROCESSING = YES
MACRO_EXPANSION = YES
EXPAND_ONLY_PREDEF = NO
SEARCH_INCLUDES = YES
SKIP_FUNCTION_MACROS = YES
# Predefined macros for CUDAX
PREDEFINED = \
"__device__=" \
"__host__=" \
"__global__=" \
"__forceinline__=" \
"__declspec(x)=" \
"__align__(x)=" \
"__cccl_lib_mdspan" \
"_CCCL_AND=&&" \
"_CCCL_CONCEPT=constexpr bool" \
"_CCCL_DOXYGEN_INVOKED" \
"_CCCL_HOST_DEVICE=" \
"_CCCL_DEVICE=" \
"_CCCL_HOST=" \
"_CCCL_FORCEINLINE=" \
"_CCCL_CONSTEXPR_CXX20=constexpr" \
"_CCCL_CONSTEXPR_CXX23=constexpr" \
"_CCCL_API=inline" \
"_CCCL_HOST_DEVICE_API=inline" \
"_CCCL_HOST_API=inline" \
"_CCCL_DEVICE_API=inline" \
"_CCCL_NODEBUG_API=inline" \
"_CCCL_NODEBUG_DEVICE_API=inline" \
"_CCCL_NODEBUG_HOST_API=inline" \
"_CCCL_TRIVIAL_API=inline" \
"_CCCL_TRIVIAL_HOST_API=inline" \
"_CCCL_TRIVIAL_DEVICE_API=inline" \
"_CCCL_PUBLIC_API=inline" \
"_CCCL_PUBLIC_DEVICE_API=inline" \
"_CCCL_PUBLIC_HOST_API=inline" \
"_CCCL_BEGIN_NAMESPACE_CUDA_STD=namespace cuda::std {" \
"_CCCL_END_NAMESPACE_CUDA_STD=}" \
"_CCCL_CATCH=catch" \
"_CCCL_CATCH_ALL=catch (...)" \
"_CCCL_CATCH_FALLTHROUGH=" \
"_CCCL_CONSTEXPR_FRIEND=friend " \
"_CCCL_CTK_AT_LEAST(x, y)=1" \
"_CCCL_CTK_BELOW(x, y)=0" \
"_CCCL_CUDACC_AT_LEAST(x, y)=1" \
"_CCCL_CUDACC_BELOW(x, y)=0" \
"_CCCL_DIAG_POP=" \
"_CCCL_DIAG_PUSH=" \
"_CCCL_DIAG_SUPPRESS_CLANG(x)=" \
"_CCCL_DIAG_SUPPRESS_GCC(x)=" \
"_CCCL_DIAG_SUPPRESS_MSVC(x)=" \
"_CCCL_DIAG_SUPPRESS_NVHPC(x)=" \
"_CCCL_EXEC_CHECK_DISABLE=" \
"_CCCL_GLOBAL_CONSTANT=inline constexpr" \
"_CCCL_HAS_CTK()=1" \
"_CCCL_HIDE_FROM_ABI=" \
"_CCCL_REQUIRES(x)= ::cuda::std::enable_if_t<x, int> = 0>" \
"_CCCL_STD_VER=2020" \
"_CCCL_SUPPRESS_DEPRECATED_POP=" \
"_CCCL_SUPPRESS_DEPRECATED_PUSH=" \
"_CCCL_TEMPLATE(x)=template<x, " \
"_CCCL_TRAILING_REQUIRES(x)=-> x requires " \
"_CCCL_TRY=try" \
"_CCCL_TYPE_VISIBILITY_DEFAULT=" \
"_CCCL_TYPE_VISIBILITY_HIDDEN=" \
"_CCCL_VISIBILITY_DEFAULT=" \
"_CCCL_VISIBILITY_HIDDEN=" \
"_CCCL_LIFETIMEBOUND=" \
"_CUDAX_CONSTEXPR_FRIEND=friend" \
"_LIBCUDACXX_HAS_SPACESHIP_OPERATOR()=1" \
"CCCL_DEPRECATED=" \
"CCCL_DEPRECATED_BECAUSE(x)=" \
"CCCL_IGNORE_DEPRECATED_CPP_DIALECT" \
"CUB_DISABLE_NAMESPACE_MAGIC" \
"CUB_IGNORE_NAMESPACE_MAGIC_ERROR" \
"CUB_NAMESPACE_BEGIN=namespace cub {" \
"CUB_NAMESPACE_END=}" \
"CUB_RUNTIME_FUNCTION=" \
"THRUST_FWD(x)=x" \
"THRUST_NAMESPACE_BEGIN=namespace thrust {" \
"THRUST_NAMESPACE_END=}" \
"THRUST_PREVENT_MACRO_SUBSTITUTION" \
"_CCCL_HOSTED()=1"
# Additional settings from repo.toml
DISTRIBUTE_GROUP_DOC = YES
DOXYFILE_ENCODING = UTF-8
INPUT_ENCODING = UTF-8
EXAMPLE_RECURSIVE = YES
EXAMPLE_PATTERNS = *.cu
AUTOLINK_SUPPORT = YES
# IMPORTANT: Aliases for custom commands
# The rst alias enables embedding reStructuredText in doxygen comments
# Using the same format as repo_docs for compatibility
ALIASES = "rst=\verbatim embed:rst:leading-asterisk"
ALIASES += "endrst=\endverbatim"
ALIASES += "rststar=\verbatim embed:rst:leading-asterisk"
ALIASES += "inlinerst=\verbatim embed:rst:inline"
# Quiet mode
QUIET = YES
WARNINGS = YES
WARN_AS_ERROR = FAIL_ON_WARNINGS
WARN_IF_UNDOCUMENTED = NO
WARN_IF_DOC_ERROR = YES
WARN_IF_INCOMPLETE_DOC = NO
WARN_NO_PARAMDOC = NO

View File

@@ -0,0 +1,27 @@
.. _cudax-containers:
Containers library
===================
.. toctree::
:glob:
:maxdepth: 1
api/class*uninitialized__buffer*
The headers of the container library provide facilities to store elements on the heap. They are heavily inspired by the
C++ `containers library <https://en.cppreference.com/w/cpp/container>`__ but deviate from the standard provided ones due to different requirements from
heterogeneous systems.
They build upon :ref:`memory_resources <libcudacxx-extended-api-memory-resources>` to ensure that e.g. execution space
annotations are checked by the type system.
.. _libcudacxx-containers-uninitialized-async-buffer:
.. list-table::
:widths: 25 45 30
:header-rows: 0
* - :ref:`<cuda/experimental/container.cuh> <cudax-containers-uninitialized-buffer>`
- Facilities providing uninitialized *heterogeneous* potentially stream ordered storage satisfying a set of properties
- cudax 2.7.0 / CCCL 2.7.0

View File

@@ -0,0 +1,52 @@
.. _cudax-graph:
Graphs library
==============
.. toctree::
:glob:
:maxdepth: 1
api/struct*graph*
The headers of the graph library provide facilities to create and manage CUDA graphs.
This library is under construction and not yet ready for production use.
.. list-table::
:widths: 25 45 30
:header-rows: 0
* - :ref:`<cuda/experimental/graph.cuh> <cudax-graph-graph-builder>`
- Class `cuda::experimental::graph_builder`: An owning wrapper for a `cudaGraph_t` object.
- cudax 2.9.0 / CCCL 2.9.0
* - :ref:`<cuda/experimental/graph.cuh> <cudax-graph-graph-node-ref>`
- Class `cuda::experimental::graph_node_ref`: A non-owning wrapper for a `cudaGraphNode_t` object.
- cudax 2.9.0 / CCCL 2.9.0
* - :ref:`<cuda/experimental/graph.cuh> <cudax-graph-graph>`
- Class `cuda::experimental::graph`: An owning wrapper for a `cudaGraphExec_t` object.
- cudax 2.9.0 / CCCL 2.9.0
.. _cudax-graph-graph-builder:
graph_builder
-------------
``cuda::experimental::graph_builder`` is an owning wrapper for a ``cudaGraph_t`` object.
See the API reference entries in this section for details.
.. _cudax-graph-graph-node-ref:
graph_node_ref
--------------
``cuda::experimental::graph_node_ref`` is a non-owning wrapper for a ``cudaGraphNode_t`` object.
See the API reference entries in this section for details.
.. _cudax-graph-graph:
graph
-----
``cuda::experimental::graph`` is an owning wrapper for a ``cudaGraphExec_t`` object.
See the API reference entries in this section for details.

View File

@@ -0,0 +1,39 @@
.. _cudax-module:
CUDA Experimental
=================
.. toctree::
:hidden:
:maxdepth: 1
Overview <self>
container
graph
places
stf
API reference <api/index>
``CUDA Experimental`` (``cudax``) provides experimental new features that are still in development and subject to change.
However, any feature within this library has important use cases and we encourage users to experiment with them.
Specifically, ``cudax`` provides:
- :ref:`asynchronous host from/to device byte-wise mdspan copy <cudax-copy-bytes>`
- :ref:`mdspan byte fill <cudax-fill-bytes>`
- :ref:`uninitialized storage <libcudacxx-containers-uninitialized-async-buffer>`
- :ref:`graph functionality <cudax-graph>`
- dimensions description functionality
- :ref:`places <cudax-places>` for managing execution and data affinity across devices
- :ref:`an implementation of the STF (Sequential Task Flow) programming model <stf>`
Stability Guarantees
---------------------
There are no stability guarantees whatsoever. We reserve the right to change both the ABI and the API of any feature
within ``cudax`` at any time without notice.
Availability
-------------
Due to its experimental nature and the lack of stability guarantees, ``cudax`` is not shipped with the CUDA toolkit but
is solely available through GitHub.

View File

@@ -0,0 +1,778 @@
.. _cudax-places:
Places
======
.. contents::
:depth: 2
Places are abstractions that represent where code executes and where data
resides across the non-uniform memory of a CUDA system. They provide a
unified interface for managing execution affinity, stream pools, memory
allocation, and device context switching -- independently of any task-based
programming model.
Places come in two flavors:
- **Execution places** (``exec_place``) determine where code is executed.
- **Data places** (``data_place``) specify where data is located in memory.
The places API is part of the ``cuda::experimental::places`` C++ namespace
and can be used standalone via the ``cuda/experimental/places.cuh`` header,
without pulling in the full CUDASTF task-graph framework. For backward
compatibility, all places types are also available in the
``cuda::experimental::stf`` namespace.
.. _places-execution-places:
Execution places
----------------
An *execution place* describes a location where computation can occur.
The following factory methods create the most common execution places:
- ``exec_place::device(id)`` -- a specific CUDA device
- ``exec_place::host()`` -- the host CPU
- ``exec_place::current_device()`` -- the CUDA device that is currently active
- ``exec_place::cuda_context(ctx, devid)`` -- an externally-owned CUDA driver
context; the device ordinal is derived from the context when ``devid`` is
omitted
When an execution place is activated, it sets the appropriate CUDA context
(e.g. calls ``cudaSetDevice``). Each execution place also has an *affine*
data place: the memory location naturally associated with it. For a device
execution place the affine data place is the device's global memory; for
the host it is pinned host memory (RAM).
A CUDA-context execution place is non-owning. The caller must keep the
``CUcontext`` alive while the place and any streams obtained from it are in use.
.. _places-data-places:
Data places
-----------
A *data place* describes a memory location where data can reside. The
following factory methods are available:
- ``data_place::device(id)`` -- global memory of a specific CUDA device
- ``data_place::host()`` -- pinned host memory
- ``data_place::managed()`` -- CUDA managed (unified) memory
- ``data_place::affine()`` -- the data place naturally associated with the
current execution place
The *affine* data place is the default: when no data place is specified,
data is placed in the memory that is local to the execution place. For
example, a task running on device 0 will access data in device 0's global
memory by default.
Non-affine placement is also supported: data can be placed on a different
device or in host memory regardless of where the computation runs. This is
useful for sparse accesses (leveraging CUDA Unified Memory page faulting)
or for addressing memory capacity constraints. Non-affine placement assumes
the hardware and OS support such accesses (NVLINK, UVM, etc.).
.. _places-container-keys:
Places as container keys
------------------------
Both ``exec_place`` and ``data_place`` can be used as keys in standard
associative containers. The library provides the required comparison and
hash support:
- **``std::map``** and **``std::set``** use ``operator<`` (strict weak
ordering) for keys. Both place types implement ``operator<``, so they
can be used as ordered map or set keys.
- **``std::unordered_map``** and **``std::unordered_set``** require a
hash function and equality. The library specializes ``cuda::experimental::stf::hash``
for both place types (accessible from both the ``stf`` and ``places`` namespaces),
and both implement ``operator==``.
This allows, for example, maintaining per-place handles (e.g. CUBLAS or
CUSOLVER handles keyed by ``exec_place``) or per-place caches keyed by
``data_place``, using either ordered or hash-based containers as needed.
The following snippet shows lazy creation of a CUBLAS handle per execution
place using an ``std::unordered_map`` keyed by ``exec_place``:
.. code:: c++
#include <cuda/experimental/places.cuh>
#include <cublas_v2.h>
using namespace cuda::experimental::places;
cublasHandle_t& get_cublas_handle(const exec_place& ep = exec_place::current_device())
{
static std::unordered_map<exec_place, cublasHandle_t, hash<exec_place>> handles;
auto& h = handles[ep];
if (h == cublasHandle_t{})
{
exec_place_scope scope(ep);
cuda_safe_call(cublasCreate(&h));
}
return h;
}
.. _places-activate:
Setting the current device or context
--------------------------------------
The ``exec_place::activate()`` method provides a generic alternative to
``cudaSetDevice()`` that works uniformly across different execution place types.
This is useful when you want to set the current CUDA device or context without
using tasks.
The method returns an ``exec_place`` representing the previous state, which can
be used to restore the original device or context.
**Behavior by execution place type:**
- **Device places** (``exec_place::device(id)``): Calls ``cudaSetDevice(id)``
- **Green context places**: Sets the current CUDA driver context via ``cuCtxSetCurrent()``
- **Host places**: No-op
**Basic usage with devices:**
.. code:: cpp
exec_place place = exec_place::device(1);
exec_place prev = place.activate(); // Switch to device 1
// ... perform operations on device 1 ...
place.deactivate(prev); // Restore previous device
**Alternative restoration pattern:**
You can also restore by calling ``activate()`` on the returned place:
.. code:: cpp
exec_place place = exec_place::device(1);
exec_place prev = place.activate();
// ... work on device 1 ...
prev.activate(); // Equivalent to place.deactivate(prev)
**Usage with green contexts (CUDA 12.4+):**
Green contexts provide SM-level partitioning of GPU resources. The
``activate()``/``deactivate()`` methods handle the underlying driver context
management:
.. code:: cpp
// Create green contexts with 8 SMs each
green_context_helper gc(8, device_id);
auto view = gc.get_view(0);
exec_place gc_place = exec_place::green_ctx(view);
exec_place prev = gc_place.activate(); // Sets green context as current
// ... GPU work runs with SM affinity ...
gc_place.deactivate(prev); // Restore original context
**RAII scope for scoped activation:**
For exception-safe code or when you want automatic restoration, use the
``exec_place_scope`` RAII helper:
.. code:: cpp
{
exec_place_scope scope(exec_place::device(1));
// Device 1 is now active
// ... perform operations on device 1 ...
}
// Previous device is automatically restored when scope goes out of scope
The guard automatically restores the previous execution place when it goes out
of scope, making it useful for exception-safe code.
.. _places-stream-management:
Stream management with execution places
----------------------------------------
Execution places can be used independently of any task system to manage CUDA
streams in a structured way. This is useful when you want to use place
abstractions (devices, green contexts) for stream management without the full
task-based programming model.
Stream pools for pooled places (``device(N)``, ``host()``) live in an
``exec_place_resources`` registry that the caller owns. Pass the registry to
``exec_place::pick_stream`` to get a CUDA stream; the per-place pool inside the
registry is created lazily on first request and is destroyed when the registry
is destroyed.
The method accepts an optional ``for_computation`` hint (defaults to ``true``)
that may select between computation and data transfer stream pools to improve
overlapping. This is purely a performance hint, and it does not affect
correctness. Not all execution places enforce it.
.. code:: cpp
#include <cuda/experimental/places.cuh>
using namespace cuda::experimental::places;
// Standalone use: own the registry yourself.
exec_place_resources resources;
// Get a stream from the current device
exec_place place = exec_place::current_device();
cudaStream_t stream = place.pick_stream(resources);
// Use the stream for CUDA operations
myKernel<<<grid, block, 0, stream>>>(d_data);
// Get streams from specific devices (sharing the same registry)
cudaStream_t stream_dev0 = exec_place::device(0).pick_stream(resources);
cudaStream_t stream_dev1 = exec_place::device(1).pick_stream(resources);
Inside a CUDASTF context, the context's ``async_resources_handle`` already
holds an ``exec_place_resources`` registry. Convenience overloads accept the
handle directly so call sites do not have to dereference it:
.. code:: cpp
cudaStream_t stream = place.pick_stream(ctx.async_resources());
Stream pools are populated lazily -- CUDA streams are only created when first
requested via ``pick_stream(resources)`` (or ``pick_stream(ctx.async_resources())``
inside CUDASTF). Self-contained places (``exec_place::cuda_stream(s)``,
green-context places) ignore the registry and return their own embedded pool
instead, so the user-provided ``cudaStream_t`` / ``CUgreenCtx`` must outlive
any place that wraps it.
.. _places-memory-allocation:
Memory allocation with data places
------------------------------------
Data places provide a unified interface for memory allocation that works across
different memory types (host, device, managed) and place extensions (green
contexts, user-defined places). This allows you to allocate memory while
benefiting from the place abstraction.
The ``data_place::allocate()`` and ``data_place::deallocate()`` methods provide
raw memory allocation. The stream parameter defaults to ``nullptr``, which is
convenient for non-stream-ordered allocations (host, managed) where the stream
is ignored:
.. code:: cpp
#include <cuda/experimental/places.cuh>
using namespace cuda::experimental::places;
// Allocate on host (pinned memory) - stream defaults to nullptr
void* host_ptr = data_place::host().allocate(1024);
// ... use host_ptr ...
data_place::host().deallocate(host_ptr, 1024);
// Allocate on a specific device (stream-ordered)
cudaStream_t stream;
cudaStreamCreate(&stream);
void* dev_ptr = data_place::device(0).allocate(1024, stream);
// ... use dev_ptr with stream ...
data_place::device(0).deallocate(dev_ptr, 1024, stream);
cudaStreamDestroy(stream);
// Allocate managed memory - stream defaults to nullptr
void* managed_ptr = data_place::managed().allocate(1024);
// ... use managed_ptr from host or device ...
data_place::managed().deallocate(managed_ptr, 1024);
**Stream-ordered vs immediate allocations:**
Different data places have different allocation behaviors:
- **Host** (``data_place::host()``): Uses ``cudaMallocHost()`` / ``cudaFreeHost()`` - immediate, stream parameter is ignored
- **Managed** (``data_place::managed()``): Uses ``cudaMallocManaged()`` / ``cudaFree()`` - immediate, stream parameter is ignored (note: ``cudaFree`` may introduce implicit synchronization)
- **Device** (``data_place::device(id)``): Uses ``cudaMallocAsync()`` / ``cudaFreeAsync()`` - stream-ordered
- **Extensions** (green contexts, etc.): Behavior depends on the extension implementation
You can query whether a place uses stream-ordered allocation with
``allocation_is_stream_ordered()``:
.. code:: cpp
data_place place = data_place::device(0);
if (place.allocation_is_stream_ordered()) {
// Allocation is stream-ordered - synchronize via the stream
void* ptr = place.allocate(size, stream);
myKernel<<<grid, block, 0, stream>>>(ptr);
place.deallocate(ptr, size, stream);
cudaStreamSynchronize(stream);
} else {
// Allocation is immediate - stream is ignored, safe to use right away
void* ptr = place.allocate(size);
// ... use ptr ...
place.deallocate(ptr, size);
}
This abstraction is particularly useful when writing generic code that needs to
work with different types of places, including custom place extensions.
Geometry-aware allocation with allocate_nd
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Some places need to know the shape of the tensor being allocated, not just its
size: a composite data place distributes the allocation according to a
partitioner that maps *element coordinates* to places. ``allocate_nd()`` takes
the tensor extents (dimension 0 varying fastest) and the element size:
.. code:: cpp
// 2-D tensor of nx x ny doubles, distributed by the place's partitioner
void* ptr = place.allocate_nd(dim4(nx, ny), sizeof(double));
// ...
place.deallocate(ptr, nx * ny * sizeof(double));
For most places this is equivalent to ``allocate(prod(dims) * elemsize)``. For
composite places it is required: the byte-count ``allocate()`` throws there,
since a byte count alone cannot carry the geometry the partitioner needs. A
caller that genuinely has untyped bytes states that explicitly with
``allocate_nd(dim4(nbytes), 1)``, which distributes the buffer with byte
granularity. This raw-byte form applies to composite places built from
scale-free partitioners only; a composite place backed by a structured
partition (see :ref:`places-structured-partitions`) accepts exactly the
extents of the tensor the partition was built for and rejects anything else,
including a flat byte count.
.. _places-vmm:
VMM-based allocation with mem_create
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
For advanced use cases involving CUDA's Virtual Memory Management (VMM) API,
``data_place`` also provides the ``mem_create()`` method. This is a lower-level
interface used internally by localized arrays (``composite_slice``) to create
physical memory segments that are then mapped into a contiguous virtual address
space.
Unlike ``allocate()``, which returns a usable pointer directly, ``mem_create()``
returns a ``CUmemGenericAllocationHandle`` that must be subsequently mapped with
``cuMemMap()`` before use:
.. code:: cpp
#include <cuda/experimental/places.cuh>
using namespace cuda::experimental::places;
// Create a physical memory handle for device 0
CUmemGenericAllocationHandle handle;
data_place::device(0).mem_create(&handle, size);
// The handle must be mapped to a virtual address before use
// (see CUDA VMM documentation for cuMemMap, cuMemSetAccess, etc.)
**When to use each method:**
- Use ``allocate()`` for most cases - it provides ready-to-use memory with
stream-ordered semantics where applicable.
- Use ``mem_create()`` only when you need explicit control over virtual memory
mapping, such as creating localized arrays that span multiple devices with a
unified virtual address space.
**Limitations of mem_create:**
- Only supports device memory and host memory (pinned)
- Managed memory is **not supported** by the VMM API
- The returned handle requires additional VMM API calls to be usable
Custom place extensions can override ``mem_create()`` to provide specialized
VMM allocation behavior (e.g., memory localization for hardware partitions).
.. _places-grid:
Grid of places
--------------
It is possible to manipulate places which are a collection of multiple places.
In particular, it is possible to define an execution place which corresponds
to multiple device execution places.
Creating grids of places
^^^^^^^^^^^^^^^^^^^^^^^^
A grid of execution places is an ``exec_place`` that contains multiple
underlying places. Grids are created with the ``make_grid`` free function:
.. code:: c++
// Create a 1D grid from a vector of places
exec_place grid = make_grid(std::vector<exec_place>{
exec_place::device(0), exec_place::device(1)
});
The ``exec_place::all_devices()`` helper creates a grid of all available
CUDA devices:
.. code:: c++
exec_place all = exec_place::all_devices();
Similarly, ``exec_place::n_devices(n)`` creates a grid from the first ``n``
devices:
.. code:: c++
exec_place first_four = exec_place::n_devices(4);
It is possible to retrieve the total number of elements in a grid using
the ``size()`` method, and individual places with ``get_place(i)``:
.. code:: c++
exec_place grid = exec_place::all_devices();
for (size_t i = 0; i < grid.size(); i++) {
exec_place dev = grid.get_place(i);
// ...
}
Shaped grids
^^^^^^^^^^^^
Grids of places need not be 1D arrays. They can be structured as a
multi-dimensional grid described with a ``dim4`` class by passing it to
``make_grid`` or ``n_devices``:
.. code:: c++
// Create a shaped grid: 8 devices arranged as a 2x2x2 cube
exec_place cube = exec_place::n_devices(8, dim4(2, 2, 2));
// Or from an explicit vector
exec_place shaped = make_grid(my_places, dim4(4, 2));
Note that the total size of the ``dim4`` must match the number of places.
It is possible to query the *shape* of the grid using ``get_dims()``,
which returns a ``dim4`` object. Individual places can be accessed by
multi-dimensional position using ``get_place(pos4)``.
Reshaping and collapsing grid axes
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
An existing grid can be viewed with different dimensions using
``reshape()``. The new dimensions must contain exactly the same number of
places:
.. code:: c++
exec_place cube = make_grid(my_places, dim4(2, 3, 4));
exec_place flat = cube.reshape(dim4(24));
Reshaping changes only the grid coordinate system. It preserves dimension-0-
fastest linear order, so ``flat.get_place(i) == cube.get_place(i)`` for every
linear index ``i``. It does not reorder, replicate, or remove places.
``collapse_axes(first, last)`` is a convenience operation that combines a
contiguous inclusive range of axes. The collapsed extent is the product of
the selected extents; later axes shift left and trailing extents become one:
.. code:: c++
exec_place grid = make_grid(my_places, dim4(2, 3, 4));
exec_place grid_6x4 = grid.collapse_axes(0, 1); // dim4(6, 4)
exec_place grid_2x12 = grid.collapse_axes(1, 2); // dim4(2, 12)
exec_place grid_24 = grid.collapse_axes(0, 3); // dim4(24)
These operations are useful when a partition should consume several axes of
a processor grid as one logical axis. They are coordinate transformations,
not :ref:`places-partitioning`: the latter decomposes a place into constituent
resources.
.. _places-partitioning:
Partitioning grids
^^^^^^^^^^^^^^^^^^
The ``place_partition`` class partitions an execution place at a given
granularity. This is useful for splitting a multi-device grid into its
constituent devices, or for partitioning a device into green contexts or
CUDA streams.
The partitioning granularity is specified by ``place_partition_scope``:
- ``place_partition_scope::cuda_device`` -- partition into individual devices
- ``place_partition_scope::green_context`` -- partition into green contexts (CUDA 12.4+)
- ``place_partition_scope::cuda_stream`` -- partition into CUDA streams
.. code:: c++
exec_place grid = exec_place::all_devices();
// Partition into individual devices
place_partition devices(grid, place_partition_scope::cuda_device);
for (auto& dev : devices) {
// dev is an exec_place for a single device
}
// Convert back to an exec_place grid
exec_place new_grid = devices.to_exec_place();
The ``exec_place::partition_by_scope()`` method provides a shorthand that
returns a new ``exec_place`` grid directly:
.. code:: c++
exec_place grid = exec_place::all_devices();
exec_place by_device = grid.partition_by_scope(place_partition_scope::cuda_device);
.. _places-data-partitioning:
Data partitioning policies
^^^^^^^^^^^^^^^^^^^^^^^^^^
When using a grid of places with CUDASTF constructs such as ``parallel_for``,
*data partitioning policies* express how data and index spaces are dispatched
over the different places of a grid.
.. code:: c++
class MyPartition : public partitioner_base {
public:
template <typename S_out, typename S_in>
static const S_out apply(const S_in& in, pos4 position, dim4 grid_dims);
void get_executor(pos4* result, pos4 data_coords, dim4 data_dims, dim4 grid_dims);
};
A partitioning class must implement an ``apply`` method which takes:
- a reference to a shape of type ``S_in``
- a position within a grid of execution places, described using an object of
type ``pos4``
- the dimension of this grid expressed as a ``dim4`` object
``apply`` returns a shape which corresponds to the subset of the ``in``
shape associated to this entry of the grid. Note that the output shape
type ``S_out`` may be different from the ``S_in`` type of the input
shape.
To support different types of shapes, appropriate overloads of the
``apply`` method should be implemented.
This ``apply`` method is typically used by the ``parallel_for``
construct in order to dispatch indices over the different places.
A partitioning class must also implement the ``get_executor`` virtual
method which allows localized data allocators. This
method indicates, for each entry of a shape, on which place this entry
should *preferably* be allocated.
``get_executor`` writes a ``pos4`` coordinate in the execution place
grid into ``*result``, and its input arguments are:
- a coordinate within the shape described as a ``pos4`` object
- the dimension of the shape expressed as a ``dim4`` object
- the dimension of the execution place grid expressed as a ``dim4`` object
Defining the ``get_executor`` makes it possible to map a piece of data
over an execution place grid. The ``get_executor`` method of a partitioning
policy in an execution place grid therefore defines the *affine data
place* of a logical data accessed on that grid.
Predefined partitioning policies
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
There are currently two policies readily available:
- ``tiled_partition<TILE_SIZE>`` dispatches entries of a shape using a
*tiled* layout. For multi-dimensional shapes, the outermost dimension is
dispatched into contiguous tiles of size ``TILE_SIZE``.
- ``blocked_partition`` dispatches entries of the shape using a *blocked*
layout, where each entry of the grid of places receives approximately
the same contiguous portion of the shape, dispatched along the outermost
dimension.
This illustrates how a 2D shape is dispatched over 3 places using the
blocked layout:
.. code:: text
__________________________________
| | | |
| | | |
| | | |
| P 0 | P 1 | P 2 |
| | | |
| | | |
|___________|___________|_________|
This illustrates how a 2D shape is dispatched over 3 places using a
tiled layout, where the dimension of the tiles is indicated by the
``TILE_SIZE`` parameter:
.. code:: text
________________________________
| | | | | | |
| | | | | | |
| | | | | | |
| P 0 | P 1 | P 2 | P 0 | P 1 |P2|
| | | | | | |
| | | | | | |
|_____|_____|_____|_____|_____|__|
.. _places-structured-partitions:
Structured partitions
---------------------
The classic partitioning policies above are *scale-free*: ``blocked_partition``
splits whatever shape it is handed, knows nothing about the tensor it will be
applied to, and always dispatches along the outermost dimension. A
*structured partition* (``cute_partition``) is the complementary tool: it
describes, dimension by dimension, how **one specific tensor** maps onto a
grid of places.
.. code:: c++
using namespace cuda::experimental::places;
// A 3-D tensor: dimension 1 blocked over the places of the grid,
// dimensions 0 and 2 not distributed
auto part = make_partition(
dim4(nx, ny, nz),
partition_spec{whole, blocked<0>, whole},
grid.get_dims());
Each entry in ``partition_spec`` selects a policy for the corresponding
tensor dimension: ``whole`` (not distributed), ``blocked<axis>``,
``cyclic<axis>``, or ``block_cyclic<axis>(block_size)``. Rank, policy,
mesh-axis, and leaf counts are preserved in the C++ type; tensor extents,
strides, and block sizes remain runtime values. This is strictly more
expressive than the classic policies -- splitting dimension 1 of a 3-D
tensor, or mixing policies across dimensions, cannot be stated with
``blocked_partition``.
The reference shape, padding, and predication
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
The first argument of ``make_partition`` is the tensor's extents: unlike a
classic policy, a structured partition is **bound to one reference shape**,
and remains the authority on it. This is a deliberate trade, and the source
of most of the type's properties:
- Split dimensions are *padded up to divisibility* (a 10-element dimension
blocked over 3 places is treated as 12, in chunks of 4). Padding makes the
underlying layout exact and bijective, which is what keeps every query
closed-form: validation is a linear pass over the layout, and the owner of
a coordinate is a chain of divisions and modulos.
- Coordinates beyond the true extents (the *padding phantoms*) own no bytes
and do no work: consumers discard them by comparing coordinates against
the true extents. This is the *predication* idiom of CUTLASS/CuTe
("partition the rounded-up shape, predicate the boundary") rather than
per-place clamping, which would break the layout's uniformity.
Ownership can be queried directly, and -- more importantly -- a candidate
mapping can be **scored before any memory is committed**:
.. code:: c++
pos4 owner = part.owner(pos4(x, y, z)); // grid position owning (x,y,z)
// Dry run: same block-majority decision procedure as a real allocation
localized_stats stats = evaluate_localized_placement(grid, part, sizeof(double));
// stats.bytes_per_place, stats.accuracy() (estimated fraction of local bytes),
// stats.nallocs, ... -- tune the spec, then allocate
Placement through a structured partition
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
A structured partition can back a composite data place. Because the
partition is bound to one tensor, such a place is *per-tensor* -- allocate
with the partition's exact extents (compare with the classic composite
place, which is a reusable shape-free policy):
.. code:: c++
data_place dp = make_composite_data_place(grid, part);
void* ptr = dp.allocate_nd(dim4(nx, ny, nz), sizeof(double));
// physical pages land on the place owning them, per the partition
dp.deallocate(ptr, nx * ny * nz * sizeof(double));
Two structured composite places built from equal partitions compare equal,
so they denote the same data placement wherever data places are compared.
Conventions and limits
^^^^^^^^^^^^^^^^^^^^^^
- Extents follow the **dimension-0-fastest** linearization of
``dim4::get_index()`` (the convention of STF slices). A row-major front-end
must present its *whole* description in this order -- the extents, the
per-dimension ``partition_spec``, and any coordinates passed to ``owner()``
reverse together, since reversing only the extents would silently re-target
each policy at the wrong axis.
- At most 4 tensor dimensions (the ``pos4``/``dim4`` domain).
- Typed partitions and their kernel-facing sub-shapes store exactly their
layout leaves. Runtime interfaces (including C/Python opaque handles) erase
them to a canonical descriptor only at the data-place boundary.
- The partition object is trivially copyable and its queries are host/device
callable.
The ``partitioned_axpy`` example shows the intended workflow end to end:
express the partition once, evaluate it, run tasks over data placed by it,
and perform a raw geometry-aware allocation.
Computing over structured partitions
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
The same ``parallel_for`` entry point that accepts the classic policies
accepts a structured partition instance, which then decides **both** the
per-place kernel decomposition and (through the task's affine data place)
the placement of the data those kernels touch -- one object, both sides:
.. code:: c++
// Every place computes exactly the coordinates it owns
ctx.parallel_for(part, grid, lX.shape(), lX.write())
->*[] __device__(size_t x, size_t y, size_t z, auto X) { ... };
The shape argument may also be a ``box`` describing a *region within the
tensor the partition was built for* (validated by containment) -- e.g. the
interior of a stencil domain. Each place still enumerates its own
coordinates; those outside the region (like the padding phantoms of uneven
extents) are skipped by a per-coordinate predicate, so iteration stays
aligned with data ownership rather than re-splitting the region:
.. code:: c++
box interior({1ul, nx - 1}, {1ul, ny - 1}, {1ul, nz - 1});
ctx.parallel_for(part, grid, interior, lX.rw())->*...;
Predication has a cost proportional to the *rejected* fraction of the
enumerated coordinates, which makes it the right tool for regions that are
dense in their bounds (interiors: the rejected boundary shell is a
surface-to-volume fraction) and the wrong tool for thin regions. For
boundary-style updates -- a face of the domain, say -- prefer one of:
- **fuse** the boundary handling into the volumetric kernel's body when the
condition is cheap (application-dependent);
- iterate the face with a **classic scale-free policy** (tight, no rejected
coordinates) while an explicit dependency keeps placement on the
partition's composite place:
.. code:: c++
auto dist = make_composite_data_place(grid, part);
box face({0ul, nx}, {0ul, ny}, {0ul, 1ul});
ctx.parallel_for(blocked_partition(), grid, face, lX.rw(dist))->*...;
The face's few remote writes (places computing parts of a face another
place owns) are typically negligible against the volumetric traffic.
The ``fdtd_mgpu`` example demonstrates the full pattern: a single
``make_partition`` call decides which dimension splits for every task --
initialization over the full shape, updates over interior boxes, a point
source -- and places the fields' data, so changing the distribution of the
whole simulation is editing one ``partition_spec`` entry.

File diff suppressed because it is too large Load Diff

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.