CUDA ---- Constant Memory

本文深入探讨CUDA编程中常量内存与只读缓存的使用方法及其优化策略,包括如何高效利用常量内存与只读缓存提升性能,以及在1维Stencil计算中的应用实例。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

CONSTANT  MEMORY

constant Memory对于device来说只读但是对于host是可读可写。constant Memory和global Memory一样都位于DRAM,并且有一个独立的on-chip cache,比直接从constant Memory读取要快得多。每个SM上constant Memory cache大小限制为64KB。

constant Memory的获取方式不同于其它的GPU内存,对于constant Memory来说,最佳获取方式是warp中的32个thread获取constant Memory中的同一个地址。如果获取的地址不同的话,只能串行的服务这些获取请求了。

constant Memory使用__constant__限定符修饰变量。

constantMemory的生命周期伴随整个应用程序,并且可以被同一个grid中的thread和host中调用的API获取。因为constant Memory对device来说是可读的,所以只能在host初始化,使用下面的API:

cudaError_t cudaMemcpyToSymbol(const void *symbol, const void * src, size_t count, size_t offset, cudaMemcpyKind kind)

Implementing a 1D Stencil with Constant Memory

实现一个1维Stencil(数值分析领域的东,卷积神经网络处理图像的时候那个stencil),简单说就是计算一个多项式,系数放到constant Memory中,即y=f(x)这种东西,输入是九个点,如下:

{x − 4h, x − 3h, x − 2h, x − h, x, x + h, x + 2h, x + 3h, x + 4h}

在内存中的过程如下:

 

公式如下:

 

那么要放到constant Memory中的便是其中的c0、c1、c2 ……

因为每个thread使用九个点来计算一个点,所以可以使用shared memory来降低延迟。

__shared__ float smem[BDIM + 2 * RADIUS];

RADIUS定义了x两边点的个数,对于本例,RADIUS就是4。如下图所示,每个block需要RADIUS=4个halo(晕)左右边界:

 

#pragma unroll用来告诉编译器,自动展开循环。

__global__ void stencil_1d(float *in, float *out) {
// shared memory
__shared__ float smem[BDIM + 2*RADIUS];
// index to global memory
int idx = threadIdx.x + blockIdx.x * blockDim.x;
// index to shared memory for stencil calculatioin
int sidx = threadIdx.x + RADIUS;
// Read data from global memory into shared memory
smem[sidx] = in[idx];
// read halo part to shared memory
if (threadIdx.x < RADIUS) {
smem[sidx - RADIUS] = in[idx - RADIUS];
smem[sidx + BDIM] = in[idx + BDIM];
}
// Synchronize (ensure all the data is available)
__syncthreads();
// Apply the stencil
float tmp = 0.0f;
#pragma unroll
for (int i = 1; i <= RADIUS; i++) {
tmp += coef[i] * (smem[sidx+i] - smem[sidx-i]);
}
// Store the result
out[idx] = tmp;
}
View Code

Comparing with the Read-only Cache

Kepler系列的GPU允许使用texture pipeline作为一个global Memory只读缓存。因为这是一个独立的使用单独带宽的只读缓存,所以对带宽限制的kernel性能有很大的提升。

Kepler的每个SM有48KB大小的只读缓存,一般来说,在读地址比较分散的情况下,这个只读缓存比L1表现要好,但是在读同一个地址的时候,一般不适用这个只读缓存,只读缓存的读取粒度为32比特。

有两种方式来使用只读缓存:

  • 使用__ldg限定
  • 指定特定global Memory称为只读缓存

下面代码片段对于第一种情况:

__global__ void kernel(float* output, float* input) {
    ...
    output[idx] += __ldg(&input[idx]);
    ...
}

下面代码对应第二种情况,使用__restrict__来指定该数据的要从只读缓存中获取:

void kernel(float* output, const float* __restrict__ input) {
    ...
    output[idx] += input[idx];
}

一般使用__ldg是更好的选择。通过constant缓存存储的数据必须相对较小而且必须获取同一个地址以便获取最佳性能,相反,只读缓存则可以存放较大的数据,且不必地址一致。

下面的代码是之前stencil的翻版,使用过了只读缓存来存储系数,二者唯一的不同就是函数的声明:

__global__ void stencil_1d_read_only (float* in, float* out, const float *__restrict__ dcoef) {
// shared memory
__shared__ float smem[BDIM + 2*RADIUS];
// index to global memory
int idx = threadIdx.x + blockIdx.x * blockDim.x;
// index to shared memory for stencil calculatioin
int sidx = threadIdx.x + RADIUS;
// Read data from global memory into shared memory
smem[sidx] = in[idx];
// read halo part to shared memory
if (threadIdx.x < RADIUS) {
smem[sidx - RADIUS] = in[idx - RADIUS];
smem[sidx + BDIM] = in[idx + BDIM];
}
// Synchronize (ensure all the data is available)
__syncthreads();
// Apply the stencil
float tmp = 0.0f;
#pragma unroll
for (int i=1; i<=RADIUS; i++) {
tmp += dcoef[i]*(smem[sidx+i]-smem[sidx-i]);
}
// Store the result
out[idx] = tmp;
}
View Code

由于系数原本是存放在global Memory中的,然后读进缓存,所以在调用kernel之前,我们必须分配和初始化global Memory来存储系数,代码如下:

const float h_coef[] = {a0, a1, a2, a3, a4};
cudaMalloc((float**)&d_coef, (RADIUS + 1) * sizeof(float));
cudaMemcpy(d_coef, h_coef, (RADIUS + 1) * sizeof(float), cudaMemcpyHostToDevice);

下面是运行在TeslaK40上的结果,从中可知,使用只读缓存性能较差。

Tesla K40c array size: 16777216 (grid, block) 524288,32
3.4517ms stencil_1d(float*, float*)
3.6816ms stencil_1d_read_only(float*, float*, float const *)

总的来说,constant缓存和只读缓存对于device来说,都是只读的。二者都有大小限制,前者每个SM只能有64KB,后者则是48KB。对于读同一个地址,constant缓存表现好,只读缓存则对地址较分散的情况表现好。

The Warp Shuffle Instruction

之前我们有介绍shared Memory对于提高性能的好处,在CC3.0以上,支持了shuffle指令,允许thread直接读其他thread的寄存器值,只要两个thread在 同一个warp中,这种比通过shared Memory进行thread间的通讯效果更好,latency更低,同时也不消耗额外的内存资源来执行数据交换。

这里介绍warp中的一个概念lane,一个lane就是一个warp中的一个thread,每个lane在同一个warp中由lane索引唯一确定,因此其范围为[0,31]。在一个一维的block中,可以通过下面两个公式计算索引:

laneID = threadIdx.x % 32

warpID = threadIdx.x / 32

例如,在同一个block中的thread1和33拥有相同的lane索引1。

Variants of the Warp Shuffle Instruction

有两种设置shuffle的指令:一种针对整型变量,另一种针对浮点型变量。每种设置都包含四种shuffle指令变量。为了交换整型变量,使用过如下函数:

int __shfl(int var, int srcLane, int width=warpSize);

该函数的作用是将var的值返回给同一个warp中lane索引为srcLane的thread。可选参数width可以设置为2的n次幂,n属于[1,5]。

eg:如果shuffle指令如下:

int y = shfl(x, 3, 16);

则,thread0到thread15会获取thread3的数据x,thread16到thread31会从thread19获取数据x。

当传送到shfl的lane索引相同时,该指令会执行一次广播操作,如下所示:

 

另一种使用shuffle的形式如下:

int __shfl_up(int var, unsigned int delta, int width=warpSize)

该函数通过使用调用方的thread的lane索引减去delta来计算源thread的lane索引。这样源thread的相应数据就会返回给调用方,这样,warp中最开始delta个的thread不会改变,如下所示:

 

第三种shuffle指令形式如下:

int __shfl_down(int var, unsigned int delta, int width=warpSize)

该格式是相对__shfl_down来说的,具体形式如下图所示:

 

最后一种shuffle指令格式如下:

int __shfl_xor(int var, int laneMask, int width=warpSize)

这次不是加减操作,而是同laneMask做抑或操作,具体形式如下图所示:

 

所有这些提及的shuffle函数也都支持单精度浮点值,只需要将int换成float就行,除此外,和整型的使用方法完全一样。

 

 转载请注明来源:博客园-吉祥

 参考书:《professional cuda c programming》

NVIDIA CUDA板块:https://developer.nvidia.com/cuda-zone

CUDA在线文档:http://docs.nvidia.com/cuda/index.html#

转载于:https://www.cnblogs.com/1024incn/p/4706215.html

<think>好的,我现在需要回答用户关于CUDA设备端编程和实现细节的问题。首先,用户提到他们想了解设备端(device-side)的相关概念或实现细节,所以我需要确保覆盖CUDA中设备端编程的核心内容。 首先,我应该回忆一下CUDA的基本架构。CUDA将代码分为主机端(host)和设备端(device)。主机端在CPU上运行,而设备端在GPU上执行。设备端的函数通常用`__global__`或`__device__`修饰,分别表示核函数和设备函数。核函数由主机调用,在GPU上启动多个线程,而设备函数只能在GPU内部调用。 接下来,用户可能需要了解线程层次结构,比如线程块(block)、网格(grid)、以及线程索引的计算。这部分对于理解并行执行至关重要。例如,每个线程通过blockIdx和threadIdx来确定自己的位置,从而处理不同的数据。 然后,内存管理是设备端编程的关键部分。需要区分全局内存、共享内存、常量内存和纹理内存。共享内存是块内线程共享的,访问速度快,适合用作临时存储。而全局内存访问延迟高,但容量大。正确使用这些内存可以显著影响性能。 此外,同步机制如`__syncthreads()`的作用也需要解释,确保线程块内的线程在同步点前完成操作,避免竞态条件。原子操作也是设备端编程的一部分,用于处理多个线程对同一内存地址的访问冲突。 用户可能还关心设备端函数的限制,比如不能调用主机端的函数或使用某些库。此外,动态并行(Dynamic Parallelism)允许核函数启动其他核函数,这是CUDA的高级特性,可以提升程序结构的灵活性。 在实现细节方面,CUDA设备端代码的编译流程和PTX中间表示可能也需要提及。优化技巧如最大化内存合并访问、减少分支 divergence 等,对性能提升有帮助,但用户的问题更偏向概念,可能需要简要带过。 参考用户提供的引用,引用[2]提到了设备端的启动函数API,如`cudaLaunchDevice`,这属于动态并行的内容,需要包含在回答中。其他引用涉及安装和编程指南,可能不需要直接引用,但可以指出编程指南是重要资源。 最后,生成相关问题需要考虑用户可能的后续疑问,比如动态并行的具体应用、共享内存优化方法、原子操作的使用场景,以及设备端调试工具等。 现在需要将这些点组织成结构清晰的回答,确保每个部分都有逻辑连接,并正确引用相关文献。同时,检查是否所有数学公式和代码片段符合格式要求,比如使用$...$和$$...$$,以及代码块使用正确的语法高亮。</think>### CUDA设备端编程与实现细节 #### 1. **设备端函数与核函数** - **核函数**(`__global__`):由主机调用,在GPU上并行执行。例如: ```cuda __global__ void addKernel(int *a, int *b, int *c) { int i = threadIdx.x; c[i] = a[i] + b[i]; } ``` - **设备函数**(`__device__`):仅在GPU内部调用,无法从主机直接调用。 #### 2. **线程层次与执行模型** - **线程组织**:线程按层次结构组织为线程块(Block)和网格(Grid)。每个线程通过内置变量定位: - `blockIdx`(块索引) - `threadIdx`(线程索引) - `blockDim`(块维度) - **索引计算示例**:假设二维网格和块,全局索引为: $$x = blockIdx.x \times blockDim.x + threadIdx.x$$ $$y = blockIdx.y \times blockDim.y + threadIdx.y$$ #### 3. **内存模型** - **全局内存(Global Memory)**:GPU显存,所有线程可访问,但延迟高。 - **共享内存(Shared Memory)**:块内线程共享,速度快,需显式声明: ```cuda __shared__ float sharedBuffer[256]; ``` - **常量内存(Constant Memory)**:只读,适合频繁访问的常量数据。 - **寄存器(Registers)**:最快,每个线程私有。 #### 4. **同步与原子操作** - **同步函数**:`__syncthreads()`用于块内线程同步,确保数据一致性[^2]。 - **原子操作**:避免多线程竞争,例如原子加法: ```cuda atomicAdd(&counter, 1); ``` #### 5. **动态并行(Dynamic Parallelism)** - 允许核函数启动子核函数,需计算能力3.0及以上: ```cuda __global__ void parentKernel() { childKernel<<<1, 1>>>(); } ``` 设备端启动核函数通过`cudaLaunchDevice`等API实现。 #### 6. **限制与优化** - **设备端限制**:不能递归调用、不可使用静态变量、无法直接调用主机函数。 - **性能优化**:最大化内存合并访问、减少分支分歧(Branch Divergence)、合理使用共享内存。 #### 7. **调试与工具** - **CUDA-MEMCHECK**:检测内存错误。 - **Nsight Systems**:性能分析工具。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值