CUDA存储单元的使用数据与线程之间的对应关系

简介: CUDA存储单元的使用数据与线程之间的对应关系

引子

由于NVIDIA GPU采用的是SIMT的运行模式,CUDA编程中线程数量与数据的对应关系是什么呢?首先,我们来看一个经典的例子:

#include <stdio.h>
#define N (2048*2048)
#define THREADS_PER_BLOCK 512
__global__ void add(int *a, int *b, int *c)
{
  int index = threadIdx.x + blockIdx.x * blockDim.x;
  c[index] = a[index] + b[index];
}
int main()
{
  int *a, *b, *c;
  int *d_a, *d_b, *d_c;
  int size = N * sizeof( int );
/* allocate space for device copies of a, b, c */
  cudaMalloc( (void **) &d_a, size );
  cudaMalloc( (void **) &d_b, size );
  cudaMalloc( (void **) &d_c, size );
/* allocate space for host copies of a, b, c and setup input values */
  a = (int *)malloc( size );
  b = (int *)malloc( size );
  c = (int *)malloc( size );
  for( int i = 0; i < N; i++ )
  {
    a[i] = b[i] = i;
    c[i] = 0;
  }
  cudaMemcpy( d_a, a, size, cudaMemcpyHostToDevice );
  cudaMemcpy( d_b, b, size, cudaMemcpyHostToDevice );
  add<<< std::ceil(N / (double)THREADS_PER_BLOCK), THREADS_PER_BLOCK>>>( d_a, d_b, d_c );
  cudaDeviceSynchronize();
  cudaMemcpy( c, d_c, size, cudaMemcpyDeviceToHost);
  bool success = true;
  for( int i = 0; i < N; i++ )
  {
    if( c[i] != a[i] + b[i] )
    {
      printf("c[%d] = %d\n",i,c[i] );
      success = false;
      break;
    }
  }
  printf("%s\n", success ? "success" : "fail");
  free(a);
  free(b);
  free(c);
  cudaFree( d_a );
  cudaFree( d_b );
  cudaFree( d_c );
  return 0;
}

在这个例子中,我们创建了两个N维数组,采用了N个线程来计算,每个线程计算数组中的一个值。那么,问题来了,数组维数和线程总数是否必须一样呢?

线程与数据的对应关系

像上面这种情况,数组维数和线程总数是相等的,CUDA编程中,线程总数可否小于数组维数呢,我们将上面的程序简单的修改如下:

#include <stdio.h>
#define N (2048*2048)
#define THREADS_PER_BLOCK 512
__global__ void add(int *a, int *b, int *c)
{
  int index = threadIdx.x + blockIdx.x * blockDim.x;
  c[index] = a[index] + b[index];
}
__global__ void add_stride(int *a, int *b, int *c)
{
  int stride = N / 2;
  int index = threadIdx.x + blockIdx.x * blockDim.x;
  c[index] = a[index] + b[index];
  c[index + stride] = a[index + stride] + b[index + stride];
}
int main()
{
  int *a, *b, *c;
  int *d_a, *d_b, *d_c;
  int size = N * sizeof( int );
/* allocate space for device copies of a, b, c */
  cudaMalloc( (void **) &d_a, size );
  cudaMalloc( (void **) &d_b, size );
  cudaMalloc( (void **) &d_c, size );
/* allocate space for host copies of a, b, c and setup input values */
  a = (int *)malloc( size );
  b = (int *)malloc( size );
  c = (int *)malloc( size );
  for( int i = 0; i < N; i++ )
  {
    a[i] = b[i] = i;
    c[i] = 0;
  }
  cudaMemcpy( d_a, a, size, cudaMemcpyHostToDevice );
  cudaMemcpy( d_b, b, size, cudaMemcpyHostToDevice );
  add_stride<<< std::ceil(std::ceil(N / (double)THREADS_PER_BLOCK) / 2), THREADS_PER_BLOCK>>>( d_a, d_b, d_c );
  cudaDeviceSynchronize();
  cudaMemcpy( c, d_c, size, cudaMemcpyDeviceToHost);
  bool success = true;
  for( int i = 0; i < N; i++ )
  {
    if( c[i] != a[i] + b[i] )
    {
      printf("c[%d] = %d\n",i,c[i] );
      success = false;
      break;
    }
  }
  printf("%s\n", success ? "success" : "fail");
  free(a);
  free(b);
  free(c);
  cudaFree( d_a );
  cudaFree( d_b );
  cudaFree( d_c );
  return 0;
}

可以发现,现在block数变为了原来的一半,但每个block内的线程仍然和原来的相等,线程总数为N/2,数组维数依然为N,核函数中每个线程干的活是上一个例子中每个线程干的活的两倍:

__global__ void add_stride(int *a, int *b, int *c)
{
  int stride = N / 2;
  int index = threadIdx.x + blockIdx.x * blockDim.x;
  c[index] = a[index] + b[index];
  c[index + stride] = a[index + stride] + b[index + stride];
}

更进一步,我们将block数变为1,每个block内的线程仍然和原来的相等,线程总数为512(THREADS_PER_BLOCK),核函数如下:

__global__ void add_stride_one_block(int *a, int *b, int *c)
{
  int index = threadIdx.x;
  int stride = blockDim.x;
  for (int i = index; i < N; i += stride) {
      c[i] = a[i] + b[i];
  }
}

这里,采用stride = blockDim.x的原因是因为index = threadIdx.x的取值范围为[0, blockDim.x), 与情况一相比,每个线程要干N/stride倍的活。

综上,线程数量可以比数据数量小很多,线程与数据并没有天然的1-1对应关系,CUDA提供了线程能力,如何给线程派活,即写核函数,是程序员自己的事。

当然,上面的情况是GPU实际上有能力产生和数据数量相等的线程,我们人为的限制了线程的个数,当数据量非常大的时候,GPU单个Grid可以产生的总的线程数远小于数据维数时,怎么处理呢?

GPU线程容量小于数据维数

如果单个GPU(Grid)一次产生的线程总数小于数据维数,我们可以通过进行Grid级别的stride来解决这个问题:

add<<< std::ceil(N / (double)THREADS_PER_BLOCK), THREADS_PER_BLOCK>>>( d_a, d_b, d_c );
__global__ void add(int *a, int *b, int *c)
{
  for (int index = threadIdx.x + blockIdx.x * blockDim.x; index < N; index += blockDim.x * gridDim.x) {
      c[index] = a[index] + b[index];
  }
}

考虑到我们采用的是二维block和二维Grid,那么blockDim.x * gridDim.x就是GPU线程容量,上面的例子如果数据量不大,index没做index += blockDim.x * gridDim.x操作就可以把活干完的话,就退化到了第一个例子中的情况;如果数据量实在太大,那么,我们就再轮一遍,本质上和前一个例子差不多,只不过上个例子是一个block内的线程数不够,现在是一个Grid内的线程数不够

相关实践学习
部署Stable Diffusion玩转AI绘画(GPU云服务器)
本实验通过在ECS上从零开始部署Stable Diffusion来进行AI绘画创作,开启AIGC盲盒。
目录
相关文章
|
2月前
|
消息中间件 监控 安全
服务Down机了,线程池中的数据如何保证不丢失?
在分布式系统与高并发应用开发中,服务的稳定性和数据的持久性是两个至关重要的考量点。当服务遭遇Down机时,如何确保线程池中处理的数据不丢失,是每一位开发者都需要深入思考的问题。以下,我将从几个关键方面分享如何在这种情况下保障数据的安全与完整性。
65 2
|
1月前
|
缓存 安全 Java
使用 Java 内存模型解决多线程中的数据竞争问题
【10月更文挑战第11天】在 Java 多线程编程中,数据竞争是一个常见问题。通过使用 `synchronized` 关键字、`volatile` 关键字、原子类、显式锁、避免共享可变数据、合理设计数据结构、遵循线程安全原则和使用线程池等方法,可以有效解决数据竞争问题,确保程序的正确性和稳定性。
37 2
|
2月前
|
消息中间件 存储 Java
服务重启了,如何保证线程池中的数据不丢失?
【8月更文挑战第30天】为确保服务重启时线程池数据不丢失,可采用数据持久化(如数据库或文件存储)、使用可靠的任务队列(如消息队列或分布式任务队列系统)、状态监测与恢复机制,以及分布式锁等方式。这些方法能有效提高系统稳定性和可靠性,需根据具体需求选择合适方案并进行测试优化。
192 5
|
3月前
处理串口线程数据的函数
【8月更文挑战第4天】处理串口线程数据的函数。
29 4
|
3月前
|
数据处理 Python
解锁Python多线程编程魔法,告别漫长等待!让数据下载如飞,感受科技带来的速度与激情!
【8月更文挑战第22天】Python以简洁的语法和强大的库支持在多个领域大放异彩。尽管存在全局解释器锁(GIL),Python仍提供多线程支持,尤其适用于I/O密集型任务。通过一个多线程下载数据的例子,展示了如何使用`threading`模块创建多线程程序,并与单线程版本进行了性能对比。实验表明,多线程能显著减少总等待时间,但在CPU密集型任务上GIL可能会限制其性能提升。此案例帮助理解Python多线程的优势及其适用场景。
40 0
|
3月前
|
NoSQL Redis
Lettuce的特性和内部实现问题之在同步调用模式下,业务线程是如何拿到结果数据的
Lettuce的特性和内部实现问题之在同步调用模式下,业务线程是如何拿到结果数据的
|
5月前
|
存储 测试技术
【工作实践(多线程)】十个线程任务生成720w测试数据对系统进行性能测试
【工作实践(多线程)】十个线程任务生成720w测试数据对系统进行性能测试
57 0
【工作实践(多线程)】十个线程任务生成720w测试数据对系统进行性能测试
|
4月前
|
存储 缓存 NoSQL
架构设计篇问题之在数据割接过程中,多线程处理会导致数据错乱和重复问题如何解决
架构设计篇问题之在数据割接过程中,多线程处理会导致数据错乱和重复问题如何解决
|
4月前
|
存储 安全 Java
Java面试题:如何在Java应用中实现有效的内存优化?在多线程环境下,如何确保数据的线程安全?如何设计并实现一个基于ExecutorService的任务处理流程?
Java面试题:如何在Java应用中实现有效的内存优化?在多线程环境下,如何确保数据的线程安全?如何设计并实现一个基于ExecutorService的任务处理流程?
46 0
|
4月前
|
安全 Java 调度
Java面试题:Java内存优化、多线程安全与并发框架实战,如何在Java应用中实现内存优化?在多线程环境下,如何保证数据的线程安全?使用Java并发工具包中的哪些工具可以帮助解决并发问题?
Java面试题:Java内存优化、多线程安全与并发框架实战,如何在Java应用中实现内存优化?在多线程环境下,如何保证数据的线程安全?使用Java并发工具包中的哪些工具可以帮助解决并发问题?
63 0

相关实验场景

更多