CUDA核函数详解(一)

 

笔者在本科阶段实现了一个Edge-Compute-Engine,但是实现的比较粗糙和简单,计划通过cuda进行矩阵方向的重构和加速,顺便记录一些内容。

在 GPU 编程中,CUDA(Compute Unified Device Architecture)使得并行计算得以高效实现。在 CUDA 中,矩阵加法是第一个入门操作,因其简单且能展示 CUDA 中线程并行化的核心概念。本文将从最底层的原理开始,实现矩阵加法的 CUDA 核函数,并深入讨论其实现背后的一些信息。

1. 什么是矩阵加法?

矩阵加法是线性代数中的一种基本运算,定义为两个矩阵对应元素的逐一相加。对于给定的矩阵 A 和 B,矩阵 C 的每个元素

(1)C[i][j]

(2)C[i][j]=A[i][j]+B[i][j]

给出。

我们要做的,就是把这个运算搬到 GPU 上,并利用并行计算的优势加速这个操作。

2. (必知必会)CUDA 基础

在 CUDA 中,计算任务被分配给大量并行线程来完成。每个线程都执行同样的操作,但作用于不同的数据片段。为了实现这一目标,CUDA 使用 网格(grid) 和 块(block) 的结构。

线程块(Block):是一个线程的集合,所有线程在同一个块内可以共享内存。

网格(Grid):由多个线程块组成。

每个线程在自己的计算中有一个唯一的 ID,这些 ID 用于标识它在线程块和网格中的位置。

简单画了一个图:

image-20250326224642208

3. CUDA 核函数解析

接下来,我们来看一个实现矩阵加法的 CUDA 核函数:

3.1 输入参数

A 和 B:矩阵 A 和矩阵 B 的指针,它们存储在 GPU 上的全局内存中。

C:矩阵 C 是最终结果矩阵,也会存储在 GPU 上的全局内存中。

rows 和 cols:矩阵的行数和列数。

3.2 核心逻辑

让我们逐步解析每一行代码的实现细节:

3.2.1 计算线程的索引

上图为例:

image-20250326224642208

块内线程可以有xyz三维坐标:

image-20250326224959620在我们的例子里面:

blockIdx.x 和 blockIdx.y 分别表示线程块在 x 和 y 方向上的索引。

blockDim.x 和 blockDim.y 表示每个线程块在 x 和 y 方向上包含的线程数。

threadIdx.x 和 threadIdx.y 分别表示线程在其所属块内的 x 和 y 方向上的索引。

通过这几个值的组合,我们能够计算出当前线程在矩阵中的横向索引 idx 和纵向索引 idy。

3.2.2 边界检查

CUDA 中的线程会按照网格结构启动,某些线程可能会对应矩阵外部的无效位置,因此在执行矩阵加法之前,我们需要检查当前线程的索引是否在矩阵的有效范围内。如果超出范围,线程将不执行任何操作,从而避免越界访问。

3.2.3 计算线性索引

由于矩阵在内存中通常是以行主序(row-major order)存储的,因此我们需要将二维的 idx 和 idy 转换为线性的一维索引 linear_idx。这个转换的公式是:

(3)linear_idx=idx×cols+idy

这样,我们可以通过 linear_idx 直接访问矩阵中的元素。

3.2.4 执行加法操作

这里,我们将矩阵 A 和 B 对应位置的元素相加,结果存储到矩阵 C 的对应位置。这一操作是并行执行的,每个线程负责处理一个元素的加法。

linear_idx = idx * cols + idy 这是将二维的 (idx, idy) 坐标转换为 一维索引。在矩阵的存储方式中,通常是 按行主序(Row-major order) 存储的,因此,二维坐标可以转换为线性索引(展平操作)。这样,每个线程就能计算出它应该处理的矩阵位置。

矩阵加法: C[linear_idx] = A[linear_idx] + B[linear_idx] 这个操作就是每个线程执行的任务,直接将 A 和 B 对应位置的元素相加,并存入 C 的对应位置。

而后就直接并行化,无需显式循环。

3.3 测试

3.3.1 第一步:定义Matrix

这里的定义来自我自己实现的https://github.com/AllenZYJ/Edge-Computing-Engine#

row 和 col 分别表示矩阵的行数和列数。

data 是一个指向 float 类型的指针,用来存储矩阵的数据。矩阵的数据是按行主序(row-major order)存储的,即矩阵的每一行按顺序存储在内存中。

这里的构造函数接受行数 r 和列数 c 作为参数,并通过 new 关键字在堆上分配一个大小为 r * c 的 float 数组来存储矩阵的数据。括号 () 表示将数据初始化为 0。

接下来是拷贝构造函数:

拷贝赋值函数:

它们负责对象的复制过程。它们的主要作用是在对象的创建和赋值时,确保正确的内存管理,避免潜在的错误和资源泄漏。

在大规模计算,尤其是涉及大量数据和高效内存管理的计算任务中,如果没有显式定义拷贝构造函数和拷贝赋值运算符,C++ 会使用默认的拷贝构造函数和赋值运算符,通常会进行浅拷贝(shallow copy)。浅拷贝只会复制对象的成员变量(例如指针),而不会复制指针指向的内存区域。这意味着两个对象会共享同一块内存。

3.3.2 实现核函数调用

matrixAddCUDA 函数的作用是使用 CUDA 进行矩阵加法计算。它接受三个 Matrix_CU 类型的参数:A、B 和 C,分别表示输入的两个矩阵和输出的矩阵。具体步骤如下:

接下来,调用这个函数

编译执行:

结果:

以上实验完整代码:

Matrix_cuda_def