CUDA 编程指南 13.4.2 · 非官方中文译本
连续阅读
原著发布于 2026 年 9 月 10 日对照 NVIDIA 英文原文对应英文网页

5.6. 设备可调用的 API 和内建函数

本章包含可从 CUDA 核函数和设备代码中调用的 API 和内建函数的参考资料及 API 文档。

5.6.1. 内存屏障原语接口

原语 API 是用于访问cuda::barrier功能的类 C 接口。通过包含<cuda_awbarrier_primitives.h>头文件即可使用这些原语。

5.6.1.1. 数据类型

typedef /* implementation defined */ __mbarrier_t;
typedef /* implementation defined */ __mbarrier_token_t;

5.6.1.2. 内存屏障原语 API

uint32_t __mbarrier_maximum_count();
void __mbarrier_init(__mbarrier_t* bar, uint32_t expected_count);
  • bar必须是指向__shared__内存的指针。

  • expected_count <= __mbarrier_maximum_count()

  • 将*bar当前阶段和下一阶段的预期到达计数初始化为expected_count。

void __mbarrier_inval(__mbarrier_t* bar);
  • bar必须是指向驻留在共享内存中的屏障对象的指针。

  • 在将相应共享内存用于其他用途之前,必须使*bar失效。

__mbarrier_token_t __mbarrier_arrive(__mbarrier_t* bar);
  • *bar的初始化必须先发生于此调用。

  • 待到达计数不得为零。

  • 以原子方式递减屏障当前阶段的待到达计数。

  • 返回一个与递减操作紧前的屏障状态关联的到达令牌。

__mbarrier_token_t __mbarrier_arrive_and_drop(__mbarrier_t* bar);
  • *bar的初始化必须先发生于此调用。

  • 待到达计数不得为零。

  • 以原子方式递减屏障当前阶段的待到达计数和下一阶段的预期计数。

  • 返回一个与递减操作紧前的屏障状态关联的到达令牌。

bool __mbarrier_test_wait(__mbarrier_t* bar, __mbarrier_token_t token);
  • token必须与*bar的紧前阶段或当前阶段关联。

  • 如果token与*bar的紧前阶段关联,则返回true,否则返回false。

bool __mbarrier_test_wait_parity(__mbarrier_t* bar, bool phase_parity);
  • phase_parity必须指示*bar的当前阶段或紧前阶段的奇偶性。值true对应编号为奇数的阶段,值false对应编号为偶数的阶段。

  • 如果phase_parity指示*bar的紧前阶段的整数奇偶性,则返回true,否则返回false。

bool __mbarrier_try_wait(__mbarrier_t* bar, __mbarrier_token_t token, uint32_t max_sleep_nanosec);
  • token必须与*bar的紧前阶段或当前阶段关联。

  • 如果token与*bar的紧前阶段关联,则返回true。否则,正在执行的线程可能被挂起。挂起的线程会在指定阶段完成时恢复执行(返回true),或者在该阶段完成之前经过一段取决于系统的时限后恢复执行(返回false)。

  • max_sleep_nanosec指定以纳秒为单位的时限,该时限可用于替代取决于系统的时限。

bool __mbarrier_try_wait_parity(__mbarrier_t* bar, bool phase_parity, uint32_t max_sleep_nanosec);
  • phase_parity必须指示*bar的当前阶段或紧前阶段的奇偶性。值true对应编号为奇数的阶段,值false对应编号为偶数的阶段。

  • 如果phase_parity指示*bar的紧前阶段的整数奇偶性,则返回true。否则,正在执行的线程可能被挂起。挂起的线程会在指定阶段完成时恢复执行(返回true),或者在该阶段完成之前经过一段取决于系统的时限后恢复执行(返回false)。

  • max_sleep_nanosec指定以纳秒为单位的时限,该时限可用于替代取决于系统的时限。

5.6.2. 流水线原语接口

流水线原语为<cuda/pipeline>中提供的功能提供了类似 C 的接口。包含<cuda_pipeline.h>头文件即可使用流水线原语接口。在不兼容 ISO C++ 2011 的模式下编译时,请包含<cuda_pipeline_primitives.h>头文件。

注意

流水线原语 API 仅支持跟踪满足特定大小和对齐要求的从全局内存到共享内存的异步复制。它提供的功能等同于具有cuda::thread_scope_thread的cuda::pipeline对象。

5.6.2.1. memcpy_async原语

void __pipeline_memcpy_async(void* __restrict__ dst_shared,
                             const void* __restrict__ src_global,
                             size_t size_and_align,
                             size_t zfill=0);
  • 请求提交以下操作以进行异步求值:

    size_t i = 0;
    for (; i < size_and_align - zfill; ++i) ((char*)dst_shared)[i] = ((char*)src_global)[i]; /* copy */
    for (; i < size_and_align; ++i) ((char*)dst_shared)[i] = 0; /* zero-fill */
    
  • 要求:

    • dst_shared必须是指向memcpy_async的共享内存目标位置的指针。

    • src_global必须是指向memcpy_async的全局内存源位置的指针。

    • size_and_align必须为 4、8 或 16。

    • zfill <= size_and_align.

    • size_and_align必须是dst_shared和src_global的对齐值。

  • 任何线程在等待memcpy_async操作完成之前修改源内存或观察目标内存,都会构成竞争条件。在提交memcpy_async操作与等待其完成之间,以下任何行为都会引入竞争条件:

    • 从dst_shared加载。

    • 向dst_shared或src_global存储。

    • 对dst_shared或src_global执行原子更新。

5.6.2.2. 提交原语

void __pipeline_commit();
  • 将已提交的memcpy_async作为当前批次提交到流水线。

5.6.2.3. 等待原语

void __pipeline_wait_prior(size_t N);
  • 令{0, 1, 2, ..., L}为与给定线程对__pipeline_commit()的调用相关联的索引序列。

  • 等待至少到L-N为止(含该批次)的所有批次完成。

5.6.2.4. 到达屏障原语

void __pipeline_arrive_on(__mbarrier_t* bar);
  • bar指向共享内存中的一个屏障。

  • 将屏障到达计数加一,当执行顺序先于此调用的所有 memcpy_async 操作都已完成时,将到达计数减一,因此对到达计数的净影响为零。用户有责任确保增加到达计数时不会超过__mbarrier_maximum_count()。

5.6.3. 协作组 API

5.6.3.1. cooperative_groups.h

5.6.3.1.1. thread_block 类

任何 CUDA 程序员都已经熟悉一种特定的线程组:线程块。协作组扩展引入了一种新的数据类型thread_block,用于在核函数内显式表示这一概念。

class thread_block;

通过以下方式构造:

thread_block g = this_thread_block();

公有成员函数:

static void sync():使组中指定的线程同步,等同于g.barrier_wait(g.barrier_arrive())

thread_block::arrival_token barrier_arrive():到达 thread_block 屏障,返回一个需要传入barrier_wait()的令牌。

void barrier_wait(thread_block::arrival_token&& t):在thread_block屏障上等待,以右值引用的形式接受barrier_arrive()返回的到达令牌。

static unsigned int thread_rank():调用线程的序号,范围为 [0, num_threads)

static dim3 group_index():线程块在已启动网格内的三维索引

static dim3 thread_index():线程在已启动线程块内的三维索引

static dim3 dim_threads():已启动线程块的维度,以线程为单位

static unsigned int num_threads():组中的线程总数

旧版成员函数(别名):

static unsigned int size():组中的线程总数(num_threads()的别名)

static dim3 group_dim():已启动线程块的维度(dim_threads()的别名)

示例:

/// Loading an integer from global into shared memory
__global__ void kernel(int *globalInput) {
    __shared__ int x;
    thread_block g = this_thread_block();
    // Choose a leader in the thread block
    if (g.thread_rank() == 0) {
        // load from global into shared for all threads to work with
        x = (*globalInput);
    }
    // After loading data into shared memory, you want to synchronize
    // if all threads in your thread block need to see it
    g.sync(); // equivalent to __syncthreads();
}

注意:组中的所有线程都必须参与集合操作,否则行为未定义。

相关内容:thread_block数据类型派生自更通用的thread_group数据类型,后者可用于表示更广泛的组类别。

5.6.3.1.2. cluster_group 类

此组对象表示在单个簇中启动的所有线程。这些 API 可用于计算能力为 9.0 及以上的所有硬件。在这些情况下,当启动非簇网格时,这些 API 会假定存在一个 1x1x1 的簇。

class cluster_group;

通过以下方式构造:

cluster_group g = this_cluster();

公有成员函数:

static void sync():使组中指定的线程同步,等同于g.barrier_wait(g.barrier_arrive())

static cluster_group::arrival_token barrier_arrive():到达簇屏障,返回一个需要传入barrier_wait()的令牌。

static void barrier_wait(cluster_group::arrival_token&& t):在簇屏障上等待,以右值引用的形式接受barrier_arrive()返回的到达令牌。

static unsigned int thread_rank():调用线程的序号,范围为 [0, num_threads)

static unsigned int block_rank():调用线程块的序号,范围为 [0, num_blocks)

static unsigned int num_threads():组中的线程总数

static unsigned int num_blocks():组中的线程块总数

static dim3 dim_threads():已启动簇的维度,以线程为单位

static dim3 dim_blocks():已启动簇的维度,以线程块为单位

static dim3 block_index():调用线程块在已启动簇内的三维索引

static unsigned int query_shared_rank(const void *addr):获取共享内存地址所属的线程块序号

static T* map_shared_rank(T *addr, int rank):获取簇中另一个线程块的共享内存变量的地址

旧版成员函数(别名):

static unsigned int size():组中的线程总数(num_threads()的别名)

5.6.3.1.3. grid_group 类

此组对象表示在单个网格中启动的所有线程。除sync()之外的 API 始终可用,但要能够在整个网格范围内同步,需要使用协作启动 API。

class grid_group;

通过以下方式构造:

grid_group g = this_grid();

公有成员函数:

bool is_valid() const:返回 grid_group 是否能够同步

void sync() const:使组中指定的线程同步,等同于g.barrier_wait(g.barrier_arrive())

grid_group::arrival_token barrier_arrive():到达网格屏障,返回一个需要传入barrier_wait()的令牌。

void barrier_wait(grid_group::arrival_token&& t):在网格屏障上等待,以右值引用的形式接受barrier_arrive()返回的到达令牌。

static unsigned long long thread_rank():调用线程的序号,范围为 [0, num_threads)

static unsigned long long block_rank():调用线程块的序号,范围为 [0, num_blocks)

static unsigned long long cluster_rank():调用簇的序号,范围为 [0, num_clusters)

static unsigned long long num_threads():组中的线程总数

static unsigned long long num_blocks():组中的线程块总数

static unsigned long long num_clusters():组中的簇总数

static dim3 dim_blocks():已启动网格的维度,以线程块为单位

static dim3 dim_clusters():已启动网格的维度,以簇为单位

static dim3 block_index():线程块在已启动网格内的三维索引

static dim3 cluster_index():簇在已启动网格内的三维索引

旧版成员函数(别名):

static unsigned long long size():组中的线程总数(num_threads()的别名)

static dim3 group_dim():已启动网格的维度(dim_blocks()的别名)

5.6.3.1.4. thread_block_tile 类

分块组的模板化版本,其中使用模板参数指定分块的大小。在编译时已知此大小,就有可能实现更优化的执行。

template <unsigned int Size, typename ParentT = void>
class thread_block_tile;

通过以下方式构造:

template <unsigned int Size, typename ParentT>
_CG_QUALIFIER thread_block_tile<Size, ParentT> tiled_partition(const ParentT& g)

Size必须是 2 的幂且小于或等于 1024。注意事项部分介绍了在计算能力为 7.5 或更低的硬件上创建大小大于 32 的分块所需的额外步骤。

ParentT是划分出此组的父组类型。它会自动推断,但取值为 void 时,此信息将存储在组句柄中,而非类型中。

公有成员函数:

void sync() const:使组中指定的线程同步

unsigned long long num_threads() const:组中的线程总数

unsigned long long thread_rank() const:调用线程的序号,范围为 [0, num_threads)

unsigned long long meta_group_size() const:返回划分父组时创建的组数。

unsigned long long meta_group_rank() const:组在从父组划分出的分块集合中的线性序号(以 meta_group_size 为界)

T shfl(T var, unsigned int src_rank) const:请参阅线程束洗牌函数,注意:对于大于 32 的大小,组中的所有线程都必须指定相同的 src_rank,否则行为未定义。

T shfl_up(T var, int delta) const:请参阅线程束洗牌函数,仅适用于小于或等于 32 的大小。

T shfl_down(T var, int delta) const:请参阅线程束洗牌函数,仅适用于小于或等于 32 的大小。

T shfl_xor(T var, int delta) const:请参阅线程束洗牌函数,仅适用于小于或等于 32 的大小。

int any(int predicate) const:请参阅线程束投票函数

int all(int predicate) const:请参阅线程束投票函数

unsigned int ballot(int predicate) const:请参阅线程束投票函数,仅适用于小于或等于 32 的大小。

unsigned int match_any(T val) const:请参阅线程束匹配函数,仅适用于小于或等于 32 的大小。

unsigned int match_all(T val, int &pred) const:请参阅线程束匹配函数,仅适用于小于或等于 32 的大小。

旧版成员函数(别名):

unsigned long long size() const:组中的线程总数(num_threads()的别名)

注意:

  • 此处使用了thread_block_tile模板化数据结构,组大小作为模板参数而非实参传递给tiled_partition调用。

  • 使用 C++11 或更高版本编译时,shfl, shfl_up, shfl_down, and shfl_xor函数接受任意类型的对象。这意味着,只要满足以下约束,就可以对非整数类型进行洗牌:

    • 满足可平凡复制的条件,即is_trivially_copyable<T>::value == true

    • 对于小于或等于 32 的分块大小,满足sizeof(T) <= 32;对于更大的分块,满足sizeof(T) <= 8

  • 在计算能力为 7.5 或更低的硬件上,大小大于 32 的分块需要预留少量内存。可以使用cooperative_groups::block_tile_memory结构体模板来完成此操作,该结构体必须驻留在共享内存或全局内存中。

    template <unsigned int MaxBlockSize = 1024>
    struct block_tile_memory;
    

    MaxBlockSize指定当前线程块中的最大线程数。在仅以较少线程数启动的核函数中,可以使用此参数将block_tile_memory的共享内存用量降至最低。

    随后需要将此block_tile_memory传入cooperative_groups::this_thread_block,使得能够将生成的thread_block划分为大小大于 32 的分块。接受block_tile_memory参数的this_thread_block重载是一个集合操作,必须由thread_block中的所有线程调用。

    可以在计算能力为 8.0 或更高的硬件上使用block_tile_memory,以便编写一份面向多种不同计算能力的源代码。在不需要它的情况下,将其在共享内存中实例化应当不消耗任何内存。

示例:

/// The following code will create two sets of tiled groups, of size 32 and 4 respectively:
/// The latter has the provenance encoded in the type, while the first stores it in the handle
thread_block block = this_thread_block();
thread_block_tile<32> tile32 = tiled_partition<32>(block);
thread_block_tile<4, thread_block> tile4 = tiled_partition<4>(block);
/// The following code will create tiles of size 128 on all Compute Capabilities.
/// block_tile_memory can be omitted on Compute Capability 8.0 or higher.
__global__ void kernel(...) {
    // reserve shared memory for thread_block_tile usage,
    //   specify that block size will be at most 256 threads.
    __shared__ block_tile_memory<256> shared;
    thread_block thb = this_thread_block(shared);

    // Create tiles with 128 threads.
    auto tile = tiled_partition<128>(thb);

    // ...
}

5.6.3.1.5. coalesced_group 类

在 CUDA 的 SIMT 架构中,多处理器在硬件层面上以每组 32 个线程的方式执行线程,这些组称为线程束。如果应用程序代码中存在依赖数据的条件分支,导致线程束内的线程发生分歧,则线程束会串行执行每个分支,并禁用不在该路径上的线程。在路径上保持活跃的线程称为合并线程。协作组提供了发现并创建包含所有合并线程的组的功能。

通过coalesced_threads()构造组句柄是机会式的。它返回该时刻的活跃线程集合,不保证具体返回哪些线程(只要它们处于活跃状态),也不保证它们在整个执行过程中始终保持合并状态(执行集合操作时会将它们重新聚集在一起,但之后它们可以再次发生分歧)。

class coalesced_group;

通过以下方式构造:

coalesced_group active = coalesced_threads();

公有成员函数:

void sync() const:使组中指定的线程同步

unsigned long long num_threads() const:组中的线程总数

unsigned long long thread_rank() const:调用线程的序号,范围为 [0, num_threads)

unsigned long long meta_group_size() const:返回划分父组时创建的组数。如果此组是通过查询活跃线程集合创建的,例如coalesced_threads(),则meta_group_size()的值将为 1。

unsigned long long meta_group_rank() const:组在从父组划分出的分块集合中的线性序号(以 meta_group_size 为界)。如果此组是通过查询活跃线程集合创建的,例如coalesced_threads(),则meta_group_rank()的值将始终为 0。

T shfl(T var, unsigned int src_rank) const:请参阅线程束洗牌函数

T shfl_up(T var, int delta) const:请参阅线程束洗牌函数

T shfl_down(T var, int delta) const:请参阅线程束洗牌函数

int any(int predicate) const:请参阅线程束投票函数

int all(int predicate) const:请参阅线程束投票函数

unsigned int ballot(int predicate) const:请参阅线程束投票函数

unsigned int match_any(T val) const:请参阅线程束匹配函数

unsigned int match_all(T val, int &pred) const:请参阅线程束匹配函数

旧版成员函数(别名):

unsigned long long size() const:组中的线程总数(num_threads()的别名)

注意:

使用 C++11 或更高版本编译时,shfl, shfl_up, and shfl_down函数接受任意类型的对象。这意味着,只要满足以下约束,就可以对非整数类型进行洗牌:

  • 满足可平凡复制的条件,即is_trivially_copyable<T>::value == true

  • sizeof(T) <= 32

示例:

/// Consider a situation whereby there is a branch in the
/// code in which only the 2nd, 4th and 8th threads in each warp are
/// active. The coalesced_threads() call, placed in that branch, will create (for each
/// warp) a group, active, that has three threads (with
/// ranks 0-2 inclusive).
__global__ void kernel(int *globalInput) {
    // Lets say globalInput says that threads 2, 4, 8 should handle the data
    if (threadIdx.x == *globalInput) {
        coalesced_group active = coalesced_threads();
        // active contains 0-2 inclusive
        active.sync();
    }
}

5.6.3.2. cooperative_groups/async.h

5.6.3.2.1. memcpy_async

memcpy_async是一种组范围的集合 memcpy,利用硬件加速来支持从全局内存到共享内存的非阻塞内存事务。给定组中指定的一组线程,memcpy_async将通过单个流水线阶段移动指定数量的字节或输入类型的元素。此外,为了在使用memcpy_asyncAPI 时获得最佳性能,共享内存和全局内存都需要按 16 字节对齐。需要特别注意的是,虽然在一般情况下这是一个 memcpy,但只有当源为全局内存、目标为共享内存,并且两者都能够以 16、8 或 4 字节对齐进行寻址时,它才是异步的。异步复制的数据只能在 wait 或 wait_prior 调用表明相应阶段已完成向共享内存移动数据之后读取。

必须等待所有未完成的请求可能会损失一些灵活性(但会使操作更简单)。为了高效地重叠数据传输与执行,能够在等待并处理请求N的同时发起一个N+1memcpy_async请求非常重要。为此,请使用memcpy_async,并使用基于阶段的集合wait_priorAPI 等待它。更多详细信息请参阅wait 和 wait_prior。

用法 1

template <typename TyGroup, typename TyElem, typename TyShape>
void memcpy_async(
  const TyGroup &group,
  TyElem *__restrict__ _dst,
  const TyElem *__restrict__ _src,
  const TyShape &shape
);

执行``shape`` 字节的复制。

用法 2

template <typename TyGroup, typename TyElem, typename TyDstLayout, typename TySrcLayout>
void memcpy_async(
  const TyGroup &group,
  TyElem *__restrict__ dst,
  const TyDstLayout &dstLayout,
  const TyElem *__restrict__ src,
  const TySrcLayout &srcLayout
);

执行``min(dstLayout, srcLayout)`` 个元素的复制。如果布局的类型为cuda::aligned_size_t<N>,则两者必须指定相同的对齐值。

勘误在 CUDA 11.1 中引入的、同时接受 src 和 dst 输入布局的memcpy_asyncAPI 要求以元素而非字节为单位提供布局。元素类型从TyElem推断得出,其大小为sizeof(TyElem)。如果使用cuda::aligned_size_t<N>类型作为布局,则指定的元素数乘以sizeof(TyElem)必须是 N 的倍数,并且建议使用std::byte或char作为元素类型。

如果指定的复制形状或布局的类型为cuda::aligned_size_t<N>,则将保证对齐值至少为min(16, N)。在这种情况下,dst和src指针都需要按 N 字节对齐,并且复制的字节数需要是 N 的倍数。

代码生成要求:计算能力最低为 5.0,异步性要求计算能力为 8.0,C++11

需要包含cooperative_groups/memcpy_async.h头文件。

示例:

/// This example streams elementsPerThreadBlock worth of data from global memory
/// into a limited sized shared memory (elementsInShared) block to operate on.
#include <cooperative_groups.h>
#include <cooperative_groups/memcpy_async.h>

namespace cg = cooperative_groups;

__global__ void kernel(int* global_data) {
    cg::thread_block tb = cg::this_thread_block();
    const size_t elementsPerThreadBlock = 16 * 1024;
    const size_t elementsInShared = 128;
    __shared__ int local_smem[elementsInShared];

    size_t copy_count;
    size_t index = 0;
    while (index < elementsPerThreadBlock) {
        cg::memcpy_async(tb, local_smem, elementsInShared, global_data + index, elementsPerThreadBlock - index);
        copy_count = min(elementsInShared, elementsPerThreadBlock - index);
        cg::wait(tb);
        // Work with local_smem
        index += copy_count;
    }
}

5.6.3.2.2. wait和wait_prior

template <typename TyGroup>
void wait(TyGroup & group);

template <unsigned int NumStages, typename TyGroup>
void wait_prior(TyGroup & group);

wait和wait_prior集合操作允许等待 memcpy_async 复制完成。wait会阻塞调用线程,直到所有先前的复制完成。wait_prior允许最近的 NumStages 个阶段仍未完成,并等待所有更早的请求。因此,在总共请求了N次复制的情况下,它会等待,直到前N-NumStages次复制完成,而最后NumStages次复制可能仍在进行中。wait和wait_prior都会使指定的组同步。

代码生成要求:计算能力最低为 5.0,异步性要求计算能力为 8.0,C++11

需要包含cooperative_groups/memcpy_async.h头文件。

示例:

/// This example streams elementsPerThreadBlock worth of data from global memory
/// into a limited sized shared memory (elementsInShared) block to operate on in
/// multiple (two) stages. As stage N is kicked off, we can wait on and operate on stage N-1.
#include <cooperative_groups.h>
#include <cooperative_groups/memcpy_async.h>

namespace cg = cooperative_groups;

__global__ void kernel(int* global_data) {
    cg::thread_block tb = cg::this_thread_block();
    const size_t elementsPerThreadBlock = 16 * 1024 + 64;
    const size_t elementsInShared = 128;
    __align__(16) __shared__ int local_smem[2][elementsInShared];
    int stage = 0;
    // First kick off an extra request
    size_t copy_count = elementsInShared;
    size_t index = copy_count;
    cg::memcpy_async(tb, local_smem[stage], elementsInShared, global_data, elementsPerThreadBlock - index);
    while (index < elementsPerThreadBlock) {
        // Now we kick off the next request...
        cg::memcpy_async(tb, local_smem[stage ^ 1], elementsInShared, global_data + index, elementsPerThreadBlock - index);
        // ... but we wait on the one before it
        cg::wait_prior<1>(tb);

        // Its now available and we can work with local_smem[stage] here
        // (...)
        //

        // Calculate the amount of data that was actually copied, for the next iteration.
        copy_count = min(elementsInShared, elementsPerThreadBlock - index);
        index += copy_count;

        // A cg::sync(tb) might be needed here depending on whether
        // the work done with local_smem[stage] can release threads to race ahead or not
        // Wrap to the next stage
        stage ^= 1;
    }
    cg::wait(tb);
    // The last local_smem[stage] can be handled here
}

5.6.3.3. cooperative_groups/partition.h

5.6.3.3.1. tiled_partition

template <unsigned int Size, typename ParentT>
thread_block_tile<Size, ParentT> tiled_partition(const ParentT& g);
thread_group tiled_partition(const thread_group& parent, unsigned int tilesz);

tiled_partition方法是一种集体操作,它将父组划分为一维、行优先排列的子组分块。总共将创建 ((size(parent)/tilesz) 个子组,因此父组的大小必须能被Size整除。允许的父组为thread_block或thread_block_tile。

实现可能会使调用线程等待,直到父组的所有成员都已调用该操作后才恢复执行。功能仅限于原生硬件大小 1/2/4/8/16/32,并且cg::size(parent)必须大于Size参数。tiled_partition的模板版本也支持 64/128/256/512 这些大小,但在计算能力为 7.5 或更低的设备上需要执行一些额外步骤,详情请参阅thread_block_tile 类。

代码生成要求:计算能力最低为 5.0,大小大于 32 时要求使用 C++11

5.6.3.3.2. labeled_partition

template <typename Label>
coalesced_group labeled_partition(const coalesced_group& g, Label label);
template <unsigned int Size, typename Label>
coalesced_group labeled_partition(const thread_block_tile<Size>& g, Label label);

labeled_partition方法是一种集体操作,它将父组划分为一维子组,各子组内的线程处于合并状态。实现将对条件标签求值,并将标签值相同的线程分配到同一个组中。

Label可以是任意整数类型。

实现可能会使调用线程等待,直到父组的所有成员都已调用该操作后才恢复执行。

注意:此功能仍在评估中,将来可能会略有变化。

代码生成要求:计算能力最低为 7.0,C++11

5.6.3.3.3. binary_partition

coalesced_group binary_partition(const coalesced_group& g, bool pred);
template <unsigned int Size>
coalesced_group binary_partition(const thread_block_tile<Size>& g, bool pred);

binary_partition()方法是一种集体操作,它将父组划分为一维子组,各子组内的线程处于合并状态。实现将对谓词求值,并将值相同的线程分配到同一个组中。这是labeled_partition()的一种特化形式,其中标签只能为 0 或 1。

实现可能会使调用线程等待,直到父组的所有成员都已调用该操作后才恢复执行。

示例:

/// This example divides a 32-sized tile into a group with odd
/// numbers and a group with even numbers
_global__ void oddEven(int *inputArr) {
    auto block = cg::this_thread_block();
    auto tile32 = cg::tiled_partition<32>(block);

    // inputArr contains random integers
    int elem = inputArr[block.thread_rank()];
    // after this, tile32 is split into 2 groups,
    // a subtile where elem&1 is true and one where its false
    auto subtile = cg::binary_partition(tile32, (elem & 1));
}

5.6.3.4. cooperative_groups/reduce.h

5.6.3.4.1. Reduce运算符

以下是可使用reduce执行的一些基本操作的函数对象原型。

namespace cooperative_groups {
  template <typename Ty>
  struct cg::plus;

  template <typename Ty>
  struct cg::less;

  template <typename Ty>
  struct cg::greater;

  template <typename Ty>
  struct cg::bit_and;

  template <typename Ty>
  struct cg::bit_xor;

  template <typename Ty>
  struct cg::bit_or;
}

归约受限于实现在编译时可获得的信息。因此,为了利用计算能力 8.0 中引入的内建函数,cg::命名空间提供了几个与硬件相对应的函数对象。除less/greater之外,这些对象与 C++ STL 中提供的对象看起来类似。与 STL 存在任何差异的原因在于,这些函数对象在设计上旨在实际对应硬件内建函数的操作。

功能描述:

  • cg::plus:接受两个值,并使用 operator+ 返回两者之和。

  • cg::less:接受两个值,并使用 operator< 返回较小的值。其不同之处在于,返回的是较小的值,而不是布尔值。

  • cg::greater:接受两个值,并使用 operator< 返回较大的值。其不同之处在于,返回的是较大的值,而不是布尔值。

  • cg::bit_and:接受两个值,并返回 operator& 的结果。

  • cg::bit_xor:接受两个值,并返回 operator^ 的结果。

  • cg::bit_or:接受两个值,并返回 operator| 的结果。

示例:

{
    // cg::plus<int> is specialized within cg::reduce and calls __reduce_add_sync(...) on CC 8.0+
    cg::reduce(tile, (int)val, cg::plus<int>());

    // cg::plus<float> fails to match with an accelerator and instead performs a standard shuffle based reduction
    cg::reduce(tile, (float)val, cg::plus<float>());

    // While individual components of a vector are supported, reduce will not use hardware intrinsics for the following
    // It will also be necessary to define a corresponding operator for vector and any custom types that may be used
    int4 vec = {...};
    cg::reduce(tile, vec, cg::plus<int4>())

    // Finally lambdas and other function objects cannot be inspected for dispatch
    // and will instead perform shuffle based reductions using the provided function object.
    cg::reduce(tile, (int)val, [](int l, int r) -> int {return l + r;});
}

5.6.3.4.2. reduce

template <typename TyGroup, typename TyArg, typename TyOp>
auto reduce(const TyGroup& group, TyArg&& val, TyOp&& op) -> decltype(op(val, val));

reduce对传入组中指定的各线程提供的数据执行归约操作。它利用硬件加速(在 compute 80 及更高版本的设备上)执行算术加法、最小值或最大值运算,以及逻辑与、或或异或运算,同时也在较早代的硬件上提供软件回退实现。只有 4B 类型可以获得硬件加速。

group:有效的组类型为coalesced_group和thread_block_tile。

val:满足以下要求的任意类型:

  • 满足可平凡复制的条件,即is_trivially_copyable<TyArg>::value == true

  • 对于coalesced_group和大小小于或等于 32 的分块,要求sizeof(T) <= 32;对于更大的分块,要求sizeof(T) <= 8

  • 具有适用于给定函数对象的算术运算符或比较运算符。

注意:组中的不同线程可以为此参数传递不同的值。

op:可为整数类型提供硬件加速的有效函数对象为plus(), less(), greater(), bit_and(), bit_xor(), bit_or()。这些对象必须被构造,因此需要 TyVal 模板实参,即plus<int>()。归约也支持可使用operator()调用的 lambda 表达式和其他函数对象。

异步归约

template <typename TyGroup, typename TyArg, typename TyAtomic, typename TyOp>
void reduce_update_async(const TyGroup& group, TyAtomic& atomic, TyArg&& val, TyOp&& op);

template <typename TyGroup, typename TyArg, typename TyAtomic, typename TyOp>
void reduce_store_async(const TyGroup& group, TyAtomic& atomic, TyArg&& val, TyOp&& op);

template <typename TyGroup, typename TyArg, typename TyOp>
void reduce_store_async(const TyGroup& group, TyArg* ptr, TyArg&& val, TyOp&& op);

API 的*_async变体异步计算结果,由其中一个参与线程将结果存储到指定目标或用于更新指定目标,而不是由每个线程返回结果。要观察这些异步调用的效果,调用这些操作的线程组或包含这些线程的更大的组需要进行同步。

  • 对于原子存储或更新变体,atomic参数可以是CUDA C++ Standard Library中提供的cuda::atomic或cuda::atomic_ref。此 API 变体仅可在 CUDA C++ Standard Library 支持这些类型的平台和设备上使用。归约结果用于根据指定的op以原子方式更新原子对象,例如,对于cg::plus(),结果会以原子方式加到原子对象上。atomic所持有的类型必须与TyArg的类型匹配。原子对象的作用域必须包含组中的所有线程,如果多个组并发使用同一个原子对象,则作用域必须包含使用该原子对象的所有组中的所有线程。原子更新以宽松内存顺序执行。

  • 对于指针存储变体,归约结果将以弱存储方式存储到dst指针所指的位置。

5.6.3.5. cooperative_groups/scan.h

5.6.3.5.1. inclusive_scan和exclusive_scan

template <typename TyGroup, typename TyVal, typename TyFn>
auto inclusive_scan(const TyGroup& group, TyVal&& val, TyFn&& op) -> decltype(op(val, val));

template <typename TyGroup, typename TyVal>
TyVal inclusive_scan(const TyGroup& group, TyVal&& val);

template <typename TyGroup, typename TyVal, typename TyFn>
auto exclusive_scan(const TyGroup& group, TyVal&& val, TyFn&& op) -> decltype(op(val, val));

template <typename TyGroup, typename TyVal>
TyVal exclusive_scan(const TyGroup& group, TyVal&& val);

inclusive_scan和exclusive_scan对传入组中指定的各线程提供的数据执行扫描操作。对于exclusive_scan,每个线程的结果是对thread_rank小于该线程的那些线程的数据进行归约所得的结果。inclusive_scan的结果还在归约中包含调用线程的数据。

group:有效的组类型为coalesced_group和thread_block_tile。

val:满足以下要求的任意类型:

  • 满足可平凡复制的条件,即is_trivially_copyable<TyArg>::value == true

  • 对于coalesced_group和大小小于或等于 32 的分块,要求sizeof(T) <= 32;对于更大的分块,要求sizeof(T) <= 8

  • 具有适用于给定函数对象的算术运算符或比较运算符。

注意:组中的不同线程可以为此参数传递不同的值。

op:为方便使用而定义的函数对象是cooperative_groups/reduce.h中描述的plus(), less(), greater(), bit_and(), bit_xor(), bit_or()。这些对象必须被构造,因此需要 TyVal 模板实参,即plus<int>()。inclusive_scan和exclusive_scan也支持可使用operator()调用的 lambda 表达式和其他函数对象。不带此参数的重载使用cg::plus<TyVal>()。

扫描更新

template <typename TyGroup, typename TyAtomic, typename TyVal, typename TyFn>
auto inclusive_scan_update(const TyGroup& group, TyAtomic& atomic, TyVal&& val, TyFn&& op) -> decltype(op(val, val));

template <typename TyGroup, typename TyAtomic, typename TyVal>
TyVal inclusive_scan_update(const TyGroup& group, TyAtomic& atomic, TyVal&& val);

template <typename TyGroup, typename TyAtomic, typename TyVal, typename TyFn>
auto exclusive_scan_update(const TyGroup& group, TyAtomic& atomic, TyVal&& val, TyFn&& op) -> decltype(op(val, val));

template <typename TyGroup, typename TyAtomic, typename TyVal>
TyVal exclusive_scan_update(const TyGroup& group, TyAtomic& atomic, TyVal&& val);

*_scan_update集体操作接受一个额外的参数atomic,该参数可以是CUDA C++ Standard Library中提供的cuda::atomic或cuda::atomic_ref。这些 API 变体仅可在 CUDA C++ Standard Library 支持这些类型的平台和设备上使用。这些变体将根据op,使用组中所有线程的输入值之和对atomic执行更新。每个线程都会将atomic的先前值与扫描结果组合并返回。atomic所持有的类型必须与TyVal的类型匹配。原子对象的作用域必须包含组中的所有线程,如果多个组并发使用同一个原子对象,则作用域必须包含使用该原子对象的所有组中的所有线程。原子更新以宽松内存顺序执行。

以下伪代码说明了扫描的更新变体如何工作:

/*
 inclusive_scan_update behaves as the following block,
 except both reduce and inclusive_scan is calculated simultaneously.
auto total = reduce(group, val, op);
TyVal old;
if (group.thread_rank() == selected_thread) {
    atomically {
        old = atomic.load();
        atomic.store(op(old, total));
    }
}
old = group.shfl(old, selected_thread);
return op(inclusive_scan(group, val, op), old);
*/

需要包含cooperative_groups/scan.h头文件。

使用 exclusive_scan 进行流压缩的示例:

#include <cooperative_groups.h>
#include <cooperative_groups/scan.h>
namespace cg = cooperative_groups;

// put data from input into output only if it passes test_fn predicate
template<typename Group, typename Data, typename TyFn>
__device__ int stream_compaction(Group &g, Data *input, int count, TyFn&& test_fn, Data *output) {
    int per_thread = count / g.num_threads();
    int thread_start = min(g.thread_rank() * per_thread, count);
    int my_count = min(per_thread, count - thread_start);

    // get all passing items from my part of the input
    //  into a contagious part of the array and count them.
    int i = thread_start;
    while (i < my_count + thread_start) {
        if (test_fn(input[i])) {
            i++;
        }
        else {
            my_count--;
            input[i] = input[my_count + thread_start];
        }
    }

    // scan over counts from each thread to calculate my starting
    //  index in the output
    int my_idx = cg::exclusive_scan(g, my_count);

    for (i = 0; i < my_count; ++i) {
        output[my_idx + i] = input[thread_start + i];
    }
    // return the total number of items in the output
    return g.shfl(my_idx + my_count, g.num_threads() - 1);
}

使用 exclusive_scan_update 进行动态缓冲区空间分配的示例:

#include <cooperative_groups.h>
#include <cooperative_groups/scan.h>
namespace cg = cooperative_groups;

// Buffer partitioning is static to make the example easier to follow,
// but any arbitrary dynamic allocation scheme can be implemented by replacing this function.
__device__ int calculate_buffer_space_needed(cg::thread_block_tile<32>& tile) {
    return tile.thread_rank() % 2 + 1;
}

__device__ int my_thread_data(int i) {
    return i;
}

__global__ void kernel() {
    __shared__ extern int buffer[];
    __shared__ cuda::atomic<int, cuda::thread_scope_block> buffer_used;

    auto block = cg::this_thread_block();
    auto tile = cg::tiled_partition<32>(block);
    buffer_used = 0;
    block.sync();

    // each thread calculates buffer size it needs
    int buf_needed = calculate_buffer_space_needed(tile);

    // scan over the needs of each thread, result for each thread is an offset
    // of that thread’s part of the buffer. buffer_used is atomically updated with
    // the sum of all thread's inputs, to correctly offset other tile’s allocations
    int buf_offset =
        cg::exclusive_scan_update(tile, buffer_used, buf_needed);

    // each thread fills its own part of the buffer with thread specific data
    for (int i = 0 ; i < buf_needed ; ++i) {
        buffer[buf_offset + i] = my_thread_data(i);
    }

    block.sync();
    // buffer_used now holds total amount of memory allocated
    // buffer is {0, 0, 1, 0, 0, 1 ...};
}

5.6.3.6. cooperative_groups/sync.h

5.6.3.6.1. barrier_arrive和barrier_wait

T::arrival_token T::barrier_arrive();
void T::barrier_wait(T::arrival_token&&);

barrier_arrive和barrier_wait成员函数提供了一个类似于cuda::barrier的同步 API(了解更多)。协作组会自动初始化组屏障,但由于这些操作具有集体性质,到达和等待操作还有一项额外限制:组中的所有线程都必须在每个阶段到达屏障一次并等待屏障一次。当使用某个组调用barrier_arrive时,对该组调用任何集体操作或再次到达屏障的结果都是未定义的,直到通过barrier_wait调用观察到屏障阶段完成。在barrier_wait上阻塞的线程可能会在其他线程调用barrier_wait之前从同步中释放,但只能在组中的所有线程都已调用barrier_arrive之后释放。组类型T可以是任意一种隐式组。这允许线程在到达之后、等待同步完成之前执行独立的工作,从而隐藏一部分同步延迟。barrier_arrive返回一个arrival_token对象,该对象必须传递给相应的barrier_wait。令牌会以这种方式被消耗,不能用于另一次barrier_wait调用。

使用 barrier_arrive 和 barrier_wait 在整个簇中同步共享内存初始化的示例:

#include <cooperative_groups.h>

using namespace cooperative_groups;

void __device__ init_shared_data(const thread_block& block, int *data);
void __device__ local_processing(const thread_block& block);
void __device__ process_shared_data(const thread_block& block, int *data);

__global__ void cluster_kernel() {
    extern __shared__ int array[];
    auto cluster = this_cluster();
    auto block   = this_thread_block();

    // Use this thread block to initialize some shared state
    init_shared_data(block, &array[0]);

    auto token = cluster.barrier_arrive(); // Let other blocks know this block is running and data was initialized

    // Do some local processing to hide the synchronization latency
    local_processing(block);

    // Map data in shared memory from the next block in the cluster
    int *dsmem = cluster.map_shared_rank(&array[0], (cluster.block_rank() + 1) % cluster.num_blocks());

    // Make sure all other blocks in the cluster are running and initialized shared data before accessing dsmem
    cluster.barrier_wait(std::move(token));

    // Consume data in distributed shared memory
    process_shared_data(block, dsmem);
    cluster.sync();
}

5.6.3.6.2. sync

static void T::sync();

template <typename T>
void sync(T& group);

sync对组中指定的线程进行同步。组类型T可以是任意现有的组类型,因为它们都支持同步。它既可以作为每种组类型的成员函数使用,也可以作为以组为参数的自由函数使用。

5.6.3.6.2.1. 网格同步

在引入协作组之前,CUDA 编程模型只允许在核函数完成边界处在线程块之间进行同步。核函数边界会伴随隐式的状态失效,并因此可能带来性能影响。

例如,在某些用例中,应用程序具有大量小型核函数,每个核函数代表处理流水线中的一个阶段。当前 CUDA 编程模型要求存在这些核函数,以确保在下一流水线阶段工作的线程块准备好使用数据之前,当前流水线阶段的线程块已经生成数据。在这种情况下,提供全局线程块间同步的能力将允许重构应用程序,使其具有持久线程块,这些线程块能够在给定阶段完成时在设备上进行同步。

要在核函数内部对整个网格进行同步,只需使用grid.sync()函数:

grid_group grid = this_grid();
grid.sync();

而在启动核函数时,必须使用cudaLaunchCooperativeKernelCUDA 运行时启动 API 或CUDA driver equivalent,而不是<<<...>>>执行配置语法。

示例:

为了保证线程块同时驻留在 GPU 上,需要仔细考虑启动的线程块数量。例如,可以按如下方式启动与 SM 数量相同的线程块:

int dev = 0;
cudaDeviceProp deviceProp;
cudaGetDeviceProperties(&deviceProp, dev);
// initialize, then launch
cudaLaunchCooperativeKernel((void*)my_kernel, deviceProp.multiProcessorCount, numThreads, args);

或者,可以按如下方式使用占用率计算器,计算每个 SM 可以同时容纳多少个线程块,从而最大限度地提高可用并行度:

/// This will launch a grid that can maximally fill the GPU, on the default stream with kernel arguments
int numBlocksPerSm = 0;
 // Number of threads my_kernel will be launched with
int numThreads = 128;
cudaDeviceProp deviceProp;
cudaGetDeviceProperties(&deviceProp, dev);
cudaOccupancyMaxActiveBlocksPerMultiprocessor(&numBlocksPerSm, my_kernel, numThreads, 0);
// launch
void *kernelArgs[] = { /* add kernel args */ };
dim3 dimBlock(numThreads, 1, 1);
dim3 dimGrid(deviceProp.multiProcessorCount*numBlocksPerSm, 1, 1);
cudaLaunchCooperativeKernel((void*)my_kernel, dimGrid, dimBlock, kernelArgs);

一种良好的做法是,首先查询设备属性cudaDevAttrCooperativeLaunch,以确保设备支持协作式启动:

int dev = 0;
int supportsCoopLaunch = 0;
cudaDeviceGetAttribute(&supportsCoopLaunch, cudaDevAttrCooperativeLaunch, dev);

如果设备 0 支持该属性,此操作会将supportsCoopLaunch设置为 1。仅支持计算能力为 6.0 及更高的设备。此外,需要在以下任一环境下运行:

  • 不使用 MPS 的 Linux 平台

  • 使用 MPS 的 Linux 平台,且设备的计算能力为 7.0 或更高

  • 最新的 Windows 平台

5.6.4. CUDA 设备运行时

CUDA 设备运行时是在核函数代码内可用的 API,提供了许多与主机上的 CUDA 运行时 API 相同的能力。这些 API 最常用于CUDA 动态并行或设备图启动的情境中。

5.6.4.1. 在 CUDA 代码中包含设备运行时 API

与主机端运行时 API 类似,CUDA 设备运行时 API 的原型会在程序编译期间自动包含。无需显式包含cuda_device_runtime_api.h。

5.6.4.2. CUDA 设备运行时中的内存

5.6.4.2.1. 配置选项

设备运行时系统软件的资源分配由主机程序通过cudaDeviceSetLimit()API 进行控制。必须在启动任何核函数之前设置限制,并且在 GPU 正在运行程序时不得更改这些限制。

可以设置以下命名限制:

限制

行为

cudaLimitDevRuntimePendingLaunchCount

控制为缓冲尚未开始执行的核函数启动和事件而预留的内存量,这些核函数启动和事件尚未开始执行是由于依赖关系尚未解决或执行资源不足。当缓冲区已满时,在设备端核函数启动期间尝试分配启动槽会失败并返回cudaErrorLaunchOutOfResources,而尝试分配事件槽会失败并返回cudaErrorMemoryAllocation。启动槽的默认数量为 2048。应用程序可以通过设置cudaLimitDevRuntimePendingLaunchCount来增加启动槽和/或事件槽的数量。分配的事件槽数量是该限制值的两倍。

cudaLimitStackSize

控制每个 GPU 线程的栈大小,以字节为单位。CUDA 驱动程序会在每次核函数启动时根据需要自动增大每个线程的栈大小。每次启动后,此大小不会重置为原始值。要将每个线程的栈大小设置为不同的值,可以调用cudaDeviceSetLimit()来设置此限制。栈的大小将立即调整,如有必要,设备将阻塞,直到先前请求的所有任务都完成。可以调用cudaDeviceGetLimit()来获取当前每个线程的栈大小。

5.6.4.2.2. 分配与生存期

cudaMalloc()和cudaFree()在主机环境与设备环境中具有不同的语义。从主机调用时,cudaMalloc()会从未使用的设备内存中分配一个新区域。从设备运行时调用时,这些函数映射到设备端的malloc()和free()。这意味着,在设备环境中,可分配的内存总量受限于设备malloc()堆的大小,该大小可能小于可用的未使用设备内存。此外,在主机程序中对在设备上通过cudaMalloc()分配的指针调用cudaFree()是错误的,反之亦然。

主机上的cudaMalloc()

设备上的cudaMalloc()

主机上的cudaFree()

支持

不支持

设备上的cudaFree()

不支持

支持

分配限制

可用设备内存

cudaLimitMallocHeapSize

5.6.4.2.2.1. 内存声明
5.6.4.2.2.1.1. 设备内存和常量内存

在文件作用域内使用__device__或__constant__内存空间说明符声明的内存,在使用设备运行时时行为完全相同。所有核函数都可以读取或写入设备变量,无论核函数最初是由主机还是设备运行时启动的。等价地,所有核函数对在模块作用域中声明的__constant__都具有相同的视图。

5.6.4.2.2.1.2. 纹理和表面

设备运行时不允许在设备代码内创建或销毁纹理对象或表面对象。在主机上创建的纹理对象和表面对象可以在设备上自由使用和传递。无论在何处创建,动态创建的纹理对象始终有效,并且可以从父核函数传递给子核函数。

注意

设备运行时不支持在从设备启动的核函数内使用旧式模块作用域(即计算能力 2.0 或 Fermi 风格)的纹理和表面。模块作用域(旧式)纹理可以在主机上创建,并像用于任何核函数一样用于设备代码中,但只能由顶层核函数(即从主机启动的核函数)使用。

5.6.4.2.2.1.3. 共享内存变量声明

在 CUDA C++ 中,共享内存既可以声明为大小静态确定的文件作用域变量或函数作用域变量,也可以声明为extern变量,其大小由核函数的调用方在运行时通过启动配置参数确定。这两种声明类型在设备运行时中均有效。

__global__ void permute(int n, int *data) {
   extern __shared__ int smem[];
   if (n <= 1)
       return;

   smem[threadIdx.x] = data[threadIdx.x];
   __syncthreads();

   permute_data(smem, n);
   __syncthreads();

   // Write back to GMEM since we can't pass SMEM to children.
   data[threadIdx.x] = smem[threadIdx.x];
   __syncthreads();

   if (threadIdx.x == 0) {
       permute<<< 1, 256, n/2*sizeof(int) >>>(n/2, data);
       permute<<< 1, 256, n/2*sizeof(int) >>>(n/2, data+n/2);
   }
}

void host_launch(int *data) {
    permute<<< 1, 256, 256*sizeof(int) >>>(256, data);
}
5.6.4.2.2.1.4. 常量内存

不能从设备修改常量。常量只能从主机修改,但如果有一个并发执行的网格在其生命周期中的任何时刻访问该常量,那么此时从主机修改该常量的行为是未定义的。

5.6.4.2.2.1.5. 符号地址

在核函数内,只需通过&运算符即可引用设备端符号(即标记为__device__的符号),因为所有全局作用域的设备变量都位于核函数的可见地址空间中。这也适用于__constant__符号,不过在这种情况下,指针将引用只读数据。

由于可以直接引用设备端符号,那些引用符号的 CUDA 运行时 API(例如cudaMemcpyToSymbol()或cudaGetSymbolAddress())便没有必要,设备运行时也不支持它们。这意味着,无法在正在运行的核函数内更改常量数据,即使是在子核函数启动之前也不行,因为对__constant__空间的引用是只读的。

5.6.4.3. SM ID 和线程束 ID

请注意,在 PTX 中,%smid和%warpid被定义为易变值。设备运行时可能会将线程块重新调度到不同的 SM 上,以便更高效地管理资源。因此,依赖%smid或%warpid在线程或线程块的整个生命周期内保持不变是不安全的。

5.6.4.4. 启动设置 API

设备端核函数启动介绍了从设备代码启动核函数的语法,该语法使用与主机 CUDA 运行时 API 相同的三重尖括号启动表示法。

核函数启动是一种通过设备运行时库公开的系统级机制。也可以通过cudaGetParameterBuffer()和cudaLaunchDevice() API 直接从 PTX 使用该机制。CUDA 应用程序可以自行调用这些 API,其要求与 PTX 相同。在这两种情况下,用户均需负责按照规范,以正确的格式正确填充所有必要的数据结构。这些数据结构保证向后兼容。

与主机端启动一样,设备端运算符<<<>>>会映射到底层核函数启动 API。这使得以 PTX 为目标的用户能够执行启动。NVCC 编译器前端会将<<<>>>转换为这些调用。

表 61 新增的仅限设备的启动实现函数

运行时 API 启动函数

与主机运行时行为的差异说明(如无说明,则行为相同)

cudaGetParameterBuffer

从<<<>>>自动生成。请注意,API 与对应的主机 API 不同。

cudaLaunchDevice

从<<<>>>自动生成。请注意,API 与对应的主机 API 不同。

这些启动函数的 API 与 CUDA 运行时 API 中的 API 不同,其定义如下:

extern   device   cudaError_t cudaGetParameterBuffer(void **params);
extern __device__ cudaError_t cudaLaunchDevice(void *kernel,
                                        void *params, dim3 gridDim,
                                        dim3 blockDim,
                                        unsigned int sharedMemSize = 0,
                                        cudaStream_t stream = 0);

5.6.4.5. 设备管理

设备运行时不支持多 GPU;设备运行时只能操作其当前正在执行所在的设备。不过,允许查询系统中任何支持 CUDA 的设备的属性。

5.6.4.6. API 参考

此处详细介绍了设备运行时所支持的 CUDA 运行时 API 部分。主机运行时 API 和设备运行时 API 的语法完全相同;除特别说明外,语义也相同。下表概述了这些 API 与主机端可用版本的比较。

表 62 支持的 API 函数

运行时 API 函数

详细信息

cudaDeviceGetCacheConfig

cudaDeviceGetLimit

cudaGetLastError

最近一次错误是每线程状态,而非每线程块状态

cudaPeekAtLastError

cudaGetErrorString

cudaGetDeviceCount

cudaDeviceGetAttribute

可返回任意设备的属性

cudaGetDevice

始终返回从主机端看到的当前设备 ID

cudaStreamCreateWithFlags

必须传入cudaStreamNonBlocking标志

cudaStreamDestroy

cudaStreamWaitEvent

cudaEventCreateWithFlags

必须传入cudaEventDisableTiming标志

cudaEventRecord

cudaEventDestroy

cudaFuncGetAttributes

cudaMemcpyAsync

关于所有memcpy/memset函数的注意事项:

  • 仅支持异步memcpy/set函数

  • 仅允许设备到设备的memcpy

  • 不得传入局部内存指针或共享内存指针

cudaMemcpy2DAsync

cudaMemcpy3DAsync

cudaMemsetAsync

cudaMemset2DAsync

cudaMemset3DAsync

cudaRuntimeGetVersion

cudaMalloc

不得在设备上对主机上创建的指针调用cudaFree,反之亦然

cudaFree

cudaOccupancyMaxActiveBlocksPerMultiprocessor

cudaOccupancyMaxPotentialBlockSize

cudaOccupancyMaxPotentialBlockSizeVariableSMem

5.6.4.7. API 错误和启动失败

与 CUDA 运行时通常的情况一样,任何函数都可能返回错误码。最近一次返回的错误码会被记录,并可通过cudaGetLastError()调用获取。错误按线程记录,因此每个线程都可以识别其产生的最近一次错误。错误码的类型为cudaError_t。

与主机端启动类似,设备端启动可能因多种原因(参数无效等)而失败。用户必须调用cudaGetLastError()来确定启动是否产生了错误,但启动后没有错误并不意味着子核函数已成功完成。

对于设备端异常,例如访问无效地址,子网格中的错误将返回给主机。

5.6.4.8. 设备运行时流

CUDA 设备运行时公开了一些特殊的命名流,它们为从设备启动的核函数和图提供特定行为。与设备图启动相关的命名流在设备启动中有介绍。另外两个可在 CUDA 设备运行时中用于核函数和 memcpy 操作的命名流是cudaStreamTailLaunch和cudaStreamTailLaunch。本节介绍这些命名流的具体行为。

设备运行时提供命名流和未命名(NULL)流。流句柄不能传递给父网格或子网格。换言之,应将流视为创建它的网格所私有的。

设备上不支持主机端 NULL 流的跨流屏障语义(详见下文)。为了保持与主机运行时的语义兼容性,所有设备流都必须使用cudaStreamCreateWithFlags() API 并传入cudaStreamNonBlocking标志来创建。CUDA 设备运行时中不提供cudaStreamCreate() API。

由于设备运行时不支持cudaStreamSynchronize()和cudaStreamQuery(),当应用程序需要知道在流中启动的子核函数已完成时,应改用启动到cudaStreamTailLaunch流中的核函数。

5.6.4.8.1. 隐式(NULL)流

在主机程序中,未命名(NULL)流与其他流之间具有额外的屏障同步语义(详见阻塞流、非阻塞流与默认流)。设备运行时提供一个由线程块中所有线程共享的隐式未命名流,但由于所有命名流都必须使用cudaStreamNonBlocking标志创建,启动到 NULL 流中的工作不会对其他任何流(包括其他线程块的 NULL 流)中尚未完成的工作引入隐式依赖。

5.6.4.8.2. 即发即弃流

即发即弃命名流(cudaStreamFireAndForget)允许用户使用更少的样板代码启动即发即弃工作,并且没有流跟踪开销。其功能与每次启动时创建一个新流并启动到该流中完全相同,但速度更快。

即发即弃启动会被立即调度以启动,而不依赖于先前启动的网格完成。除通过父网格结束时的隐式同步外,其他任何网格启动都不能依赖即发即弃启动的完成。因此,在父网格的即发即弃工作完成之前,尾部启动或父网格所在流中的下一个网格都不会启动。

// In this example, C2's launch will not wait for C1's completion
__global__ void P( ... ) {
   C1<<< ... , cudaStreamFireAndForget >>>( ... );
   C2<<< ... , cudaStreamFireAndForget >>>( ... );
}

即发即弃流不能用于记录事件或等待事件。尝试这样做将导致cudaErrorInvalidValue。在编译时定义了CUDA_FORCE_CDP1_IF_SUPPORTED的情况下,不支持即发即弃流。使用即发即弃流要求以 64 位模式编译。

5.6.4.8.3. 尾部启动流

尾部启动命名流(cudaStreamTailLaunch)允许网格将一个新网格调度为在自身完成后启动。在大多数情况下,应该可以使用尾部启动来实现与cudaDeviceSynchronize()相同的功能。

每个网格都有自己的尾部启动流。在尾部流开始执行之前,由网格启动的所有非尾部启动工作都会隐式同步。也就是说,父网格的尾部启动只有在父网格及其启动到普通流、每线程流或即发即弃流中的所有工作均已完成后才会启动。如果两个网格被启动到同一个网格的尾部启动流中,则后一个网格只有在前一个网格及其所有后代工作均已完成后才会启动。

// In this example, C2 will only launch after C1 completes.
__global__ void P( ... ) {
   C1<<< ... , cudaStreamTailLaunch >>>( ... );
   C2<<< ... , cudaStreamTailLaunch >>>( ... );
}

启动到尾部启动流中的网格只有在父网格的所有工作均完成后才会启动,这包括父网格在所有非尾部启动流中启动的所有其他网格(及其后代),也包括在尾部启动之后执行或启动的工作。

// In this example, C will only launch after all X, F and P complete.
__global__ void P( ... ) {
   C<<< ... , cudaStreamTailLaunch >>>( ... );
   X<<< ... , cudaStreamPerThread >>>( ... );
   F<<< ... , cudaStreamFireAndForget >>>( ... )
}

父网格所在流中的下一个网格不会在父网格的尾部启动工作完成之前启动。换言之,尾部启动流的行为就好像它被插入到其父网格与其父网格所在流中的下一个网格之间一样。

// In this example, P2 will only launch after C completes.
__global__ void P1( ... ) {
   C<<< ... , cudaStreamTailLaunch >>>( ... );
}

__global__ void P2( ... ) {
}

int main ( ... ) {
   ...
   P1<<< ... >>>( ... );
   P2<<< ... >>>( ... );
   ...
}

每个网格只有一个尾部启动流。要通过尾部启动来启动并发网格,可以采用以下示例所示的方式。

// In this example,  C1 and C2 will launch concurrently after P's completion
__global__ void T( ... ) {
   C1<<< ... , cudaStreamFireAndForget >>>( ... );
   C2<<< ... , cudaStreamFireAndForget >>>( ... );
}

__global__ void P( ... ) {
   ...
   T<<< ... , cudaStreamTailLaunch >>>( ... );
}

尾部启动流不能用于记录事件或等待事件。尝试这样做将导致cudaErrorInvalidValue。在编译时定义了CUDA_FORCE_CDP1_IF_SUPPORTED的情况下,不支持尾部启动流。使用尾部启动流要求以 64 位模式编译。

5.6.4.9. ECC 错误

CUDA 核函数内的代码无法收到 ECC 错误通知。整个启动树完成后,ECC 错误会在主机端报告。在嵌套程序执行期间出现的任何 ECC 错误,要么会产生异常,要么会继续执行(取决于错误和配置)。

原著 © 2007 至 2026 NVIDIA Corporation 及其关联公司。保留所有权利。
中文译本仅作学习参考,非 NVIDIA 官方出版物。技术含义有疑问时请核对NVIDIA 英文原文。
关于本译本

输入关键词搜索全书。