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
185
cccl_upstream/docs/cudax/Doxyfile
Normal 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
|
||||
27
cccl_upstream/docs/cudax/container.rst
Normal 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
|
||||
52
cccl_upstream/docs/cudax/graph.rst
Normal 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.
|
||||
39
cccl_upstream/docs/cudax/index.rst
Normal 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.
|
||||
778
cccl_upstream/docs/cudax/places.rst
Normal 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.
|
||||
2424
cccl_upstream/docs/cudax/stf.rst
Normal file
330
cccl_upstream/docs/cudax/stf/custom_data_interface.rst
Normal 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));
|
||||
}
|
||||
}
|
||||
5
cccl_upstream/docs/cudax/stf/images/dag-sections-0.dot
Normal 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"]
|
||||
}
|
||||
BIN
cccl_upstream/docs/cudax/stf/images/dag-sections-0.png
Normal file
|
After Width: | Height: | Size: 6.9 KiB |
14
cccl_upstream/docs/cudax/stf/images/dag-sections-1.dot
Normal 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"]
|
||||
}
|
||||
BIN
cccl_upstream/docs/cudax/stf/images/dag-sections-1.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
36
cccl_upstream/docs/cudax/stf/images/dag-sections-2.dot
Normal 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) "]
|
||||
}
|
||||
BIN
cccl_upstream/docs/cudax/stf/images/dag-sections-2.png
Normal file
|
After Width: | Height: | Size: 42 KiB |
69
cccl_upstream/docs/cudax/stf/images/dag-sections.dot
Normal 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) "]
|
||||
}
|
||||
BIN
cccl_upstream/docs/cudax/stf/images/dag-sections.png
Normal file
|
After Width: | Height: | Size: 101 KiB |
|
After Width: | Height: | Size: 11 KiB |
BIN
cccl_upstream/docs/cudax/stf/images/dot-output-axpy-events.png
Normal file
|
After Width: | Height: | Size: 132 KiB |
BIN
cccl_upstream/docs/cudax/stf/images/dot-output-axpy.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
cccl_upstream/docs/cudax/stf/images/dot-output-heat.png
Normal file
|
After Width: | Height: | Size: 113 KiB |
10
cccl_upstream/docs/cudax/stf/images/graph_01.dot
Normal 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;
|
||||
}
|
||||
BIN
cccl_upstream/docs/cudax/stf/images/graph_01.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
16
cccl_upstream/docs/cudax/stf/images/graph_02.dot
Normal 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"];
|
||||
}
|
||||
BIN
cccl_upstream/docs/cudax/stf/images/graph_02.png
Normal file
|
After Width: | Height: | Size: 32 KiB |
BIN
cccl_upstream/docs/cudax/stf/images/ncu-ui.png
Normal file
|
After Width: | Height: | Size: 171 KiB |
26
cccl_upstream/docs/cudax/stf/images/task-sequence-user.dot
Normal 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];
|
||||
}
|
||||
BIN
cccl_upstream/docs/cudax/stf/images/task-sequence-user.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
68
cccl_upstream/docs/cudax/stf/images/task-sequence.dot
Normal 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]
|
||||
}
|
||||
BIN
cccl_upstream/docs/cudax/stf/images/task-sequence.png
Normal file
|
After Width: | Height: | Size: 49 KiB |
114
cccl_upstream/docs/cudax/stf/lower_level_api.rst
Normal 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.
|
||||