CUDA编程——性能优化基本技巧

news2025/5/16 1:16:00

本文主要介绍下面三种技巧:

  • 使用 __restrict__ 让编译器放心地优化指针访存
  • 想办法让同一个 Warp 中的线程的访存 Pattern 尽可能连续,以利用 Memory coalescing
  • 使用 Shared memory

0. 弄清Kernael函数是Compute-bound 还是 Memory-bound 

先摆出一个知识点,一般来说,Compute-bound 的 Kernel 不太常见,常见的 Compute-bound 的 Kernel 可能只有矩阵乘法与卷积核比较大的卷积,大多数都是Memory-bound,所以下面我们主要关注如何优化访存

在经典的冯诺依曼架构下,ALU (Arithmetic Logic Unit,计算逻辑单元,可以简单理解为加法器、乘法器等) 要从内存中取操作数,进行对应的计算(如乘法),并写回内存。所以,计算速度会受到两个因素的限制:ALU 进行计算的速度,与内存的存取速度。如果一个程序的运行速度瓶颈在于前者,那么称其为 Compute-bound 的;如果瓶颈在于后者,那么称其为 Memory-bound 的。

由于 CPU 中运算单元较少,且 CPU 具有多级缓存,所以空间连续性、时间连续性较好的程序在 CPU 上一般是 Compute-bound 的。而 GPU 则恰恰相反:GPU 的核心的规模一般很大,比如 RTX 4090 可以在一秒内做 82.58T 次 float16 运算(暂不考虑 Tensor core),但其内存带宽只有 1TB/s,每秒只能传输 0.5T 个 float16。这便导致 GPU 上的操作更可能会受到内存带宽的限制,成为 Memory-bound。

如何估测一个 CUDA Kernel 是 Compute-bound 还是 Memory-bound 呢?我们可以计算它的 “算存比”,也即,

 算存比 = 计算次数/访存次数

并将其与 GPU 的 每秒能做的运算次数/每秒能做的访存次数做比较。

例子:

__global__ void pointwise_add_kernel(int* C, const int* A, const int* B, int n) {
    for (int i = blockIdx.x * blockDim.x + threadIdx.x; i < n; i += blockDim.x * gridDim.x)
        C[i] = A[i] + B[i];
}

对于上面的 pointwise_add_kernel,其需要访问 3N 次内存(读取A[i]、读取B[i]、写入C[i]),同时做 N次加法,所以其存算比为 N/3N=1/3

对于RTX 4090 可以在一秒内做 82.58T 次 float16 运算(暂不考虑 Tensor core),但其内存带宽只有 1TB/s,每秒只能传输 0.5T 个 float16。起存算比为 82.58/0.5=165.16

因此这个 pointwise_add_kernel为 Memory-bound。

1. __restrict__

restrict关键字用于修饰指针。通过加上restrict关键字,编程者可提示编译器:在该指针的生命周期内,其指向的对象不会被别的指针所引用

 大家还记得什么是 Pointer aliasing 嘛?简单来说,下面两段代码并不是等价的:

void f1(int* x, int* y) {
    *x += *y;
    *x += *y;
}
void f2(int* x, int* y) {
    *x += 2*(*y);
}

这是因为,x 和 y 两个指针可能指向相同的内存。考虑 f(x, x),第一段代码将把 *x 变为 4(*x),而第二段代码则会把 *x 变为 3(*x)

Pointer aliasing 可能会抑制编译器做出某些优化。比如在上面的代码中,f1() 需要 5 次访存而 f2() 仅需三次,后者更优。但由于编译器并不能假设 x 和 y ,它不敢做这个优化。

所以,我们需要 “显式地” 告诉编译器,两个指针不会指向相同的内存地址(准确来说,应该是 “改变一个指针指向的地址的数据,不会影响到通过其他指针读取的数据”),从而让编译器“放心地” 做出优化nvcc 支持一个关键字,叫做 __restrict__,加上它,编译器就可以放心地把指针指向的值存在寄存器里,而不是一次又一次地访存,进而提高了性能

我们可以对比一下示例代码中的 gemm_gpu_mult_block_no_restrict.cu 与 gemm_gpu_mult_block.cu 的性能。在 4090 上,前者平均耗时 40420.75,后者平均耗时 3988.38。可以看出,性能提升幅度不容小觑。

为了验证性能下降确实是由于没有了 __restrict__ 关键字后的额外访存带来的,我们可以对比 gemm_gpu_mult_block.cu 与 gemm_gpu_mult_block_no_restrict_reg.cu 的性能。后者虽然没有使用 __restrict__ 关键字,但它把中间的累加结果存在了变量中,而不是每一次都写回 C 数组。在 4090 上,二者的性能非常相似。这说明,在缺少 __restrict__ 关键字的时候,代码需要进行许多不必要的访存,进而拖慢了速度。

2. Memory Coalescing内存合并

Memory Coalescing主要目的就是:充分利用内存带宽。

我们先来了解GPU的调度方式。Grid 里包含若干 Thread block,每个 Thread block 则又包含若干 Thread,那么这些 Thread 是如何被调度的呢?它们被按编号分成了若干组,每一组中有 32 个 Thread(即,线程 0 ~ 31 为第一组,32 ~ 63 为第二组,依次类推),这样的 “组” 便被叫做 Warp

GPU 的调度是以 Warp 作为基本单位的。每个时钟周期内,同一个 Warp 中的所有线程都会执行相同的指令。

(注意,thread, block, grid是软件概念,warp是硬件概念,如以下示意图所示)

(1)Transaction 的基本要求

  • 长度为 32 个 Byte:在 GPU 的内存访问中,每个事务(Transaction)的大小被固定为 32 字节。这是硬件设计上的一种规定,用于优化内存访问的效率。

  • 开始地址是 32 的倍数:事务(Transaction)的起始地址必须是 32 字节对齐的。也就是说,如果事务的起始地址是 A,那么 Amod32=0。这种对齐要求可以简化硬件设计,并提高内存访问的效率。

(2)Warp 中线程的内存访问模式

  • Warp:在 GPU 中,线程(Thread)是以 Warp 为单位进行调度的。一个 Warp 通常包含多个线程(例如 32 个线程)。

  • 线程的内存访问范围

    • 如果一个 Warp 中的第 i 个线程要访问地址范围为 4i∼4i+3 的内存,这意味着每个线程访问 4 字节的数据。

    • 由于每个事务(Transaction)的大小是 32 字节,而每个线程访问 4 字节,因此一个 Warp 中的 32 个线程总共需要 4 个事务来完成所有线程的内存访问。因为 32×4 字节 = 128 字节,而 128 字节正好可以分成 4 个 32 字节的事务。

然而,内存带宽是有上限的,且每一个 Transaction 的大小都是 32 Byte,这注定了每一秒 GPU 核心可以发起的 Transaction 数量是有上限的。

接下来请阅读 CUDA Best Practices,了解 Memory coalescing 在一个具体的例子中的优化效果。

总之,我们需要尽量保证同一个 Warp 中每一个 Thread 的访存是 coalesced 的,以充分利用内存带宽。

3. Shared Memory

Share memory 既可以用来在同一个 Thread block 的不同 Thread 之间共享数据(最常见的用法是 Reduction),也可以用来优化访存性能。我们现在主要关注后者。

在学习 Shared memory 之前,我们需要先了解一下 CUDA 的内存模型:

CUDA 中大致有这几种内存:

  • Global Memory:俗称显存,位于 GPU 核心外部,很大(比如 A100 有 80GB),但是带宽很有限
  • L2 Cache:位于 GPU 核心内部,是显存的缓存,程序不能直接使用
  • Register:寄存器,位于 GPU 核心内部,Thread 可以直接调用
  • Shared memory:位于 GPU 核心内部,每个 Thread block 中的所有 Thread 共用同一块 Shared memory(因此,Shared memory 可以用来在同一个 Thread block 的不同 Thread 之间共享数据),并且带宽极高(因此,Shared memory 可以用来优化性能)。

我们还是以矩阵乘法为例。在上面的 gemm_gpu_mult_block.cu 中,为了计算大小分别为 n×k 与 k×m的两个矩阵乘法,我们一共访问了大约 2nmk次内存。这十分不合算,因为三个矩阵加起来也就只有 nk+km+nm个元素。

Tiling

我们尝试使用 Shared memory 来优化矩阵乘法。具体的,我们使用一种叫做 Tiling 的技术。

Tiling 是一种将矩阵划分为小块(Tile)的技术,每个线程块处理一个 Tile。通过这种方式,可以将矩阵的子块加载到 Shared Memory 中,减少全局内存的访问次数,并提高内存访问的局部性。

接下来请阅读这篇文章Tiled Matrix Multiplication(里面有可视化图片)。

在阅读上面那篇文章之后,请阅读示例代码中的 gemm_gpu_tiling.cu,看看我如何实现 Tiling 版本的矩阵乘法。在 4090 上,gemm_gpu_mult_block 耗时 3988.38 us,gemm_gpu_tiling 耗时 311.38 us,性能提升约 10 倍。

#include "gemm_gpu_tiling.h"

#include <cassert>

#include <cuda_runtime_api.h>

constexpr int TILE_SIZE = 32;

// gemm_gpu_tiling - GEMM on GPU, using tiling & shared memory to optimize
// global memory accesses
__global__
void gemm_gpu_tiling_kernel(
	int* __restrict__ C,		// [n, m], on gpu
	const int* __restrict__ A,	// [n, k], on gpu
	const int* __restrict__ B,	// [k, m], on gpu
	const int n,
	const int m,
	const int k
) {
	// We copy the tile from a/b into shared memory, and then do the calculation
	__shared__ int a_tile[TILE_SIZE][TILE_SIZE];
	__shared__ int b_tile[TILE_SIZE][TILE_SIZE];
	int my_c_result = 0;
	for (int tile_index = 0; tile_index < k/TILE_SIZE; ++tile_index) {
		// Step 1. Load the tile from a/b into a/b_tile
		a_tile[threadIdx.y][threadIdx.x] = A[(blockIdx.x*TILE_SIZE + threadIdx.y)*k + (tile_index*TILE_SIZE + threadIdx.x)];
		b_tile[threadIdx.y][threadIdx.x] = B[(tile_index*TILE_SIZE + threadIdx.y)*m + (blockIdx.y*TILE_SIZE + threadIdx.x)];
		__syncthreads();
		// Step 2. Calculate the contribution to my_c_result
		for (int i = 0; i < TILE_SIZE; ++i) {
			my_c_result += a_tile[threadIdx.y][i] * b_tile[i][threadIdx.x];
		}
		__syncthreads();
	}
	// Step 3. Store my_c_result
	C[(blockIdx.x*TILE_SIZE + threadIdx.y)*m + (blockIdx.y*TILE_SIZE + threadIdx.x)] = my_c_result;
}

void gemm_gpu_tiling(
	int* __restrict__ C,		// [n, m], on gpu
	const int* __restrict__ A,	// [n, k], on gpu
	const int* __restrict__ B,	// [k, m], on gpu
	const int n,
	const int m,
	const int k
) {
	assert (n % TILE_SIZE == 0);
	assert (m % TILE_SIZE == 0);
	assert (k % TILE_SIZE == 0);
	dim3 grid_dim = dim3(n / TILE_SIZE, m / TILE_SIZE);
	dim3 block_dim = dim3(TILE_SIZE, TILE_SIZE);
	gemm_gpu_tiling_kernel<<<grid_dim, block_dim>>>(C, A, B, n, m, k);
}

参考资料:

主要参考:CUDA 编程入门 - HPC Wiki

https://github.com/interestingLSY/CUDA-From-Correctness-To-Performance-Code

https://zhuanlan.zhihu.com/p/349726808

https://hackmd.io/@yaohsiaopid/ryHNKkxTr

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2376481.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

道通EVO MAX系列无人机-支持二次开发

道通EVO MAX系列无人机-支持二次开发 EVO Max 系列采用Autel Autonomy自主飞行技术&#xff0c;实现复杂环境下的全局路径规划、3D场景重建、自主绕障和返航&#xff1b;高精度视觉导航能力&#xff0c;使其在信号干扰强、信号遮挡、信号弱等复杂环境下&#xff0c;依然获得高精…

计算机网络-MPLS LDP基础实验配置

前面我们学习了LDP的会话建立、标签发布与交换、LDP的工作原理&#xff0c;今天通过一个基础实验来加深记忆。 一、LDP基础实验 实验拓扑&#xff1a; 1、IGP使用OSPF进行通告&#xff0c;使用Lookback接口作为LSR ID&#xff0c;LDP ID自动生成。 2、实验目的&#xff1a;使…

HPE ProLiant DL360 Gen11 服务器,配置 RAID 5 教程!

今天的任务&#xff0c;是帮客户的一台HPE ProLiant DL360 Gen11 服务器&#xff0c;配置RAID 5。依然是按照我的个人传统习惯&#xff0c;顺便做一个教程&#xff0c;分享给有需要的粉丝们。如果你在实际操作中&#xff0c;遇到了什么问题&#xff0c;欢迎在评论区留言&#x…

SARIMA-LSTM融合模型对太阳黑子数量预测分析|附智能体数据代码

全文智能体链接&#xff1a;https://tecdat.cn/?p41969 分析师&#xff1a;Peng Fan 本研究以太阳黑子活动数据为研究对象&#xff0c;旨在帮助客户探索其未来走势并提供预测分析。首先&#xff0c;通过对数据的清洗和处理&#xff0c;包括离群值的识别与处理以及时间序列的建…

C# WinForm DataGridView 非常频繁地更新或重新绘制慢问题及解决

非常频繁地更新 DataGridView问题描述&#xff1a; 在 C# 中无法在合理的时间内刷新我的 DataGridView &#xff0c;我每秒通过网络发送 20 个数据包&#xff0c;获取数据。我想解析这些数据并将其放入 DataGridView 中。我还想调整 DataGridView 的更新间隔&#xff0c;从 0.1…

【数据结构】红黑树(C++)

目录 一、红黑树的概念 二、红黑树的性质 三、红黑树结点定义 四、红黑树的操作 1. 插入操作 1.1 插入过程 1.2 调整过程 1.2.1 叔叔节点存在且为红色 1.2.2 叔叔节点存在且为黑色 1.2.3 叔叔节点不存在 2. 查找操作 2.1 查找逻辑 2.2 算法流程图 2.3 使用示例 …

Android Framework学习五:APP启动过程原理及速度优化

文章目录 APP启动优化概述APP启动流程点击图片启动APP的过程启动触发Zygote 与应用进程创建Zygote进程的创建应用进程初始化 ApplicationActivity 启动与显示 优化启动时黑白屏现象可优化的阶段Application阶段相关优化 Activity阶段数据加载阶段 Framework学习系列文章 APP启动…

Meta的AIGC视频生成模型——Emu Video

大家好&#xff0c;这里是好评笔记&#xff0c;公主号&#xff1a;Goodnote&#xff0c;专栏文章私信限时Free。本文详细介绍Meta的视频生成模型Emu Video&#xff0c;作为Meta发布的第二款视频生成模型&#xff0c;在视频生成领域发挥关键作用。 &#x1f33a;优质专栏回顾&am…

Axure难点解决分享:统计分析页面引入Echarts示例动态效果

亲爱的小伙伴,在您浏览之前,烦请关注一下,在此深表感谢! Axure产品经理精品视频课已登录CSDN可点击学习https://edu.csdn.net/course/detail/40420 课程主题:统计分析页面引入Echarts示例动态效果 主要内容:echart示例引入、大小调整、数据导入 应用场景:统计分析页面…

Docker 常见问题及其解决方案

一、安装与启动问题 1.1 安装失败 在不同操作系统上安装 Docker 时&#xff0c;可能会出现安装失败的情况。例如&#xff0c;在 Ubuntu 系统中&#xff0c;执行安装命令后提示依赖缺失。这通常是因为软件源配置不正确或系统缺少必要的依赖包。 解决方案&#xff1a; 确保系统…

IC解析之TPS92682-Q1(汽车LED灯控制IC)

目录 1 IC特性介绍2 主要参数3 接口定义4 工作原理分析TPS92682-Q1架构工作模式典型应用通讯协议 控制帧应答帧协议5 总结 1 IC特性介绍 TPS92682 - Q1 是德州仪器&#xff08;TI&#xff09;推出的一款双通道恒压横流控制器&#xff0c;同时还具有各种电器故障保护&#xff0c…

6.01 Python中打开usb相机并进行显示

本案例介绍如何打开USB相机并每隔100ms进行刷新的代码,效果如下: 一、主要思路: 1. 打开视频流、读取帧 self.cam_cap = cv2.VideoCapture(0) #打开 视频流 cam_ret, cam_frame = self.cam_cap.read() //读取帧。 2.使用定时器,每隔100ms读取帧 3.显示到Qt的QLabel…

2023华为od统一考试B卷【二叉树中序遍历】

前言 博主刷的华为机考题&#xff0c;代码仅供参考&#xff0c;因为没有后台数据&#xff0c;可能有没考虑到的情况 如果感觉对你有帮助&#xff0c;请点点关注点点赞吧&#xff0c;谢谢你&#xff01; 题目描述 思路 0.用Character数组存储树&#xff0c;index下标的左右…

在Spark搭建YARN

&#xff08;一&#xff09;什么是SparkONYarn模式 Spark on YARN&#xff08;Yet Another Resource Negotiator&#xff09;是 Spark 框架在 Hadoop 集群中运行的一种部署模式&#xff0c;它借助 Hadoop YARN 来管理资源和调度任务。 架构组成 ResourceManager&#xff1a;作…

LeetCode_sql刷题(3482.分析组织层级)

题目描述&#xff1a;3482. 分析组织层级 - 力扣&#xff08;LeetCode&#xff09; 表&#xff1a;Employees ------------------------- | Column Name | Type | ------------------------- | employee_id | int | | employee_name | varchar | | manager_id …

不用服务器转码,Web端如何播放RTSP视频流?

在物联网、智慧城市、工业互联网等新兴技术浪潮下&#xff0c;实时视频流&#xff08;如RTSP协议&#xff09;作为安防监控、生产巡检、远程协作等场景的核心数据载体&#xff0c;其价值愈发凸显。然而&#xff0c;一个长期困扰行业的痛点始终存在——‌如何在Web浏览器中直接播…

如何开发一款 Chrome 浏览器插件

Chrome是由谷歌开发的网页浏览器&#xff0c;基于开源软件&#xff08;包括WebKit和Mozilla&#xff09;开发&#xff0c;任何人都可以根据自己需要使用、修改或增强它的功能。Chrome凭借着其优秀的性能、出色的兼容性以及丰富的扩展程序&#xff0c;赢得了广大用户的信任。市场…

GitHub打开缓慢甚至失败的解决办法

在C:\Windows\System32\drivers\etc的hosts中增加如下内容&#xff1a; 20.205.243.166 github.com 199.59.149.236 github.global.ssl.fastly.net185.199.109.153 http://assets-cdn.github.com 185.199.108.153 http://assets-cdn.github.com 185.199.110.153 http://asset…

18前端项目----Vue项目收尾优化|重要知识

收尾/知识点汇总 项目收尾二级路由未登录全局路由守卫路由独享守卫图片懒加载路由懒加载打包上线 重要知识点汇总组件通信方式1. props2. 自定义事件3. 全局事件总线4. 订阅与发布pubsub5. Vuex6. 插槽 sync修饰符attrs和listeners属性children和parent属性mixin混入作用域插槽…

仿RabbitMQ 模拟实现消息队列

文章目录 项目项目介绍开发环境技术选型 开始项目前第三方框架内容介绍muduo搭建服务端&#xff0c;客户端服务端&#xff1a;客户端&#xff1a;makefile muduo库protobuf通信服务端&#xff1a;客户端 sqlitegtest线程池future 认识&#xff0c;async使用promis使用package_t…