CUDA简单的基础概念

CUDA简单的基础概念

一、简单的实现向量加法

我们打开cuda的官网(点击这个连接地址),有个简单的流程代码:

// 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
    ...
}

我们翻译一下各个步骤你就可以直接看出来了使用GPU进行向量加法的步骤了:

// vector_add.cu
#include <stdio.h>

// CUDA核函数,用于在GPU上执行向量加法
__global__ void vectorAdd(int *a, int *b, int *c, int N) {
    // 获取当前线程的索引
    int i = blockIdx.x * blockDim.x + threadIdx.x;

    // 确保线程索引在向量大小范围内
    if (i < size) {
        // 计算向量元素相加结果
        c[i] = a[i] + b[i];
    }
}

int main() {
    int size = 1000;
    int a[size], b[size], c[size];

    // 在设备上分配内存空间
    int *dev_a, *dev_b, *dev_c;
    cudaMalloc((void**)&dev_a, size * sizeof(int));
    cudaMalloc((void**)&dev_b, size * sizeof(int));
    cudaMalloc((void**)&dev_c, size * sizeof(int));

    // 初始化输入向量 a 和 b
    for (int i = 0; i < size; ++i) {
        a[i] = i;
        b[i] = i * 2;
    }

    // 将输入向量 a 和 b 复制到设备内存
    cudaMemcpy(dev_a, a, size * sizeof(int), cudaMemcpyHostToDevice);
    cudaMemcpy(dev_b, b, size * sizeof(int), cudaMemcpyHostToDevice);

    // 定义每个块中的线程数和块数
    int threadsPerBlock = 256;
    int blocksPerGrid = (size + threadsPerBlock - 1) / threadsPerBlock;

    // 调用 CUDA 核函数执行向量加法
    vectorAdd<<<blocksPerGrid, threadsPerBlock>>>(dev_a, dev_b, dev_c, size);

    // 将计算结果从设备复制到主机内存
    cudaMemcpy(c, dev_c, size * sizeof(int), cudaMemcpyDeviceToHost);

    // 打印结果向量 c 的前10个元素
    for (int i = 0; i < 10; ++i) {
        printf("%d ", c[i]);
    }
    printf("\n");

    // 释放设备上的内存空间
    cudaFree(dev_a);
    cudaFree(dev_b);
    cudaFree(dev_c);

    return 0;
}

从上面的注释我们就可以看出CUDA基础开发流程是什么样子了。

二、CUDA基础开发流程:

在CUDA编程中,设备端的大部分程序代码可以独立于主机端的程序代码执行,当一个Kernel函数被调用后,控制权会返回到主机端的CPU控制。一般来说,CPU负责串行的工作而由GPU进行并行的操作。
 

CUDA的开发流程虽然因为不同的接口方式有所不同,但一般分以下几步:
1、分配CPU和GPU内存
2、复制数据从CPU端到GPU端
3、调用CUDA kernel来执行运算
4、运算结束将结果数据从GPU拷贝至CPU
5、释放CPU和GPU内存

三、基础概念

通过代码展示一个简单的CUDA实现向量加法的流程我们可以看到里面涉及到了很多的概念:

1、CPU和GPU

CPU我们可以把它看做一个指挥者,主机端,host,而完成大量计算的GPU是我们的计算设备,device。

img

  • 左图:四核CPU=4个ALU(算数逻辑单元)+1个控制单元(control)+1个缓存(cache),内存(DRAM)一般不在片上,CPU通过总线访问内存。
  • 右图:GPU,N个ALU,红色框SM内所有ALU公用一个Control单元和Cache
2、SM

一个SM相当于一个完整的多核CPU,但是ALU多了(计算能力提升),control少了(控制能力减弱),所以对于控制(逻辑)复杂的程序,GPU的SM没法和CPU比较,但是对于逻辑简单,数据量大的任务,GPU更高效。并且,一个GPU有非常多SM且越来越多。

在 GPU 中,SM 通常是指 “Streaming Multiprocessor”,即流式多处理器。它是 NVIDIA GPU 架构中的核心组件,相当于 GPU 的 “心脏”,类似于 CPU 的核心,负责执行图形和计算操作。
每个 SM 包含多个流处理器(CUDA Core)或线程,还配备了共享内存、控制单元和其他加速计算单元,如张量核心或光线追踪单元等。SM 是 GPU 进行资源分配和调度的基本单位,GPU 会把线程块分配到 SM 上,由 SM 负责调度块内的线程执行计算任务。SM 的数量和性能是衡量 GPU 整体性能的重要指标之一,其数量越多,GPU 的并行计算能力通常越强。

3、CUDA基础框架说明

CUDA编程中,是通过CPU和GPU共同来完成的,CPU居于主导的控制地位而GPU用于协助进行大规模的数据计算和处理。CPU与GPU通过PCIE总线连接到一起,GPU之间通信通过PCIe或者是NVLink来实现。CPU被称为主机端(host)而GPU端被称为设备端(device)

CPU 与 GPU 的芯片资源分布示例

4、 CUDA编程结构

异构环境中的多个CPU和多个GPU通过PCIe总线相互通信,也通过PCIe总线相互分隔,需要区分两种设备的内存:
• 主机:CPU及其内存
• 设备:GPU及其内存
这两个内存从硬件到软件都是隔离的,需要用内存来回拷贝的方法编写程序

img

核函数被调用后控制立刻归还给主机线程,即第一个并行代码执行时,很有可能第二段host代码已经开始同步执行了

5、CUDA编程模型概述

编程模型可以理解为:用于控制异构计算设备工作模式的语法,内存结构,线程结构等,GPU中大致可以分为:

  • 核函数
  • 内存管理
  • 线程管理

(1)、核函数

CUDA 核函数(Kernel Function)是 CUDA 编程模型的核心,它定义了在 GPU 上并行执行的计算任务。

简单来说,你可以将一个 CUDA 程序理解为由两部分组成:

  1. 主机(Host)代码:运行在 CPU 上,负责处理串行逻辑、数据准备和启动核函数。
  2. 设备(Device)代码:即核函数,运行在 GPU 上,负责处理大规模并行计算任务。

CUDA 核函数在 C/C++ 代码中定义,通过 __global__ 关键字声明。它与普通函数有明显区别,普通函数在 CPU 上顺序执行,而核函数在 GPU 的多个线程上并行执行。例如:

__global__ void vectorAdd(float* a, float* b, float* c, int n) {
    // 计算当前线程的全局索引
    int idx = threadIdx.x + blockIdx.x * blockDim.x;
    if (idx < n) {
        c[idx] = a[idx] + b[idx];
    }
}

核函数调用时会启动大量线程并行执行。每个线程执行相同的核函数代码,但处理不同的数据。这些线程以层次化的结构组织,包括线程块(block)和网格(grid)。上述 addKernel 核函数,每个线程根据自身索引 index 从输入数组 a 和 b 中取出对应元素相加,结果存入数组 c

核函数不能像普通函数那样直接调用,必须通过一个特殊的语法 <<< >>> 来配置其并行执行的规模。

// 语法:核函数名<<<网格配置, 线程块配置>>>(参数列表); vectorAdd<<<numBlocks, threadsPerBlock>>>(d_a, d_b, d_c, N);

这里的 <<< >>> 被称为执行配置,它定义了线程的层次结构

线程(Thread):GPU 执行的最小单位。每个线程独立执行核函数中的代码。

线程块(Block):一组线程的集合。块内的线程可以通过共享内存(Shared Memory)快速通信,并使用 __syncthreads() 进行同步。

网格(Grid):包含所有线程块的集合。  例如,<<<10, 256>>> 表示启动一个包含 10 个线程块的网格,每个块内有 256 个线程,总共将有 2560 个线程并行执行该核函数。

(2)、内存管理

CUDA的API可以分配管理Device上的内存,当然也可以用CDUA管理Host上的内存,主机上的传统标准库也能完成主机内存管理

内存分层结构如下图所示:

img

(3)、线程管理
a、分层组织结构

一个核函数只能有一个grid,一个grid可以有很多个块,每个块可以有很多的线程:

img

同一个线程块block中的线程可以完成下述协作:

  • 同步
  • 共享内存

==不同线程块block内线程不能相互影响!他们是物理隔离的!==

b、线程编号

每个线程执行同样的一段串行代码,如何让同一段代码对应不同的数据?

首先需要将线程彼此区分开,使得这些线程也能区分自己的数据:

  • blockIdx(block在grid内的位置索引,block index)
  • threadIdx(thread在block内的位置索引,thread index)

因为一个核函数只有一个grid,所以不需要gridIdx

这两个内置结构体基于uint3定义,包含3个无符号整数的结构,通过3个字段指定:

①blockIdx

  • blockIdx.x // block在grid中的x坐标
  • blockIdx.y // block在grid中的y坐标
  • blockIdx.z // block在grid中的z坐标

gridDim表示grid中block索引的范围,grid一般是二维的即grid通常被分为2D blcok

②threadIdx

  • threadIdx.x // thread在block中的x坐标
  • threadIdx.y // thread在block中的y坐标
  • threadIdx.z // thread在block中的z坐标

blockDim表示block中thread索引的范围,block一般是三维的即block通常被分为3D thread

blockDim.x、blockDim.y、blockDim.z

注意:dim3是手工定义的,主机端可见。uint3是设备端在执行的时候可见的,不可以在核函数运行时修改,初始化完成后uint3值就不变了

6、CUDA重要的关键字

在CUDA编程中主要的关键字有:
1、__global__
此关键字用来定义核函数,由<<< >>>配置在执行核心的执行线程数,每个线程都有唯一一个线程ID并可在内核中进行访问。它的特点包括:
主机端或计算能力在大于等于3.2的设备端调用,在设备端执行;
其返回值必须为void类型且不能是类成员函数;
必须指定其执行配置即上面提到的<<<>>>;
函数的调用是异步的即其在设备执行完成前返回;
2、__device__
它被定义为只能由设备端调用并且在设备端执行的函数,需要注意的是,它不能与关键字__global__一起使用

3、__host__
这个关键字用来定义在主机上执行的函数,其特点为:
函数只能在主机端调用和执行;
它不可以和关键字__global__一起使用,但可以与__device__一起使用

本文原创作者:冯一川(csdn:ifeng12358),未经作者授权同意,请勿转载。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

冯一川

谢谢老板对我的支持!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值