CUDA 12.4文档5 编程接口-使用CUDA运行时-初始化&设备内存

本博客参考官方文档进行介绍,全网仅此一家进行中文翻译,走过路过不要错过。

官方网址:https://docs.nvidia.com/cuda/cuda-c-programming-guide/

本文档分成多个博客进行介绍,在本人专栏中含有所有内容:

https://blog.csdn.net/qq_33345365/category_12610860.html

CUDA 12.4为2024年3月2日发表,本专栏开始书写日期2024/4/8,当时最新版本4.1

本人会维护一个总版本,一个小章节的版本,总版本会持续更新,小版本会及时的调整错误和不合理的翻译,内容大部分使用chatGPT 4翻译,部分内容人工调整


开始编辑时间:2024/4/8 ;最后编辑时间:2024/4/14

6.2 CUDA运行时

运行时是在cudart库中实现的,该库链接到应用程序,可以静态地通过cudart.lib或libcudart.a链接,也可以动态地通过cudart.dll或libcudart.so链接。需要cudart.dll和/或cudart.so进行动态链接的应用程序通常将它们作为应用程序安装包的一部分包含进来。只有在链接到相同CUDA运行时实例的组件之间传递CUDA运行时符号的地址才是安全的。

所有的入口点都以cuda为前缀。

如在++异构编程章节++ 中提到,CUDA编程模型假定一个系统由主机和设备组成,二者各自拥有独立的内存。++设备内存章节++给出了用于管理设备内存的运行时函数的概览。

++共享内存章节++ 阐述了在++线程层次章节++结构中引入的共享内存的使用,以最大化性能。

锁业内存章节的主机内存引入了与数据传输(数据在主机和设备内存之间传输)同时进行的内核执行所需要的页锁定主机内存。

++异步并行章节++执行描述了用于在系统各级别启用异步并行执行的概念和API。

++多设备系统章节++展示了编程模型如何扩展到拥有多个设备连接到同一主机的系统。

错误检查章节描述了如何适当地检查运行时生成的错误。

++调用栈章节++提到了用于管理CUDA C++调用栈的运行时函数。

++纹理和表面内存章节++介绍了纹理和表面内存空间,这提供了另一种访问设备内存的方式;它们也展示了GPU纹理硬件的一个子集。

++图形互操作性章节++介绍了运行时提供的与两个主要的图形API,OpenGL和Direct3D,进行互操作的各种函数。

6.2.1 初始化

从CUDA 12.0开始,cudaInitDevice()cudaSetDevice()调用初始化运行时和与指定设备相关联的主要上下文。如果没有这些调用,运行时将隐式地使用设备0并根据需要自我初始化以处理其他运行时API请求。在计时运行时函数调用以及解释第一次调用运行时的错误代码时,需要记住这一点。在12.0之前,cudaSetDevice()不会初始化运行时,应用程序通常使用无操作的运行时调用cudaFree(0)来将运行时初始化与其他api活动隔离(无论是为了计时还是错误处理)。

运行时为系统中的每个设备创建一个CUDA上下文(关于CUDA上下文的更多详细信息,请参见++上下文章节++ )。这个上下文是这个设备的主要上下文,并在第一个运行时函数中初始化,该函数需要在这个设备上有一个活动的上下文。它在应用程序的所有主机线程之间共享。作为这个上下文创建的一部分,如果需要(参见++实时编译章节++ ),设备代码会被实时编译并加载到设备内存中。这一切都是透明的。如果需要,例如,对于驱动API的互操作性,设备的主要上下文可以从驱动API中访问,如在++运行时和设备APIs的互操作性章节++中所述。

当主机线程调用cudaDeviceReset()时,这将销毁主机线程当前操作的设备的主要上下文(即,当前设备如在++设备选择章节++中定义)。任何具有此设备为当前设备的主机线程所做的下一个运行时函数调用将为该设备创建一个新的主要上下文。

注:CUDA接口使用的全局状态在主程序启动时初始化,在主程序终止时销毁。CUDA运行时和驱动程序无法检测这个状态是否无效,所以在程序启动或终止(在main之后)期间使用任何这些接口(隐式或显式)都会导致未定义的行为。

从CUDA 12.0开始,cudaSetDevice()现在在更改主机线程的当前设备后,将显式初始化运行时。CUDA的上一版本将新设备上的运行时初始化延迟到在cudaSetDevice()之后进行第一次运行时调用。这个变化意味着,现在检查cudaSetDevice()的返回值是否有初始化错误变得非常重要。参考手册中的错误处理和版本管理部分的运行时函数不会初始化运行时。

6.2.2 设备内存 Device Memory

如在异构编程中所述,CUDA编程模型假设一个由主机和设备组成的系统,每个设备都有自己独立的内存。内核操作设备内存,因此运行时提供了分配、回收和复制设备内存的函数,以及在主机内存和设备内存之间传输数据的函数。

设备内存可以以线性内存或CUDA数组的形式分配。

CUDA数组是为纹理获取优化的不透明内存布局。它们在Texture和Surface Memory中有描述。

线性内存在单一统一的地址空间中分配,这意味着分别分配的实体可以通过指针相互引用,例如在二叉树或链表中。地址空间的大小取决于主机系统(CPU)和使用的GPU的计算能力:

表1:线性内存地址空间

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

注:在计算能力为5.3(Maxwell)及更早的设备上,CUDA驱动程序创建了一个未提交的40位虚拟地址预留,以确保内存分配(指针)落入支持的范围内。这个预留出现为保留的虚拟内存,但在程序实际分配内存之前不会占用任何物理内存。

线性内存通常使用cudaMalloc()分配并使用cudaFree()释放,主机内存和设备内存之间的数据传输通常使用cudaMemcpy()完成。在Kernels的向量加法代码示例中,需要将向量从主机内存复制到设备内存:

python 复制代码
∕∕ Device code
__global__ void VecAdd(float* A, float* B, float* C, int N)
{
    int i = blockDim.x * blockIdx.x + threadIdx.x;
    if (i < N)
    C[i] = A[i] + B[i];
}
∕∕ Host code
int main()
{
    int N = ...;
    size_t size = N * sizeof(float);
    ∕∕ Allocate input vectors h_A and h_B in host memory
    float* h_A = (float*)malloc(size);
    float* h_B = (float*)malloc(size);
    float* h_C = (float*)malloc(size);
    ∕∕ Initialize input vectors
    ...
    ∕∕ Allocate vectors in device memory
    float* d_A;
    cudaMalloc(&d_A, size);
    float* d_B;
    cudaMalloc(&d_B, size);
    float* d_C;
    cudaMalloc(&d_C, size);
    ∕∕ Copy vectors from host memory to device memory
    cudaMemcpy(d_A, h_A, size, cudaMemcpyHostToDevice);
    cudaMemcpy(d_B, h_B, size, cudaMemcpyHostToDevice);
    ∕∕ Invoke kernel
    int threadsPerBlock = 256;
    int blocksPerGrid = (N + threadsPerBlock - 1) ∕ threadsPerBlock;
    VecAdd<<<blocksPerGrid, threadsPerBlock>>>(d_A, d_B, d_C, N);
    ∕∕ Copy result from device memory to host memory
    ∕∕ h_C contains the result in host memory
    cudaMemcpy(h_C, d_C, size, cudaMemcpyDeviceToHost);
    ∕∕ Free device memory
    cudaFree(d_A);
    cudaFree(d_B);
    cudaFree(d_C);
    ∕∕ Free host memory
    ...
}

线性内存也可以通过cudaMallocPitch()cudaMalloc3D()分配。这些函数被推荐用于二维或三维数组的分配,因为它们可以确保分配适当地填充以满足在++设备内存访问章节++ 中描述的对齐要求,因此可以确保在访问行地址或在二维数组和设备内存的其他区域之间进行复制时(使用cudaMemcpy2D()cudaMemcpy3D()函数)得到最佳的性能。返回的pitch(或stride)必须用来访问数组元素。下面的代码示例分配了一个width x height的浮点值二维数组,并展示了如何在设备代码中循环遍历数组元素:

c 复制代码
∕* Host code */
int width = 64, height = 64;
float* devPtr;
size_t pitch;
cudaMallocPitch(&devPtr, &pitch, width * sizeof(float), height);
MyKernel<<<100, 512>>>(devPtr, pitch, width, height);

∕∕ Device code
__global__ void MyKernel(float* devPtr, size_t pitch, int width, int height) 
{ 
    for (int r = 0; r < height; ++r) {
        float* row = (float*)((char*)devPtr + r * pitch);
        for (int c = 0; c < width; ++c) {
            float element = row[c];
        }
    }
}

以下代码示例分配了一个宽度x高度x深度的浮点值三维数组,并展示了如何在设备代码中循环遍历数组元素:

c 复制代码
∕∕ Host code
int width = 64, height = 64, depth = 64;
cudaExtent extent = make_cudaExtent(width * sizeof(float), height, depth);
cudaPitchedPtr devPitchedPtr;
cudaMalloc3D(&devPitchedPtr, extent);
MyKernel<<<100, 512>>>(devPitchedPtr, width, height, depth);
∕∕ Device code
__global__ void MyKernel(cudaPitchedPtr devPitchedPtr, int width, int height, int depth)
{
    char* devPtr = devPitchedPtr.ptr;
    size_t pitch = devPitchedPtr.pitch;
    size_t slicePitch = pitch * height;
    for (int z = 0; z < depth; ++z) {
        char* slice = devPtr + z * slicePitch;
        for (int y = 0; y < height; ++y) {
            float* row = (float*)(slice + y * pitch);
            for (int x = 0; x < width; ++x) {
                float element = row[x];
            }
        }
    }
}

注:为了避免分配过多的内存从而影响整个系统的性能,您可以根据问题的规模向用户请求分配参数。如果分配失败,您可以回退到其他较慢的内存类型(如cudaMallocHost()cudaHostRegister()等),或者返回一个错误告诉用户需要多少被拒绝的内存。如果您的应用程序出于某种原因无法请求分配参数,我们建议在支持的平台上使用cudaMallocManaged()。

参考手册列出了所有用于在通过cudaMalloc()分配的线性内存、通过cudaMallocPitch()或cudaMalloc3D()分配的线性内存、CUDA数组以及为在全局或常数内存空间中声明的变量分配的内存之间复制内存的各种函数。下面的代码示例展示了通过运行时API访问全局变量的各种方法:

c 复制代码
__constant__ float constData[256];
float data[256];
cudaMemcpyToSymbol(constData, data, sizeof(data));
cudaMemcpyFromSymbol(data, constData, sizeof(data));
__device__ float devData;
float value = 3.14f;
cudaMemcpyToSymbol(devData, &value, sizeof(float));
__device__ float* devPointer;
float* ptr;
cudaMalloc(&ptr, 256 * sizeof(float));
cudaMemcpyToSymbol(devPointer, &ptr, sizeof(ptr));

cudaGetSymbolAddress()用于检索指向为在全局内存空间中声明的变量分配的内存的地址。通过cudaGetSymbolSize()获取分配的内存的大小。

6.2.3 设备内存L2访问管理

当一个CUDA核重复访问全局内存中的数据区域时,这种数据访问可以被认为是持久的。另一方面,如果数据只被访问一次,那么这种数据访问可以被认为是流式的。

从CUDA 11.0开始,计算能力8.0和以上的设备具有影响L2缓存中数据持久性的能力,可能提供更高带宽和更低延迟的全局内存访问。

6.2.3.1 为持久访存设置的L2缓存预留 L2 cache Set-Aside for Persisting Accesses

可以设置L2缓存的一部分用于持久化访问全局内存的数据。持久化访问优先使用这部分预留的L2缓存,而正常或流式访问全局内存只能在持久化访问未使用此部分L2缓存时才能使用它。

持久访问的L2缓存预留大小可以在限制内进行调整:

c 复制代码
cudaGetDeviceProperties(&prop, device_id);
size_t size = min(int(prop.l2CacheSize * 0.75), prop.persistingL2CacheMaxSize);
cudaDeviceSetLimit(cudaLimitPersistingL2CacheSize, size); ∕* set-aside 3∕4 of L2 cache, for persisting accesses or the max allowed*∕

当GPU配置为多实例GPU(MIG)模式时,L2缓存预留功能将被禁用。

使用多进程服务(MPS)时,不能通过cudaDeviceSetLimit来改变L2缓存预留大小。相反,预留大小只能在MPS服务器启动时通过环境变量CUDA_DEVICE_DEFAULT_PERSISTING_L2_CACHE_PERCENTAGE_LIMIT来指定。

6.2.3.2 持节访存的L2策略 L2 Policy for Persisting Accesses

访问策略窗口指定了连续的全局内存区域以及该区域内访问的L2缓存的持久性属性。

下面的代码示例展示了如何使用CUDA Stream设置一个L2持久访问窗口。

CUDA Stream例子

c 复制代码
cudaStreamAttrValue stream_attribute; ∕∕ Stream level attributes data structure
stream_attribute.accessPolicyWindow.base_ptr = reinterpret_cast<void*>(ptr); ∕∕ Global Memory data pointer
stream_attribute.accessPolicyWindow.num_bytes = num_bytes; ∕∕ Number of bytes for persistence access.
∕∕ (Must be less than cudaDeviceProp::accessPolicyMaxWindowSize)
stream_attribute.accessPolicyWindow.hitRatio = 0.6; ∕∕ Hint for cache hit ratio
stream_attribute.accessPolicyWindow.hitProp = cudaAccessPropertyPersisting; ∕∕ Type of access property on cache hit
stream_attribute.accessPolicyWindow.missProp = cudaAccessPropertyStreaming; ∕∕ Type of access property on cache miss.
    
∕∕Set the attributes to a CUDA stream of type cudaStream_t
cudaStreamSetAttribute(stream, cudaStreamAttributeAccessPolicyWindow, &stream_
attribute);

当一个核函数随后在CUDA流中执行时,全局内存范围[ptr...ptr+num_bytes)内的内存访问比访问其他全局内存位置更可能在L2缓存中持久化。

如下面的例子所示,L2的持久化也可以为CUDA图形Kernel节点设置:

CUDA GraphKernelNode 例子:

c 复制代码
cudaKernelNodeAttrValue node_attribute; ∕∕ Kernel level attributes data structure
node_attribute.accessPolicyWindow.base_ptr = reinterpret_cast<void*>(ptr); ∕∕ Global Memory data pointer
node_attribute.accessPolicyWindow.num_bytes = num_bytes; ∕∕ Number of bytes for persistence access.
∕∕ (Must be less than cudaDeviceProp::accessPolicyMaxWindowSize)
node_attribute.accessPolicyWindow.hitRatio = 0.6; ∕∕ Hint for cache hit ratio
node_attribute.accessPolicyWindow.hitProp = cudaAccessPropertyPersisting; ∕∕ Type of access property on cache hit
node_attribute.accessPolicyWindow.missProp = cudaAccessPropertyStreaming; ∕∕ Type of access property on cache miss.
    
∕∕Set the attributes to a CUDA Graph Kernel node of type cudaGraphNode_t
cudaGraphKernelNodeSetAttribute(node, cudaKernelNodeAttributeAccessPolicyWindow, & node_attribute);

hitRatio参数可以用来指定接收hitProp属性的访问的比例。在上述两个例子中,全局内存区域[ptr...ptr+num_bytes)中的60%内存访问具有持久化属性,40%的内存访问具有流式属性。被分类为持久化(即hitProp)的特定内存访问是随机的,概率大约为hitRatio;概率分布取决于硬件架构和内存范围。

例如,如果L2预留缓存大小为16KB,而accessPolicyWindow中的num_bytes为32KB:

  • 对于hitRatio为0.5的情况,硬件将随机选择32KB窗口的16KB,标为持久化,并缓存在预留的L2缓存区域。
  • 对于hitRatio为1.0的情况,硬件将尝试将整个32KB窗口缓存在预留的L2缓存区。由于预留区域小于窗口,缓存行将会被驱逐,以保持最近使用的32KB数据的16KB在L2缓存的预留部分。

因此,hitRatio可以用来避免缓存行的抖动,总体上减少数据进出L2缓存的数量。

小于1.0的hitRatio值可以用来人为控制并发CUDA流的不同accessPolicyWindow可以在L2中缓存的数据量。例如,设置L2预留缓存大小为16KB;两个并发的内核在两个不同的CUDA流中,每个都有一个16KB的accessPolicyWindow,并且都有1.0的hitRatio值,它们可能会在竞争共享的L2资源时驱逐彼此的缓存行。然而,如果两个accessPolicyWindow的hitRatio值都为0.5,它们就不太可能驱逐自己或彼此的持久化缓存行。

6.2.3.3 L2访问属性 L2 Access Properties

为不同的全局内存数据访问定义了三种访问属性:

  1. cudaAccessPropertyStreaming:带有流式属性的内存访问不太可能在L2缓存中持久化,因为这些访问会优先被驱逐。
  2. cudaAccessPropertyPersisting:带有持久性属性的内存访问更可能在L2缓存中持久化,因为这些访问会被优先保留在L2缓存的预留部分。
  3. cudaAccessPropertyNormal:此访问属性强制将之前应用的持久访问属性重置为正常状态。来自之前CUDA核心的带有持久属性的内存访问可能会在其预期使用后长期保留在L2缓存中。这种持久化会减少后续不使用持久化属性的核函数可用的L2缓存量。使用cudaAccessPropertyNormal属性重置访问属性窗口会移除先前访问的持久化(优先保留)状态,就像先前的访问没有访问属性一样。

6.2.3.4 L2持久化例子 L2 Persistence Example

以下示例展示了如何为持久访问预留L2缓存,通过CUDA Stream在CUDA内核中使用预留的L2缓存,然后重置L2缓存。

c 复制代码
cudaStream_t stream;
cudaStreamCreate(&stream); 
∕∕ Create CUDA stream
cudaDeviceProp prop; 
∕∕ CUDA device properties variable
cudaGetDeviceProperties( &prop, device_id); 
∕∕ Query GPU properties
size_t size = min( int(prop.l2CacheSize * 0.75) , prop.persistingL2CacheMaxSize );
cudaDeviceSetLimit( cudaLimitPersistingL2CacheSize, size); 
∕∕ set-aside 3∕4 of L2 cache for persisting accesses or the max allowed
size_t window_size = min(prop.accessPolicyMaxWindowSize, num_bytes); 
∕∕ Select minimum of user defined num_bytes and max window size.
cudaStreamAttrValue stream_attribute; 
∕∕ Stream level attributes data structure
stream_attribute.accessPolicyWindow.base_ptr = reinterpret_cast<void*>(data1); 
∕∕ Global Memory data pointer
stream_attribute.accessPolicyWindow.num_bytes = window_size; 
∕∕ Number of bytes for persistence access
stream_attribute.accessPolicyWindow.hitRatio = 0.6; 
∕∕ Hint for cache hit ratio
stream_attribute.accessPolicyWindow.hitProp = cudaAccessPropertyPersisting; 
∕∕ Persistence Property
stream_attribute.accessPolicyWindow.missProp = cudaAccessPropertyStreaming; 
∕∕ Type of access property on cache miss
cudaStreamSetAttribute(stream, cudaStreamAttributeAccessPolicyWindow, &stream_
attribute); ∕∕ Set the attributes to a CUDA Stream
for(int i = 0; i < 10; i++) {
	cuda_kernelA<<<grid_size,block_size,0,stream>>>(data1); 
	∕∕ This data1 is used by a kernel multiple times
} 
∕∕ [data1 + num_bytes) benefits from L2 persistence
cuda_kernelB<<<grid_size,block_size,0,stream>>>(data1); 
∕∕ A different kernel in the same stream can also benefit

∕∕ from the persistence of data1
stream_attribute.accessPolicyWindow.num_bytes = 0; 
∕∕ Setting the window size to 0 disable it
cudaStreamSetAttribute(stream, cudaStreamAttributeAccessPolicyWindow, &stream_
attribute); ∕∕ Overwrite the access policy attribute to a CUDA Stream
cudaCtxResetPersistingL2Cache(); 
∕∕ Remove any persistent lines in L2
cuda_kernelC<<<grid_size,block_size,0,stream>>>(data2); 
∕∕ data2 can now benefit from full L2 in normal mode
6.2.3.5 重置L2访问为常规 Reset L2 Access to Normal

之前CUDA内核中的持久化L2缓存行可能会在使用后长时间保留在L2中。因此,将L2缓存重置为正常对于流式或正常内存访问使用正常优先级的L2缓存很重要。有三种方法可以将持久访问重置为正常状态。

  1. 使用访问属性cudaAccessPropertyNormal重置之前的持久化内存区域。
  2. 通过调用cudaCtxResetPersistingL2Cache()将所有持续的L2缓存行重置为正常。
  3. 最终未被触及的行会自动重置为正常。强烈不建议依赖自动重置,因为自动重置所需的时间长度不确定。
6.2.3.6 管理L2预留缓存的利用 Manage Utilization of L2 set-aside cache

在不同的CUDA流中并行执行的多个CUDA内核可能会有不同的访问策略窗口分配给他们的流。然而,L2预留缓存部分是被所有这些并发的CUDA内核共享的。因此,这一预留缓存部分的总体利用率是所有并发内核单独使用的总和。当持久访问的容量超过预留的L2缓存的容量时,将内存访问标记为持久的好处就会减少。

为了管理预留的L2缓存部分的利用率,一个应用程序必须考虑以下因素:

  • L2预留缓存的大小。
  • 可能会并发执行的CUDA内核。
  • 可能会并发执行的所有CUDA内核的访问策略窗口。
  • 当何时以及如何需要重置L2以允许正常或流式访问以等同优先级使用之前预留的L2缓存。
6.2.3.7 查询L2缓存属性 Manage Utilization of L2 set-aside cache

和L2缓存相关的属性是cudaDeviceProp结构的一部分,可以使用CUDA运行时API cudaGetDeviceProperties进行查询。

CUDA设备属性包括:

  • l2CacheSize:GPU上可用的L2缓存量。
  • persistingL2CacheMaxSize:可为持久内存访问预留的L2缓存的最大量。
  • accessPolicyMaxWindowSize:访问策略窗口的最大大小。
6.2.3.8 Control L2 Cache Set-Aside Size for Persisting Memory Access

用于持久内存访问的L2预留缓存大小是通过CUDA运行时API cudaDeviceGetLimit查询的,并使用CUDA运行时API cudaDeviceSetLimit设置为cudaLimit。设置此限制的最大值是cudaDeviceProp::persistingL2CacheMaxSize。

c 复制代码
enum cudaLimit {
	∕* other fields not shown *∕
	cudaLimitPersistingL2CacheSize
};
相关推荐
探索云原生1 天前
大模型推理指南:使用 vLLM 实现高效推理
ai·云原生·kubernetes·gpu·vllm
若石之上4 天前
DeepSpeed:PyTorch优化库,使模型分布式训练能高效使用内存和更快速
pytorch·内存·gpu·deepspeed·速度·zero
luoganttcc5 天前
ubuntu.24安装cuda
cuda
qiang425 天前
想租用显卡训练自己的网络?AutoDL保姆级使用教程(PyCharm版)
pycharm·gpu·autodl·租显卡
扫地的小何尚8 天前
NVIDIA RTX 系统上使用 llama.cpp 加速 LLM
人工智能·aigc·llama·gpu·nvidia·cuda·英伟达
藓类少女8 天前
【深度学习】使用硬件加速模型训练速度
人工智能·深度学习·分布式训练·gpu
高性能服务器9 天前
马斯克万卡集群AI数据中心引发的科技涟漪:智算数据中心挑战与机遇的全景洞察
数据中心·hpc·高性能计算·智算中心·马斯克ai数据中心·colossus·xai
centurysee10 天前
【一文搞懂】GPU硬件拓扑与传输速度
gpu·nvidia
吃肉夹馍不要夹馍10 天前
CublasLt 极简入门
cuda·cublas·gemm·cublaslt