LeetGPU习题01:Matrix Addition
在 LeetGPU 的习题列表中,Element-wise 算子指的是对输入张量/数组的每个元素独立执行相同操作、不依赖于其他元素或全局统计量的算子。
以下是明确的 Element-wise 算子:
算子说明
| 名称 | 说明 |
|---|---|
| Vector Addition | 两个向量逐元素相加 |
| Matrix Addition | 两个矩阵逐元素相加 |
| Matrix Copy | 逐元素复制矩阵 |
| Color Inversion | 对每个像素独立取反 |
| Reverse Array | 反转数组,每个元素独立移动位置 |
| ReLU | 逐元素应用 ReLU 函数 |
| Leaky ReLU | 逐元素应用 Leaky ReLU |
| Sigmoid Activation | 逐元素应用 Sigmoid 函数 |
| Value Clipping | 逐元素裁剪到指定范围 |
| Sigmoid Linear Unit (SiLU) | 逐元素 SiLU 激活 |
| Swish-Gated Linear Unit (SWiGLU) | 逐元素 SWiGLU(门控部分也为逐元素) |
| Gaussian Error Gated Linear Unit (GEGLU) | 逐元素 GEGLU 激活 |
| RGB to Grayscale | 每个像素独立转换,不依赖邻域 |
| Interleave Arrays | 交替合并两数组,每个输出元素仅依赖对应位置输入 |
| Rotary Positional Embedding | 对每个位置独立应用旋转矩阵 |
| Weight Dequantization | 每个权重独立反量化 |
| INT8 Quantized MatMul(仅反量化部分) | 反量化部分为逐元素,整体不是 |
| Simple Inference | 线性层前向包含矩阵乘,非 element-wise,但其中的激活部分可能是逐元素 |
Vector Addition
1. Matrix Addition题目
实现一个在 GPU 上对两个包含 32 位浮点数的矩阵进行逐元素相加的程序。程序接收两个相同维度的输入矩阵,输出一个矩阵,其中的每个元素为对应位置元素之和。
示例 1:
输入:A = [[1.0, 2.0], [3.0, 4.0]]B = [[5.0, 6.0], [7.0, 8.0]]
输出:C = [[6.0, 8.0], [10.0, 12.0]]示例 2:
输入:A = [[1.5, 2.5, 3.5], [4.5, 5.5, 6.5], [7.5, 8.5, 9.5]]B = [[0.5, 0.5, 0.5], [0.5, 0.5, 0.5], [0.5, 0.5, 0.5]]
输出:C = [[2.0, 3.0, 4.0], [5.0, 6.0, 7.0], [8.0, 9.0, 10.0]]约束条件
- 输入矩阵
A和B维度相同 - 1 ≤ N ≤ 4096
- 所有元素均为 32 位浮点数
- 性能评测基于 N = 4,096
2. Pytorch题解
import torch# A, B, C are tensors on the GPUdef solve(A: torch.Tensor, B: torch.Tensor, C: torch.Tensor, N: int): C.copy_(A + B)3. Triton题解
(1) 向量化 (2)二维直接计算

3.1. 向量化如何计算offset?
在编写Triton Kernel的时候,我们希望一个现成一次处理多个连续元素,例如,每个线程一次读取4个float32, 就等价于一条128位宽的向量加载指令,能够显著提升内存带宽利用率.
那么如何来生成每个线程要访问的全局内存偏移量呢?
一个直观的例子
假设每个线程块有4个线程,每个线程一次处理2个元素,则BLOCK_SIZE=4,VEC_WIDTH=2,pid=0(起始 0).
我们希望这4个线程共同覆盖一块连续的内存区域,总元素为BLOCK_SIZE * VEC_WIDTH = 8
. 一种自然的分配方式是:
线程0: [0, 1]线程1: [2, 3]线程2: [4, 5]线程3: [6, 7]因此我们希望Triton能够生成一个形状为(4, 2)的偏移量矩阵,其中第i行第j列的值刚好是全局内存的线性索引.
block_start = pid * BLOCK_SIZE * VEC_WIDTH
offsets = ( block_start # 当前线程块的起始偏移 + tl.arange(0, BLOCK_SIZE)[:, None] * VEC_WIDTH # 每个线程的基地址偏移 + tl.arange(0, VEC_WIDTH)[None, :] # 线程内的向量偏移)按步骤拆解
首先是计算线程块的起始偏移:
block_start = pid * BLOCK_SIZE * VEC_WIDTH.
每个线程块整体负责的数量为BLOCK_SIZE * VEC_WIDTH, 乘以pid后,就得到当前线程块在整个数据中的起始索引.
其次计算每个线程负责的第一个元素偏移:
tl.arange(0, BLOCK_SIZE)[:, None] * VEC_WIDTH.
tl.arange(0, BLOCK_SIZE) 生成一个一维向量 [0, 1, 2, 3],代表线程的局部 ID。
[:, None] 将其转换为列向量 (BLOCK_SIZE, 1),即形状变为 4×1.
[[0], [1], [2], [3]]乘以VEC_WIDTH后得到:
[[0], # 线程0负责的第一个元素距离块起始的偏移 [2], # 线程1负责的第一个元素偏移 [4], [6]]最后再加上线程内的连续向量偏移:
tl.arange(0, VEC_WIDTH)[None, :]
同样先生成一个[0, 1]的范围,然后[None,:]将其转换为行向量,即形状为(1, 2).
现在,将三者相加,根据Triton的广播机制,列会沿着列方向复制,行会沿着行方向复制,最终的到一个的矩阵:
[[0 + 0, 0 + 1], → [[0, 1], [2 + 0, 2 + 1], [2, 3], [4 + 0, 4 + 1], [4, 5], [6 + 0, 6 + 1]] [6, 7]]计算完成偏移量之后,我们将其展平为一维数组:
offsets = tl.reshape(offsets, (BLOCK_SIZE * VEC_WIDTH,))这样我们就可以直接用这个偏移量数组调用tl.load或者tl.store,一次性进行向量化访问.
3.2. 二维块指针构造
实际上,在处理矩阵、图像这类天然具有二维结构的数据时,将并行任务也按二维方式划分,往往能让代码思路更加清晰。此时,每个线程块直接对应数据中的一个子矩阵(或子图像区域),省去了将二维坐标手动转换为一维线性 ID 的麻烦。更重要的是,这种划分方式让我们可以灵活调整行、列两个方向上的并行度,更好地适配不同硬件架构与问题规模。
Triton 提供了 tl.make_block_ptr 来优雅地描述这种二维块访问模式。我们先通过一个直观的例子感受它的便利,再逐一拆解每个参数的含义。
一个直观的例子
假设我们有一个4*4的矩阵A,在GPU以行优先存储,内存布局如下:

我们有一个 4×4 的矩阵 A,在 GPU 内存中按行优先(row-major)方式存储,其内存布局如下图所示(数字表示元素在内存中的线性偏移):
列 0 列 1 列 2 列 3┌────┬────┬────┬────┐│ 0 │ 1 │ 2 │ 3 │ 行 0├────┼────┼────┼────┤│ 4 │ 5 │ 6 │ 7 │ 行 1├────┼────┼────┼────┤│ 8 │ 9 │10 │11 │ 行 2├────┼────┼────┼────┤│12 │13 │14 │15 │ 行 3└────┴────┴────┴────┘现在我们希望在 kernel 中每次处理一个 2×2 的子块。 例如,假设当前线程块负责处理图中右下角的子块(包含元素 10, 11, 14, 15),它的左上角位于矩阵的第 2 行、第 2 列。
我们使用tl.make_block_ptr来描述这个子块:
a_block_ptr = tl.make_block_ptr( base=a_ptr, # ① 内存基地址 shape=(4, 4), # ② 完整矩阵形状 strides=(4, 1), # ③ 行步长、列步长 offsets=(2, 2), # ④ 子块左上角坐标 block_shape=(2, 2), # ⑤ 子块大小 order=(1, 0) # ⑥ 线程映射顺序)创建好这个块指针后,我们只需一行 tl.load(a_block_ptr),Triton 便会自动计算出所有需要访问的全局内存地址,并安全地处理边界情况。接下来,我们详细看看每个参数的具体作用。
参数详解
- base —— 内存基地址,即矩阵在 GPU 内存中的起始地址,通常通过 PyTorch Tensor 的 .data_ptr() 方法获得。
- shape —— 完整数据的逻辑形状. 此处的 (4, 4) 告诉 Triton 矩阵共有4行4列。
- strides —— 各维度的内存跨步. 这是连接“逻辑坐标”与“物理地址”的关键。对于行优先存储的矩阵:
沿着第 0 维(行) 移动 1,意味着从当前行跳到下一行,需要跨越一整行的元素,因此步长为 4(即一行有 4 个元素)。
沿着第 1 维(列) 移动 1,意味着在同一行内移动到下一列,相邻元素在内存中紧挨着,因此步长为 1。 所以 strides = (4, 1)。反之,如果矩阵是列优先存储,则 strides 应为 (1, 4)。
- offsets —— 子块左上角的起始坐标. 它是一个元组 (row_start, col_start)。
在实际 kernel 中,offsets 会根据线程块的 ID 动态计算,例如:
pid_m = tl.program_id(0) # 行方向的块 IDpid_n = tl.program_id(1) # 列方向的块 IDoffsets = (pid_m * BLOCK_M, pid_n * BLOCK_N)- block_shape —— 要加载的块大小.
这是我们在 kernel 中设定的超参数,例如 BLOCK_M = 2、BLOCK_N = 2。它决定了每次 tl.load 会从内存中读取多少个元素。合适的块大小需要根据共享内存容量、计算资源以及数据复用程度进行权衡,常见取值如 64×64、128×128 等。
- order —— 线程数据分布映射顺序
这个参数稍微有些进阶。简单来说,它告诉编译器数据在块内是如何分配给各个线程的,会影响生成的 PTX 代码结构。在大多数常规场景下(例如非 Tensor Core 的简单加载/存储),order=(0,1) 与 order=(1,0) 的性能差异微乎其微,因此你不必过分纠结。
不过,当你使用 NVIDIA Hopper 架构(H100 等)并希望启用 TMA(Tensor Memory Accelerator) 硬件加速时,正确设置 order 就显得至关重要了,它可以带来数倍的访存性能提升。一般情况下,对于矩阵乘法中的 A 矩阵加载,常用 order=(1,0);对于 B 矩阵,常用 order=(0,1)。若想深入探究,可以借我一张H100测试一下!
3.3. Naive, Vec, 2D 完整实现
import torchimport tritonimport triton.language as tlimport timeimport numpy as np
# 方案 1:直接加法@triton.jitdef matrix_add_kernel(a, b, c, n_elements, BLOCK_SIZE: tl.constexpr): pid = tl.program_id(axis=0)
block_start = pid * BLOCK_SIZE offsets = block_start + tl.arange(0, BLOCK_SIZE) mask = offsets < n_elements
ga = tl.load(a + offsets, mask=mask) gb = tl.load(b + offsets, mask=mask) gc = ga + gb tl.store(c + offsets, gc, mask=mask)
def solve_triton_naive(a: torch.Tensor, b: torch.Tensor, c: torch.Tensor, N: int): BLOCK_SIZE = 1024 n_elements = N * N grid = (triton.cdiv(n_elements, BLOCK_SIZE),) matrix_add_kernel[grid](a, b, c, n_elements, BLOCK_SIZE)
# 方案 2:Triton 一维向量化 + Autotune@triton.autotune( configs=[ triton.Config({'BLOCK_SIZE': 1024, 'VEC_WIDTH': 1}, num_warps=4), triton.Config({'BLOCK_SIZE': 1024, 'VEC_WIDTH': 2}, num_warps=4), triton.Config({'BLOCK_SIZE': 2048, 'VEC_WIDTH': 2}, num_warps=8), triton.Config({'BLOCK_SIZE': 4096, 'VEC_WIDTH': 4}, num_warps=8), triton.Config({'BLOCK_SIZE': 4096, 'VEC_WIDTH': 8}, num_warps=16), ], key=['n_elements'],)@triton.jitdef matrix_add_kernel_1d( a_ptr, b_ptr, c_ptr, n_elements: tl.constexpr, BLOCK_SIZE: tl.constexpr, VEC_WIDTH: tl.constexpr,): pid = tl.program_id(axis=0) block_start = pid * BLOCK_SIZE * VEC_WIDTH
offsets = block_start + tl.arange(0, BLOCK_SIZE)[:, None] * VEC_WIDTH + tl.arange(0, VEC_WIDTH)[None, :] offsets = tl.reshape(offsets, (BLOCK_SIZE * VEC_WIDTH,))
mask = offsets < n_elements
a_vals = tl.load(a_ptr + offsets, mask=mask, other=0.0) b_vals = tl.load(b_ptr + offsets, mask=mask, other=0.0) c_vals = a_vals + b_vals
tl.store(c_ptr + offsets, c_vals, mask=mask)
def solve_triton_1d(A: torch.Tensor, B: torch.Tensor, C: torch.Tensor, N: int): n_elements = N * N grid = lambda meta: (triton.cdiv(n_elements, meta['BLOCK_SIZE'] * meta['VEC_WIDTH']),) matrix_add_kernel_1d[grid](A, B, C, n_elements)
# 方案 3:Triton 二维块指针 + Autotune@triton.autotune( configs=[ triton.Config({'BLOCK_M': 128, 'BLOCK_N': 128}, num_warps=4), triton.Config({'BLOCK_M': 128, 'BLOCK_N': 256}, num_warps=4), triton.Config({'BLOCK_M': 256, 'BLOCK_N': 128}, num_warps=8), triton.Config({'BLOCK_M': 256, 'BLOCK_N': 256}, num_warps=8), triton.Config({'BLOCK_M': 512, 'BLOCK_N': 128}, num_warps=8), triton.Config({'BLOCK_M': 512, 'BLOCK_N': 256}, num_warps=8), triton.Config({'BLOCK_M': 512, 'BLOCK_N': 512}, num_warps=8), ], key=['N'],)@triton.jitdef matrix_add_kernel_2d( a_ptr, b_ptr, c_ptr, N, BLOCK_M: tl.constexpr, BLOCK_N: tl.constexpr,): pid_m = tl.program_id(axis=0) pid_n = tl.program_id(axis=1)
a_block_ptr = tl.make_block_ptr( base=a_ptr, shape=(N, N), strides=(N, 1), offsets=(pid_m * BLOCK_M, pid_n * BLOCK_N), block_shape=(BLOCK_M, BLOCK_N), order=(1, 0), ) b_block_ptr = tl.make_block_ptr( base=b_ptr, shape=(N, N), strides=(N, 1), offsets=(pid_m * BLOCK_M, pid_n * BLOCK_N), block_shape=(BLOCK_M, BLOCK_N), order=(1, 0), ) c_block_ptr = tl.make_block_ptr( base=c_ptr, shape=(N, N), strides=(N, 1), offsets=(pid_m * BLOCK_M, pid_n * BLOCK_N), block_shape=(BLOCK_M, BLOCK_N), order=(1, 0), )
a = tl.load(a_block_ptr, boundary_check=(0, 1)) b = tl.load(b_block_ptr, boundary_check=(0, 1)) c = a + b tl.store(c_block_ptr, c, boundary_check=(0, 1))
def solve_triton_2d(A: torch.Tensor, B: torch.Tensor, C: torch.Tensor, N: int): grid = lambda meta: ( triton.cdiv(N, meta['BLOCK_M']), triton.cdiv(N, meta['BLOCK_N']), ) matrix_add_kernel_2d[grid](A, B, C, N)
# ------------------------------------------------------------# 性能测试工具# ------------------------------------------------------------def benchmark(func, A, B, C, N, warmup=10, repeat=100): """ 运行指定函数多次,返回平均耗时(毫秒)。 """ # 预热 for _ in range(warmup): func(A, B, C, N) torch.cuda.synchronize()
# 计时 start = time.perf_counter() for _ in range(repeat): func(A, B, C, N) torch.cuda.synchronize() end = time.perf_counter()
avg_time_ms = (end - start) / repeat * 1000 return avg_time_ms
def verify_results(C_triton_naive, C_triton_1d, C_triton_2d): """ 验证三种方案结果是否一致。 """ if torch.allclose(C_triton_naive, C_triton_1d, atol=1e-5): print("✅ Naive Triton 与 Triton 1D 结果一致") else: print("❌ Naive Triton 与 Triton 1D 结果不一致")
if torch.allclose(C_triton_naive, C_triton_2d, atol=1e-5): print("✅ Naive Triton 与 Triton 2D 结果一致") else: print("❌ Naive Triton 与 Triton 2D 结果不一致")
def main(): # 检查 CUDA 可用性 if not torch.cuda.is_available(): raise RuntimeError("CUDA 不可用,请在有 GPU 的环境下运行") device = torch.device("cuda") print(f"运行设备: {torch.cuda.get_device_name(device)}")
# 问题规模 N = 4096
# 分配 GPU 张量 A = torch.randn(N, N, device=device, dtype=torch.float32) B = torch.randn(N, N, device=device, dtype=torch.float32) C_triton_naive = torch.empty_like(A) C_triton_1d = torch.empty_like(A) C_triton_2d = torch.empty_like(A)
print(f"\n矩阵大小: {N} x {N} ({N*N} 个元素)")
# 验证正确性 solve_triton_naive(A, B, C_triton_naive, N) solve_triton_1d(A, B, C_triton_1d, N) solve_triton_2d(A, B, C_triton_2d, N) verify_results(C_triton_naive, C_triton_1d, C_triton_2d)
# 性能测试 print("\n开始性能测试 (预热 10 次,计时 100 次取平均)...\n")
time_pytorch = benchmark(solve_triton_naive, A, B, C_triton_naive, N) time_triton_1d = benchmark(solve_triton_1d, A, B, C_triton_1d, N) time_triton_2d = benchmark(solve_triton_2d, A, B, C_triton_2d, N)
# 输出结果 print(f"Triton 直接加法: {time_pytorch:.4f} ms") print(f"Triton 1D 向量化: {time_triton_1d:.4f} ms") print(f"Triton 2D 块指针: {time_triton_2d:.4f} ms")
# 计算加速比 baseline = time_pytorch print(f"\n相对于 Naive 的加速比:") print(f" Triton 1D: {baseline / time_triton_1d:.2f}x") print(f" Triton 2D: {baseline / time_triton_2d:.2f}x")
# 计算内存带宽 bytes_per_element = 4 # float32 total_bytes = 3 * N * N * bytes_per_element # A读 + B读 + C写 bw_pytorch = total_bytes / (time_pytorch / 1000) / 1e9 bw_triton_1d = total_bytes / (time_triton_1d / 1000) / 1e9 bw_triton_2d = total_bytes / (time_triton_2d / 1000) / 1e9
print(f"\n估算内存带宽 (GB/s):") print(f" Triton Naive: {bw_pytorch:.2f} GB/s") print(f" Triton 1D: {bw_triton_1d:.2f} GB/s") print(f" Triton 2D: {bw_triton_2d:.2f} GB/s")
if __name__ == "__main__": main()4. CUDA 题解
在CUDA中,我也给出了三个版本的题解。
4.1. 一维Naive
__global__ void add_naive(const float* A, const float* B, float* C, int N) { int total = N * N; int tid = blockIdx.x * blockDim.x + threadIdx.x; if (tid < total) { C[tid] = A[tid] + B[tid]; }}特点:每个线程只处理一个矩阵元素,grid 需覆盖全部元素数量(约 65536 个 block)。
同一 warp 内线程 tid 连续,访问 A[tid]、B[tid]、C[tid] 均是连续地址。这是合并访问的范例,带宽利用率已然不低(通常 60‑70% 峰值)。
每个线程只做一次加法就去排队退休了。线程块启动、索引计算、条件判读等“调度开销”相对于实际计算过于沉重。
在我的GPU上,反而这个版本的速度一直都是最快的。
4.2. 一维 Grid‑Stride Loop
__global__ void add_grid_stride_scalar(const float* A, const float* B, float* C, int N) { int total = N * N; int tid = blockIdx.x * blockDim.x + threadIdx.x; int stride = gridDim.x * blockDim.x; for (int i = tid; i < total; i += stride) { C[i] = A[i] + B[i]; }}与朴素版最大的不同:grid 不再覆盖所有元素,而是固定为一个较小值(例如 SM 数量 × 8 个 block),让每个线程在循环中不断向前跳步。
为什么这样做更快?
- 减少线程块调度开销:只有少量 block 需要启动、轮转,硬件调度器压力骤降。
- 更高的 SM 占用率:所有 SM 从一开始就被填满,不存在尾部 block 带来的闲置。
- 编译器更容易优化循环:循环体内简单的 LD / FADD / ST 序列可以被流水线化,指令级并行度更高。
疑问:Grid‑Stride 版本中每个线程会依次访问多个不连续的地址(间隔 stride),这会不会破坏合并访问?
同一个 warp 的线程在同一迭代内访问的地址组还是连续的(
tid连续)。只是线程自己下一次访问地址跳到了tid + stride。单个 warp 一次事务的连续性没有变,所以合并访问特性完全保留。
4.3. 一维 Grid‑Stride + float4
const float4* A4 = reinterpret_cast<const float4*>(A);// ...for (int i = tid; i < num_vec; i += stride) { float4 a = __ldg(A4 + i); float4 b = __ldg(B4 + i); float4 c; c.x = a.x + b.x; c.y = a.y + b.y; c.z = a.z + b.z; c.w = a.w + b.w; C4[i] = c;}**为什么这里出现了 __ldg **
__ldg 强制通过只读缓存加载,不污染 L1。对于纯输入数据(A、B),这样既能利用缓存预取,又给 C 的写入保留 L1 容量。
在 Ada 架构以后,NVCC 对简单标量循环也能自动向量化。但显式写 float4 可以让你掌控对齐和缓存路径,在跨平台或对编译器能力不信任时仍是首选。
向量化的先决条件:
- 数据必须 16 字节对齐(
cudaMalloc默认满足)。 N*N能被 4 整除最好,否则需要尾部标量处理。
4.4. 二维 Grid‑Stride
__global__ void add_2d_grid_stride(const float* A, const float* B, float* C, int N) { int row = blockIdx.y * blockDim.y + threadIdx.y; int col = blockIdx.x * blockDim.x + threadIdx.x; int stride_y = gridDim.y * blockDim.y; int stride_x = gridDim.x * blockDim.x; for (int r = row; r < N; r += stride_y) for (int c = col; c < N; c += stride_x) C[r * N + c] = A[r * N + c] + B[r * N + c];}二维索引直观地映射到矩阵的行和列,让代码更像数学公式。但它真的更快吗?
内存访问分析:
对同一行内的连续 c 值,地址 r * N + c 是连续的——列方向的合并访问完好。但内层循环的跳步 c += stride_x 仍然保持连续特征,只是外层 r += stride_y 会让线程跳跃整行,此时访问变成大跨步。对于行主序存储,跨行访问并不破坏单次事务的连续性,所以性能与一维 Grid‑Stride 大致持平。
那什么时候用二维?
- 代码可维护性优先的场景(图像处理、矩阵分块)。
- 当后续计算需要利用二维数据布局时,二维映射能减少索引计算复杂度。
- 对于仅做逐元素操作的纯访存核函数,二维版本并无性能优势。
多数高性能基础算子库(如 cuBLAS、CUTLASS 的 element-wise 部分)内部依然采用一维 Grid‑Stride + 向量化,因为它的指令开销最小。
4.5. 性能对比与结论
Kernel Time(ms) BW(GB/s) %Peak--------------------------------------------------------------1D Naive 0.4483 449.1 89.11D Grid-Stride scalar 0.4693 429.0 85.11D Grid-Stride float4 0.4549 442.5 87.82D Grid-Stride scalar 0.4575 440.1 87.3
Speedup vs 1D Naive: 1D Grid-Stride scalar: 0.96x 1D Grid-Stride float4: 0.99x 2D Grid-Stride scalar: 0.98x
All kernels verified OK.4.6. 完整的CUDA实现
#include <cuda_runtime.h>#include <stdio.h>#include <stdlib.h>#include <math.h>
__global__ void add_naive(const float* __restrict__ A, const float* __restrict__ B, float* __restrict__ C, int N) { int total = N * N; int tid = blockIdx.x * blockDim.x + threadIdx.x; if (tid < total) { C[tid] = A[tid] + B[tid]; }}
__global__ void add_grid_stride_scalar(const float* __restrict__ A, const float* __restrict__ B, float* __restrict__ C, int N) { int total = N * N; int tid = blockIdx.x * blockDim.x + threadIdx.x; int stride = gridDim.x * blockDim.x; for (int i = tid; i < total; i += stride) { C[i] = A[i] + B[i]; }}
__global__ void add_grid_stride_vec4(const float* __restrict__ A, const float* __restrict__ B, float* __restrict__ C, int N) { int total = N * N; int tid = blockIdx.x * blockDim.x + threadIdx.x; int stride = gridDim.x * blockDim.x;
const float4* A4 = reinterpret_cast<const float4*>(A); const float4* B4 = reinterpret_cast<const float4*>(B); float4* C4 = reinterpret_cast<float4*>(C);
int num_vec = total / 4; for (int i = tid; i < num_vec; i += stride) { float4 a = __ldg(A4 + i); float4 b = __ldg(B4 + i); float4 c; c.x = a.x + b.x; c.y = a.y + b.y; c.z = a.z + b.z; c.w = a.w + b.w; C4[i] = c; } int rem = num_vec * 4; for (int i = rem + tid; i < total; i += stride) { C[i] = A[i] + B[i]; }}
__global__ void add_2d_grid_stride(const float* __restrict__ A, const float* __restrict__ B, float* __restrict__ C, int N) { int row = blockIdx.y * blockDim.y + threadIdx.y; int col = blockIdx.x * blockDim.x + threadIdx.x; int stride_y = gridDim.y * blockDim.y; int stride_x = gridDim.x * blockDim.x;
for (int r = row; r < N; r += stride_y) { for (int c = col; c < N; c += stride_x) { int idx = r * N + c; C[idx] = A[idx] + B[idx]; } }}
// ------------------------------------------------------------------// 辅助函数:理论带宽 (GB/s)// ------------------------------------------------------------------float theo_bw_GBs(int dev) { int clock_kHz, bus_width; cudaDeviceGetAttribute(&clock_kHz, cudaDevAttrMemoryClockRate, dev); cudaDeviceGetAttribute(&bus_width, cudaDevAttrGlobalMemoryBusWidth, dev); return (2.0f * clock_kHz * 1000.0f * (bus_width / 8)) / 1e9f;}
// ------------------------------------------------------------------// 辅助函数:benchmark(返回单次平均耗时 ms)// ------------------------------------------------------------------typedef void (*kernel_fn)(const float*, const float*, float*, int);float bench(kernel_fn fn, dim3 grid, dim3 block, const float* dA, const float* dB, float* dC, int N, int warmup, int iters, cudaStream_t s = 0) { for (int i = 0; i < warmup; ++i) fn<<<grid, block, 0, s>>>(dA, dB, dC, N); cudaDeviceSynchronize();
cudaEvent_t start, stop; cudaEventCreate(&start); cudaEventCreate(&stop); cudaEventRecord(start, s); for (int i = 0; i < iters; ++i) fn<<<grid, block, 0, s>>>(dA, dB, dC, N); cudaEventRecord(stop, s); cudaEventSynchronize(stop); float ms; cudaEventElapsedTime(&ms, start, stop); cudaEventDestroy(start); cudaEventDestroy(stop); return ms / iters;}
// ------------------------------------------------------------------// 辅助函数:验证// ------------------------------------------------------------------bool verify(const float* ref, const float* out, int N) { int total = N * N; for (int i = 0; i < total; ++i) { if (fabsf(ref[i] - out[i]) > 1e-5f) { printf(" mismatch at %d: %f vs %f\n", i, ref[i], out[i]); return false; } } return true;}
// ------------------------------------------------------------------// 主程序// ------------------------------------------------------------------int main() { int dev = 0; cudaSetDevice(dev); cudaDeviceProp prop; cudaGetDeviceProperties(&prop, dev); float peak_bw = theo_bw_GBs(dev);
const int N = 4096; const int total = N * N; size_t bytes = total * sizeof(float); int warmup = 5, iters = 10;
// 主机数据 float *hA = (float*)malloc(bytes); float *hB = (float*)malloc(bytes); float *hRef = (float*)malloc(bytes); float *hC = (float*)malloc(bytes); for (int i = 0; i < total; ++i) { hA[i] = (float)(rand() % 100) / 10.0f; hB[i] = (float)(rand() % 100) / 10.0f; hRef[i] = hA[i] + hB[i]; }
// 设备内存 float *dA, *dB, *dC; cudaMalloc(&dA, bytes); cudaMalloc(&dB, bytes); cudaMalloc(&dC, bytes); cudaMemcpy(dA, hA, bytes, cudaMemcpyHostToDevice); cudaMemcpy(dB, hB, bytes, cudaMemcpyHostToDevice);
printf("Device: %s | Peak BW: %.0f GB/s | %dx%d (%dM elements)\n\n", prop.name, peak_bw, N, N, total / (1024*1024));
int sm_count = prop.multiProcessorCount;
// ---------- 启动参数 ---------- const int block_1d = 256; dim3 grid_naive((total + block_1d - 1) / block_1d); // 覆盖全部元素 dim3 grid_gs(sm_count * 8); // 一维 Grid‑Stride
// 二维 Grid‑Stride: 8x8 个 block,每个 block 16x16 线程 dim3 grid_2d(8, 8); dim3 block_2d(16, 16);
// ---------- 运行 Kernel ---------- const int K = 4; // 四个 kernel float ms[K], bw[K]; bool ok[K];
// 1) Naive ms[0] = bench(add_naive, grid_naive, block_1d, dA, dB, dC, N, warmup, iters); cudaMemcpy(hC, dC, bytes, cudaMemcpyDeviceToHost); ok[0] = verify(hRef, hC, N); bw[0] = (3.0 * bytes) / (ms[0] * 1e6);
// 2) Grid‑Stride scalar ms[1] = bench(add_grid_stride_scalar, grid_gs, block_1d, dA, dB, dC, N, warmup, iters); cudaMemcpy(hC, dC, bytes, cudaMemcpyDeviceToHost); ok[1] = verify(hRef, hC, N); bw[1] = (3.0 * bytes) / (ms[1] * 1e6);
// 3) Grid‑Stride float4 ms[2] = bench(add_grid_stride_vec4, grid_gs, block_1d, dA, dB, dC, N, warmup, iters); cudaMemcpy(hC, dC, bytes, cudaMemcpyDeviceToHost); ok[2] = verify(hRef, hC, N); bw[2] = (3.0 * bytes) / (ms[2] * 1e6);
// 4) 二维 Grid‑Stride ms[3] = bench(add_2d_grid_stride, grid_2d, block_2d, dA, dB, dC, N, warmup, iters); cudaMemcpy(hC, dC, bytes, cudaMemcpyDeviceToHost); ok[3] = verify(hRef, hC, N); bw[3] = (3.0 * bytes) / (ms[3] * 1e6);
// ---------- 输出结果 ---------- printf("%-28s Time(ms) BW(GB/s) %%Peak\n", "Kernel"); printf("--------------------------------------------" "------------------\n"); printf("%-28s %7.4f %7.1f %5.1f\n", "1D Naive", ms[0], bw[0], bw[0]/peak_bw*100); printf("%-28s %7.4f %7.1f %5.1f\n", "1D Grid-Stride scalar", ms[1], bw[1], bw[1]/peak_bw*100); printf("%-28s %7.4f %7.1f %5.1f\n", "1D Grid-Stride float4", ms[2], bw[2], bw[2]/peak_bw*100); printf("%-28s %7.4f %7.1f %5.1f\n", "2D Grid-Stride scalar", ms[3], bw[3], bw[3]/peak_bw*100);
printf("\nSpeedup vs 1D Naive:\n"); printf(" 1D Grid-Stride scalar: %.2fx\n", ms[0]/ms[1]); printf(" 1D Grid-Stride float4: %.2fx\n", ms[0]/ms[2]); printf(" 2D Grid-Stride scalar: %.2fx\n", ms[0]/ms[3]);
bool all_ok = true; for (int i = 0; i < K; ++i) all_ok = all_ok && ok[i]; printf("\n%s\n", all_ok ? "All kernels verified OK." : "Verification FAILED.");
// 清理 cudaFree(dA); cudaFree(dB); cudaFree(dC); free(hA); free(hB); free(hRef); free(hC); return 0;}支持与分享
如果这篇文章对你有帮助,欢迎分享给更多人或赞助支持!
部分内容可能已过时