Nvidia <cuda programming guide>文章转载

CUDA官方文档转接

参考cuda toolkit documentation

一、简介

  1. GPU将更多晶体管用于数据计算,而CPU将更多晶体管用于流管理。
  2. GPU通过用计算掩盖访存延迟,而CPU采用大容量的cache复杂的流处理

1.1 可扩展编程模型

  • 利用多核处理器如GPU的编程面临的问题是:如何透明地利用日益增长的核数

  • cuda提供了一个简单的编程模型,而又能解决上述问题的方法。

    核心分为三个关键抽象

    • 线程组织层级结构。
    • 共享内存。
    • 阻碍同步。

    保留了线程协作表达性,但是提供了自动扩展的能力。

  • 线程block可以放到任何SM上运行。

Nvidia <cuda programming guide>文章转载_第1张图片

图1. 自动扩展性

block间执行的隔离性使得 每个block并不在乎在哪个SM上运行,所以当硬件SM数量比较多时,可以同时运行多个block,当SM数量较小时,block之间可以排队等待在一个SM上运行。自动扩展性的由来

二、CUDA编程模型

2.1 kernel

2.2 线程层级结构

2.3 memory hierarchy

2.4 异构编程

三、编程接口

3.1编译模型

3.1.1 编译工作流–离线编译

  1. 分离主机代码设备代码

  2. 将设备代码编译成汇编格式(PTX code)或二进制格式(cubin object)。

  3. 修改主机代码,将<<<...>>>语法替换成必要的CUDA runtime function calls。来启动 PTX格式代码或者cubin object

  4. 剩下的C++代码要么交给其他要么交给nvcc去编译。

3.1.2 编译工作流–即时编译

  1. 系统运行时加载PTX code
  2. 被driver编译成二进制文件。
  3. 通过**CUDA Environment Variables**可以控制CUDA编译时的操作。

3.2 CUDA运行时

在cudart库中实现,要么通过cudart.liblibcudart.a或者cudart.dlllibcudart.so

连接到同一个cuda runtime instance组件之间传递cuda runtime symbol的地址才是安全的。

3.2.1 CUDA Runtime初始化

  • cuda runtime没有显式的初始化函数,当调用第一个runtime函数时才会初始化一个runtime instance

  • runtime为系统中每个设备创建一个上下文(context)

    • 这一个context被称为primary context:在第一次调用需要active context的接口时创建。
    • 此应用上所有线程共享此primary context
    • 创建primary context时,如果必要,device code被JIT编译,然后上传至device memory。
  • 以上操作是透明的,如果需要,可以通过driver API获得。

  • 当线程调用cudaDeviceReset()时,它将销毁它current work on的primary context。

    • 任何线程再在此设备上调用runtime function时会重新创建一个primary context。
  • CUDA interface使用global state:

    • global state程序启动时初始化
    • global state程序销毁时销毁
    • CUDA runtime和driver无法检测global state的可用性,任何在global state可用范围外的调用都导致Undefined behavior

Context:(具体看J.1节)

参考stackoverflow问答

  • 所有在driverAPI中的资源和动作,都包含在一个CUDA 上下文中。
  • 除了模块、纹理和表面引用,每个context拥有它们自己地址空间

3.2.2 设备内存

设备内存可以以_线性内存_和_CUDA 数组_。

CUDA 数组

cuda数组是为纹理拉取优化的不公开数据结构。

线性内存

分配在单一的统一地址空间内。可以用指针相互引用。

地址空间的大小取决于主机结构和GPU计算能力。

x86_64 (AMD64) POWER (ppc64le) ARM64
up to compute capability 5.3 (Maxwell) 40bit 40bit 40bit
compute capability 6.0 (Pascal) or newer up to 47bit up to 49bit up to 48bit

3.2.4 共享内存

shared memory比global memory更快。

3.2.6 并行执行

以下的任务可以相互间并发进行:

  • 主机上的计算
  • 设备上的计算
  • 从主机到设备内存传输
  • 从设备到主机内存传输
  • 一个设备内部内存传输
  • 设备间内存传输
3.2.6.1 主机和设备间并行

从主机的角度来看,以下操作是异步的:

  • 启动kernel
  • 一个设备内部内存传输
  • 从主机到设备的小于64KB的内存传输
  • Async结尾的内存操作函数
  • 设备内存操作
  • 设置CUDA_LAUNCH_BLOCKING变量可以使设备一时间只运行一个kernel,用于debug
  • kernel启动在有分析器时是同步的,除非支持异步分析器。
  • Async函数可能也会因为涉及not page-locked内存而同步起来。

3.2.6.2 并行kernel 执行

  • 并发的kernel必须在同一context中。
  • 能同时运行的kernel数量受GPU规格限制。
3.2.6.3 数据传输与kernel执行并行
  • 检查asyncEngineCount可以看出来是否支持异步并行。
  • 在同一设备间的内存拷贝也可以并行。
  • 主机内存必须是page-locked才能被Async函数使用。
3.2.6.4 并行数据传输
  • 数据传输设备可以并行
  • 涉及到的内存只能是page-locked。
3.2.6.5 流
  • 应用通过streams管理上述的并行操作。
  • stream就是将一系列操作(可能由不同主机线程提出)顺序执行。
  • 不同流之间的command顺序不能保证。
  • 当前置条件满足时,stream中的任务会被执行。
  • synchronize调用的成功暗示前面的命令都已经执行完了。
3.2.6.5.1 创建与销毁
  1. 一个stream通过一个stream obj定义
  2. 可以将stream obj当做一个参数传递给kernel launch或内存传输操作。
cudaStream_t stream[2];
for(int i =0 ; i < 2; i++){
    cudaStreamCreate(&stream[i]);
}
// destruction
for(int i =0;i< 2; i++){
    cudaStreamDestroy(stream[i]);
}
// cudaStreamDestroy会如此工作:立刻返回成功
// 如果还有工作在这个stream上做,会等它们做完自动返回
3.2.6.5.2 default stream

没有指定stream、或者设置stream参数为0,会被认为是默认stream。

  • 编译时--default-stream legacy会让主机所有线程共享一个默认stream,NULL Stream
    • 每个设备有一个NULL stream,被所有host threads所用。
    • 不加--default-streamflag时的默认情况
  • 编译时--default-stream per-thread,default stream是一个普通stream,并且每个线程都有自己的default stream。

null stream上的命令这样调度(设备角度上的同步)参考nvidia default_stream:

  • 当设备上所有其他stream的命令都执行完了,才能运行NULLstream上的command
  • NULL stream上必须在其他stream上的命令开始前完成。
3.2.6.5.3 显式同步

有许多显式streams间同步的方法。

cudaDeviceSynchronize()等待所有主机线程的所有stream的前置任务都完成了。

cudaStreamSynchronize()将stream作为参数,等待,直到给定stream上所有之前的命令都执行完了。

cudaStreamWaitEvent()将stream和event作为参数,event之后的调用都被阻塞了。

cudaStreamQuery()让应用可以知道一个stream中的明林是否都已经执行完了。

3.2.6.5.4 隐式同步

两个stream中的指令不可执行,当他们中某个执行在做下面的事:

  • 主机page-locked内存分配
  • 设备内存分配
  • 设备内存set,memset
  • 设备内部内存拷贝
  • NULL stream上的CUDA指令
  • a switch between the L1/shared memory configurations described in Compute Capability 3.x and Compute Capability 7.x.

支持kernel并发执行的设备上,任何需要依赖检查(判定一个streamed kernel是否完成)的操作

  • 只有当所有先前启动的所有stream中的kernel的所有block都启动运行了,才能开始执行
  • 阻塞所有接下来kernel启动,直到被检查的kernel启动完成了。

需要依赖检查的操作包括:

  • 在这个stream中所有启动需要检查的commands
  • 所有在这个stream上调用的cudaStreamQuery()

因此应用如果想要提升kernel并发的性能,需要:

  • 所有独立的操作应该在不独立的操作前完成
  • 尽可能减少同步调用。
3.2.6.5.5 掩盖操作
3.2.6.5.6 调用主机程序(callback)
  • 通过将cudaLaunchHostFunc()压入stream中,当它之前的所有命令都执行完了(kernel运行完、内存拷贝完等),它就会被调用。
  • 它之后的stream中的命令也会等cudaLaunchHostFunc()执行完后才能运行。
  • 指定的回调函数不能使用cudaAPI,否则会造成循环等待。
3.2.6.5.7 stream优先级

可以使用cudaStreamCreateWithPriority()来创建带有优先级的stream。

通过cudaDeviceGetStreamPriorityRange()可以获取优先级。

高优先级的stream会提前工作。

4. 硬件实现

NVIDIA的GPU架构是建立在可扩展的SM组上。当host上的程序调用了一个kernel,blocks of the grid会被枚举,并且分发到不同的SM上(如果SM能支撑起运算)。

  • 一个block的所有线程会在一个SM上同时执行(warp替换)。
  • 多个block可以一个SM上同时执行
    • 当有block终结,会有新的block在这个SM上启动。

4.1 SIMT 架构

  • 当一个SM获得一个block要执行时,SM将block划分成warps,并且每个warp通过_warp scheduler_单独调度。
  • 一个warp一次执行一个指令,如果warp内线程出现分支,那么warp会解决分支。
    • 处理分支只发生在warp内,warp间是独立运行的。

4.2 硬件多线程

  • 每个warp的上下文context都被SM on-chip地维护,在warp的整个生命周期。
    • 因此从一个warp的context切换到另一个没有影响。
    • warp scheduler选取已经可以运行的warp进行执行。
  • 每个SM都有一组32位的寄存器,被warps划分(partition)占有,生命周期内不释放。
  • 并行data cacheshared memoryblocks划分占有,生命周期内不释放。

以上,

SM运行一个kernel时,可同时reside、process的block数量和warp数量由下列因素决定:

  • kernel使用的寄存器数量、共享内存使用量(独享SM时理想值)。

  • 实际上SM可用的寄存器数量以及共享内存数量(现实SM的限制)。

    最后

  • SM对reside的warps和block数量限制。

计算能力(compute capability)由这些原因构成:SM对reside的warps、blocks数量的限制,以及寄存器数量、共享内存数量

  • 假如没有任何一个SM能执行kernel的一个block,那么kernel会启动失败。

为一个block分配的寄存器数量、和共享内存数量,在CUDA Occupancy Calculator中可以查看。

B.2 变量空间指示符

变量空间指示符指明了变量在设备上的位置。

  • 没有__device__ __shared__ __constant__描述的,一般都在寄存器上。

B.2.1 __device__

指明一个变量在设备上。

  • 在global内存空间。
  • 生命周期与创建它的CUDA context一样长。
  • 每个设备有不同的对象
  • 可以被grid中所有线程访问到,主机可以通过cudaGetSymbolAddress()、cudaGetSymbolSize()、cudaMemcpyToSymbol()、cudaMemcpyFromSymbol()访问。

B.2.2 __constant__

选择性地与__device__合用,描述这样的变量:

  • 在常量内存空间。
  • 与创建它的context生命周期一样长。
  • 每个设备中有不同的对象。
  • 可以被grid中所有线程访问到,主机可以通过cudaGetSymbolAddress()、cudaGetSymbolSize()、cudaMemcpyToSymbol()、cudaMemcpyFromSymbol()访问。

B.2.3 __shared__

选择性地与__device__合用,描述这样的变量:

  • 在一个block的shared memory上。
  • 与block的生命周期相同
  • 每个block有不同的对象。
  • 只有block内的线程才能访问。
  • 没有常量地址。

当在shared memory中声明一个变量作为external array例如:

extern __shared__ float shared[];
// 数组的大小在运行时确定。
// 所有以这种方式声明的,都从内存中相同地址空间开始。
// 所以这个array中展开的变量都必须通过偏移量显式管理。

B.14 原子函数

  • 原子函数执行一个read-modify-write原子操作在32-bit或64-bit的字,在global或shared内存上。

  • 只有一个线程完成了原子操作,才能被另一个线程访问。

  • 系统级原子性:当前所有线程包括CPUs和GPUs上的线程,都原子性。

    • atomicAdd_system
  • 设备级别原子性:当前设备上所有cuda线程原子性。

    • atomicAdd
  • block级原子性:当前block内部线程原子性。

    • atomicAdd_block

B.32 执行配置

  • 执行kernel时,需要传递执行配置给kernel。
  • <<>>
    • Dg: dim3类型数据,定义grid的维度
    • Db: dim3类型数据,定义block的维度
    • Ns: size_t类型数据,指明每个block运行时动态分配的shared memory字节数,补充于静态分配的字节数。
      • 可选变量,默认是0;
    • S: 是cudaStream_t类型的数据,指定关联的stream。
      • 可选变量,默认是0;

一下场景,启动一个kernel会失效:

  1. DgDb超过设备能接受的范围。
  2. Ns大于设备能分配的最大share memory。

J. Driver API

  • 这块附表是对CUDA Runtime提出的概念的补充。

  • driver API都在cuda(libcuda.so)动态链接库中实现

    • 在device driver安装时拷贝到系统。
    • 所有entry都以cu开头
  • 都是基于handle,不可避免的API:

    • 大部分Obj都被不透明的handle指代,并且可以由函数去处理这些Obj
  • Obj如下:

CUDA Driver API中可用的Obj

Obj Handle 描述
Device CUdevice cuda设备
Context CUcontext CUDA context
Module CUmodule 类似于动态链接库一样的东西
Function CUfunction kernel
Heap memory CUdeviceptr 指向device memory的指针
CUDA array CUarray 设备上不透明的一维或二维数据。
Stream CUstream 描述一个Stream的Obj
Event CUevent 描述一个Event的Obj
  • driver API必须以cuInit()初始化,在任何driver API被调用前。
    • 随后CUDA context被建立,然后attach to一个特定的设备。
    • context被当前cpu线程使用。
  • 在CUDA context内,kernel必须被主机代码显式地以PTX或二进制的形式加载。
    • 以C++写的kernel必须编译成PTX或二进制obj。

J.1 Context

  • CUDA context类似一个CPU进程。

  • 所有driver API中的资源以及动作都被包括在一个context内。

    • context销毁后资源也会被自动回收。
    • 除了module、texture、或surface引用,每个context都有自己的地址空间。
  • 一个host thread只能有一个device context current at a time

    • 当context被通过cuCtxCreate()创建时,它就变成当前调用线程的current context。
    • 当host thread调用涉及not current context的代码时,会返回CUDA_ERROR_INVALID_CONTEXT
    • 每个host thread都有a stack of current context
      • cuCtxCreate()会将新context插入栈顶。
      • cuCtxPopCurrent将context从host thread中detach出来,变成浮流context。如果stack下有context,那么它将成为Current context。
  • 每个context都会记录使用数。cuCtxCreate()创建一个count为1的context,cuCtxAttach()增加count数,cuCtxDetach()减少count数。

J.2 Module

  • module是动态加载的包of : device code、data that are output by nvcc。
  • name for 所有symbol包括functions、全局变量、texture和surface引用、都在module scope内维护。
    • 第三方库编写的module可能在同一个context中交互。

J.3 Kernel Execution

CUresult cuLaunchKernel(CUfunction f, unsigned int gridDimX, unsigned int gridDimY, unsigned int gridDimZ, unsigned int blockDimX, unsigned int blockDimY, unsigned int blockDimZ, unsigned int sharedMemBytes, CUstream hStream, void ** kernelParams,  void ** extra);
  • cuLaunchKernel启动一个kernel with 给定的执行配置。
  • 参数传递可以作为kernelParams传递。
    • kernelParams是一组指针。
    • 第n个指针内存对应第n个参数。
  • 要么作为最后一个参数extra传递。
    • 参数彼此之间设置好偏移量,保证对齐(让机器代码可以读懂)打包传递。
    • 将保存参数的buffer的指针传递过去。

参数对齐:

  1. 内置的vector type的对齐要求有表。
  2. 基本类型与主机的对齐要求一致,可以通过__alignof()获取。
    1. 只有一个例外,就是device code总是对double和long long设置对齐为两个字,而主机编译器对齐为一个字。

J.4 Runtime和Driver API之间的交互

  • runtime API和driver API可以共存。
  • 当driver API开启一个context之后,runtime API会直接使用这个。
  • runtime API初始化一个context,driver API用cuCtxGetCurrent()可以获取。
  • runtime隐式创建的context被称为_primary context_,driver API可以通过Primary Context Management方法控制。
  • 所有的function 从device到version管理章节,都可以交换使用。

你可能感兴趣的:(cuda编程,cuda)