GPU架构及异构计算介绍GPU架构以及异构计算的基本原理

简介: GPU架构及异构计算介绍GPU架构以及异构计算的基本原理

一篇文章理解CUDA架构、编程与进阶使用


本文章详细介绍了CUDA的架构和基础编程方法,并对他的进阶优化方法进行了简单介绍,以便大家对CUDA编程有一个整体的认知。


一、CUDA架构

下图为GPU硬件模型:

一块GPU包括3级:GPU、多核处理器、线程处理器

  • 一个GPU包含多个多核处理器(SM,图中的Mlultiprocessor),GPU的内存是全局内存global memory(可以被所有线程访问)
  • 一个多核处理器包含多个线程处理器,多核处理器的内存是共享内存shared memory(编程时划分好block后,一个block内的所有线程可以访问共享内存)
  • 线程处理器,最基本的计算单元,有自己的局部内存和寄存器,只能自己访问

在CUDA编程时,我们经常用到thread, block, grid,其中thread对应硬件上的线程处理器,grid对应一块GPU。而block可以由我们自定义维度,对应到硬件上,其实是由一个多核处理器中的多个线程处理器组合而成,可以将一个多核处理器划分为多个block。

线程束(warp)是最基本的执行单元,一个warp包含32个基本的计算单元-线程thread,也就是说比如我发一个指令,那么线程束中的32个thread将会并行执行该指令。(所以在我们划分blocksize的时候,一般都会设置成32的倍数)

二、CUDA编程基础

CUDA编程并行计算整体流程

  1. 在GPU上分配显存,将CPU上的数据拷贝到显存上
  2. 利用核函数完成GPU显存中数据的计算
  3. 将显存中的计算结果拷贝回CPU内存中

从矩阵加法和矩阵乘法来学习CUDA编程的基本框架(耐心看完下面代码,基本都有注解,可以模仿范式编写自己的代码)

1.矩阵加法

计算矩阵加法:C = A + B,设A B为一维矩阵,长度为n
//核函数(即在GPU中执行的函数/用__global__申明)
__global__
void vecAddKernel(float* A_d, float* B_d, float* C_d, int n)
{
    int i = threadIdx.x + blockDim.x * blockIdx.x;  //计算线程ID
    if (i < n) C_d[i] = A_d[i] + B_d[i];  //筛选ID小于n的线程,即例如线程1计算C_d[1] = A_d[1] + B_d[1]
}
//主函数
int main(int argc, char *argv[]) {
    int n = 10000;
    size_t size = n * sizeof(float);
    // 在CPU上分配内存
    float *a = (float *)malloc(size);
    float *b = (float *)malloc(size);
    float *c = (float *)malloc(size);
  //初始化a b的值(将需要计算的向量放到分配好的内存中)
    for (int i = 0; i < n; i++) {
        float af = rand() / double(RAND_MAX);
        float bf = rand() / double(RAND_MAX);
        a[i] = af;
        b[i] = bf;
    }
  //在GPU上分配显存(格式按照 参考下面代码,size为需要分配的显存大小)
    float *da = NULL;
    float *db = NULL;
    float *dc = NULL;
    cudaMalloc((void **)&da, size);
    cudaMalloc((void **)&db, size);
    cudaMalloc((void **)&dc, size);
  //将CPU上初始化的a b值拷贝到GPU上
    cudaMemcpy(da,a,size,cudaMemcpyHostToDevice);
    cudaMemcpy(db,b,size,cudaMemcpyHostToDevice);
  //划分GPU的block和Grid
    int threadPerBlock = 256;  //一个warp大小为32,一般设置为32的倍数
    int blockPerGrid = (n + threadPerBlock - 1)/threadPerBlock;  //根据划分的blocksize计算gridsize
  //调用核函数
    vecAddKernel <<< blockPerGrid, threadPerBlock >>> (da, db, dc, n);
    //将GPU上的计算结果拷贝回CPU
    cudaMemcpy(c,dc,size,cudaMemcpyDeviceToHost);
  //释放GPU显存资源
    cudaFree(da);
    cudaFree(db);
    cudaFree(dc);
  //释放CPU内存资源
    free(a);
    free(b);
    free(c);
    return 0;
}

2.矩阵乘法

计算矩阵乘法:C = A * B,矩阵A的维度为M*K,矩阵B的维度为K*N
#define M 512
#define K 512
#define N 512
void initial(float *array, int size)
{
  for (int i = 0; i < size; i++)
  {
    array[i] = (float)(rand() % 10 + 1);
  }
}
//核函数(传入显存ABC以及维度信息MNK)
__global__ void multiplicateMatrix(float *array_A, float *array_B, float *array_C, int M_p, int K_p, int N_p)
{
  //这里我们划分的lblock和grid是二维的,分别计算线程的二维索引(x方向和y方向的索引)
  int ix = threadIdx.x + blockDim.x*blockIdx.x;//row number,
  int iy = threadIdx.y + blockDim.y*blockIdx.y;//col number
  if (ix < N_p && iy < M_p)  //筛选线程,每个线程计算C中的一个元素,线程的xy索引与C的元素位置索引对应
  {
    float sum = 0;
    for (int k = 0; k < K_p; k++) //C中的某个元素为A中对应行和B中对应列向量的乘积和。
    {
      sum += array_A[iy*K_p + k] * array_B[k*N_p + ix];
    }
    array_C[iy*N_p + ix] = sum;
  }
}
//主函数
int main(int argc, char **argv)
{
  int Axy = M * K;
  int Bxy = K * N;
  int Cxy = M * N;
  float *h_A, *h_B, *hostRef, *deviceRef;
  //在CPU上分配内存
  h_A = (float*)malloc(Axy * sizeof(float)); 
  h_B = (float*)malloc(Bxy * sizeof(float));
  h_C = (float*)malloc(Cxy * sizeof(float));
  initial(h_A, Axy);
  initial(h_B, Bxy);
  //在GPU上分配显存
  float *d_A, *d_B, *d_C;
  cudaMalloc((void**)&d_A, Axy * sizeof(float));
  cudaMalloc((void**)&d_B, Bxy * sizeof(float));
  cudaMalloc((void**)&d_C, Cxy * sizeof(float));
  //将CPU上初始化的a b值拷贝到GPU上
  cudaMemcpy(d_A, h_A, Axy * sizeof(float), cudaMemcpyHostToDevice);
  cudaMemcpy(d_B, h_B, Bxy * sizeof(float), cudaMemcpyHostToDevice);
  //划分GPU的block和Grid
    int dimx = 2;
    int dimy = 2;
    dim3 block(dimx, dimy);
    dim3 grid((M + block.x - 1) / block.x, (N + block.y - 1) / block.y);
    //调用核函数
    multiplicateMatrix<<<grid,block>>> (d_A, d_B, d_C, M, K, N);
  //将GPU上计算结果拷贝回CPU
    cudaMemcpy(h_C, d_C, Cxy * sizeof(float), cudaMemcpyDeviceToHost);
  //释放GPU显存资源
    cudaFree(d_A);
    cudaFree(d_B);
    cudaFree(d_C);
  //释放CPU内存资源
    free(h_A);
    free(h_B);
    free(h_C);
return (0);
}

三、CUDA进阶 I——利用共享内存加速访存

一般我们将数据发送到GPU后,默认保存到全局内存,而全局内存的读写速度特别慢,这个时候我们将数据从全局内存放到线程块的共享内存中,计算过程中,读取访问速度更快的共享内存,将会大大减少数据访问耗时,提高程序速度。

1.CUDA内存读写速度比较

下列几种内存的架构参见下图:

  • 线程寄存器(~1周期)
  • Block共享内存(~5周期)
  • Grid全局内存(~500周期)
  • Grid常量内存(~5周期)

2.申请共享内存

前面对比了共享内存和全局内存的访问速度,为了进一步提高访存速度,可以把全局内存一部分数据拷贝到共享内存中(由于共享内存的大小有限,大概只有几十K,所以只能分多次拷贝数据)

申请共享内存的方式分为静态申请动态申请

申请共享内存关键字:__ shared __

块内共享内存同步:__syncthreads()函数(块内不同线程之间同步)

  • 静态申请
__global__ void staticFun(int* d, int n)
{
  __shared__ int s[64];  //静态申请,需要指定申请内存的大小
  int t = treadIdx.x;
  s[t] = d[t];  //将全局内存数据拷贝到申请的共享内存中,之后利用共享内存中的数据参与运算将会比调
  //用全局内存数据参与运算快(由于共享内存有限,不能全部拷贝到共享内存,这其中就涉及到分批拷贝问题了)
  __syncthreads();//需要等所有线程块都拷贝完成后再进行计算
}
staticFun<<1,n>>(d, n);
  • 动态申请
__global__ void dynamicFun(int *d, int n)
{
  extern __shared__ int s[]; //动态申请,不需要指定大小,需要加上extern关键字
  int t = threadIdx.x;
  s[t] = d[t];
  __syncthreads();
}
dynamicFun<<1, n, n*sizeof(int)>>(d, n); //动态申请需要在外部指定共享内存大小

上面内容只是让大家对共享内存如何加速运算有一个初步的认识,详细使用方法可以参考我的另外一篇文章:CUDA加速计算矩阵乘法&进阶玩法(共享内存)

后面的内容有待补充。。。有用的话记得点赞搜藏o

四、CUDA进阶 II——利用stream加速大批量文件IO读写耗时

1. 认识CUDA stream

CUDA的stream流,类似我们经常使用CPU时开多线程。

  • 当我们使用GPU进行计算时,如果我们没有主动开启stream流,GPU会自动创建默认流来执行核函数,默认流和CPU端的计算是同步的。(也即在CPU执行任务过程中,必须等GPU执行完核函数后,才能继续往下执行)
  • 当我们使用GPU进行计算时,我们可以主动开启多个stream流,类似CPU开启多线程。我们可以将大批量文件读写分给多个流去执行,或者用不同的流分别计算不同的核函数。开启的多个流之间是异步的,流与CPU端的计算也是异步的。所以我们需要注意加上同步操作。
    值得注意的是,受PCIe总线带宽的限制,当一个流在进行读写操作时,另外一个流不能同时进行读写操作,但是其他流可以进行数值计算任务。这个有点类似与CPU中的流水线机制。

2. CUDA stream API介绍

• 创建一个stream

cudaStream_t stream;

cudaStreamCreate(&stream);

• 将host数据拷贝到device

cudaMemcpyAsync(dst, src, size, type, stream)

• kernel在流中执行

kernel_name<<<grid, block, stream>>>(praments);

• 同步和查询

cudaError_t cudaStreamSynchronize(cudaStream_t stream)

cudaError_t cudaStreamQuery(cudaStream_t stream);

• 销毁流

cudaError_t cudaStreamDestroy(cudaStream_t stream)

上面仅对CUDA stream有一个简单的介绍和认知,CUDA stream使用示例在这篇文章进行了介绍:CUDA优化方案—stream的使用

官方参考文档:https://developer.nvidia.com/blog/gpu-pro-tip-cuda-7-streams-simplify-concurrency/

五、CUDA进阶 III——调用cuBLAS库API进行矩阵计算

cuBLAS是一个BLAS的实现,允许用户使用NVIDIA的GPU的计算资源。使用cuBLAS 的时候,应用程序应该分配矩阵或向量所需的GPU内存空间,并加载数据,调用所需的cuBLAS函数,然后从GPU的内存空间上传计算结果至主机,cuBLAS API也提供一些帮助函数来写或者读取数据从GPU中。

• 列优先的数组,索引以1为基准

• 头文件 include "cublas_v2.h“

• 三类函数(向量标量、向量矩阵、矩阵矩阵)

cuBlas使用范例

int main(int argc, char **argv)
{
    ......
        cublasStatus_t status;
        cublasHandle_t handle;
        cublasCreate(&handle);
        float a = 1, b = 0;
        cublasSgemm(
          handle,
          CUBLAS_OP_T,   //矩阵A的属性参数,转置,按行优先
          CUBLAS_OP_T,   //矩阵B的属性参数,转置,按行优先
          M,          //矩阵A、C的行数
          N,          //矩阵B、C的列数
          K,          //A的列数,B的行数,此处也可为B_ROW,一样的
          &a,             //alpha的值
          d_A,            //左矩阵,为A
          K,          //A的leading dimension,此时选择转置,按行优先,则leading dimension为A的列数
          d_B,            //右矩阵,为B
          N,          //B的leading dimension,此时选择转置,按行优先,则leading dimension为B的列数
          &b,             //beta的值
          d_C,            //结果矩阵C
          M           //C的leading dimension,C矩阵一定按列优先,则leading dimension为C的行数
        );
        cudaMemcpy(deviceRef, d_C, Cxy * sizeof(float), cudaMemcpyDeviceToHost);
        cudaDeviceSynchronize();
        ......
}


相关实践学习
部署Stable Diffusion玩转AI绘画(GPU云服务器)
本实验通过在ECS上从零开始部署Stable Diffusion来进行AI绘画创作,开启AIGC盲盒。
目录
相关文章
|
25天前
|
存储 SQL 关系型数据库
MySQL进阶突击系列(03) MySQL架构原理solo九魂17环连问 | 给大厂面试官的一封信
本文介绍了MySQL架构原理、存储引擎和索引的相关知识点,涵盖查询和更新SQL的执行过程、MySQL各组件的作用、存储引擎的类型及特性、索引的建立和使用原则,以及二叉树、平衡二叉树和B树的区别。通过这些内容,帮助读者深入了解MySQL的工作机制,提高数据库管理和优化能力。
|
1月前
|
人工智能 前端开发 编译器
【AI系统】LLVM 架构设计和原理
本文介绍了LLVM的诞生背景及其与GCC的区别,重点阐述了LLVM的架构特点,包括其组件独立性、中间表示(IR)的优势及整体架构。通过Clang+LLVM的实际编译案例,展示了从C代码到可执行文件的全过程,突显了LLVM在编译器领域的创新与优势。
65 3
|
2月前
|
运维 持续交付 云计算
深入解析云计算中的微服务架构:原理、优势与实践
深入解析云计算中的微服务架构:原理、优势与实践
81 1
|
7天前
|
机器学习/深度学习 算法 PyTorch
深度强化学习中SAC算法:数学原理、网络架构及其PyTorch实现
软演员-评论家算法(Soft Actor-Critic, SAC)是深度强化学习领域的重要进展,基于最大熵框架优化策略,在探索与利用之间实现动态平衡。SAC通过双Q网络设计和自适应温度参数,提升了训练稳定性和样本效率。本文详细解析了SAC的数学原理、网络架构及PyTorch实现,涵盖演员网络的动作采样与对数概率计算、评论家网络的Q值估计及其损失函数,并介绍了完整的SAC智能体实现流程。SAC在连续动作空间中表现出色,具有高样本效率和稳定的训练过程,适合实际应用场景。
35 7
深度强化学习中SAC算法:数学原理、网络架构及其PyTorch实现
|
3天前
|
机器学习/深度学习 弹性计算 人工智能
阿里云服务器ECS架构区别及选择参考:X86计算、ARM计算等架构介绍
在我们选购阿里云服务器的时候,云服务器架构有X86计算、ARM计算、GPU/FPGA/ASIC、弹性裸金属服务器、高性能计算可选,有的用户并不清楚他们之间有何区别,本文主要简单介绍下这些架构各自的主要性能及适用场景,以便大家了解不同类型的架构有何不同,主要特点及适用场景有哪些。
|
8天前
|
存储 人工智能 运维
面向AI的服务器计算软硬件架构实践和创新
阿里云在新一代通用计算服务器设计中,针对处理器核心数迅速增长(2024年超100核)、超多核心带来的业务和硬件挑战、网络IO与CPU性能增速不匹配、服务器物理机型复杂等问题,推出了磐久F系列通用计算服务器。该系列服务器采用单路设计减少爆炸半径,优化散热支持600瓦TDP,并实现CIPU节点比例灵活配比及部件模块化可插拔设计,提升运维效率和客户响应速度。此外,还介绍了面向AI的服务器架构挑战与软硬件结合创新,包括内存墙问题、板级工程能力挑战以及AI Infra 2.0服务器的开放架构特点。最后,探讨了大模型高效推理中的显存优化和量化压缩技术,旨在降低部署成本并提高系统效率。
|
1月前
|
存储 机器学习/深度学习 人工智能
【AI系统】计算图优化架构
本文介绍了推理引擎转换中的图优化模块,涵盖算子融合、布局转换、算子替换及内存优化等技术,旨在提升模型推理效率。计算图优化技术通过减少计算冗余、提高计算效率和减少内存占用,显著改善模型在资源受限设备上的运行表现。文中详细探讨了离线优化模块面临的挑战及解决方案,包括结构冗余、精度冗余、算法冗余和读写冗余的处理方法。此外,文章还介绍了ONNX Runtime的图优化机制及其在实际应用中的实现,展示了如何通过图优化提高模型推理性能的具体示例。
57 4
【AI系统】计算图优化架构
|
1月前
|
机器学习/深度学习 人工智能 API
【AI系统】昇腾异构计算架构 CANN
本文介绍了昇腾 AI 异构计算架构 CANN,涵盖硬件层面的达·芬奇架构和软件层面的全栈支持,旨在提供高性能神经网络计算所需的硬件基础和软件环境。通过多层级架构,CANN 实现了高效的 AI 应用开发与性能优化,支持多种主流 AI 框架,并提供丰富的开发工具和接口,助力开发者快速构建和优化神经网络模型。
47 1
|
1月前
|
SQL 存储 关系型数据库
MySQL进阶突击系列(01)一条简单SQL搞懂MySQL架构原理 | 含实用命令参数集
本文从MySQL的架构原理出发,详细介绍其SQL查询的全过程,涵盖客户端发起SQL查询、服务端SQL接口、解析器、优化器、存储引擎及日志数据等内容。同时提供了MySQL常用的管理命令参数集,帮助读者深入了解MySQL的技术细节和优化方法。
|
2月前
|
机器学习/深度学习 弹性计算 人工智能
阿里云服务器架构有啥区别?X86计算、Arm、GPU异构、裸金属和高性能计算对比
阿里云ECS涵盖x86、ARM、GPU/FPGA/ASIC、弹性裸金属及高性能计算等多种架构。x86架构采用Intel/AMD处理器,适用于广泛企业级应用;ARM架构低功耗,适合容器与微服务;GPU/FPGA/ASIC专为AI、图形处理设计;弹性裸金属提供物理机性能;高性能计算则针对大规模并行计算优化。