AMD HIP并行编程语言及其矢量相加实例——一文带你快速入门

  ✍️写在前面:随着计算的应用场景变得日益复杂多样,为了跟上人工智能算法对算力的需求,GPU硬件架构快速走向多样化,GPU生产厂家众多,且在商业和市场等因素的影响下,GPU通用计算编程模型也日益多元化。因此,并行编程语言的种类也变得繁杂,AMD作为一家在CPU和GPU领域都有较大影响力的企业,自然有属于自己成熟的计算生态,而HIP就是一个像CUDA一样的并行编程语言,但是目前网上关于HIP的资料并不是很多,所以我决定写一篇关于HIP并行编程基础的文章,希望这篇文章可以带大家快速入门。

  🥳这里先沾上目录:

目录

概述&背景

HIP编程模型

HIP编程结构

内存和线程管理

核函数的启动和编写

基于HIP的并行程序设计步骤

HIP程序示例-基于HIP的并行矢量相加

HIP时间计时API


概述&背景

  AMD 的 GPU 早期主要使用 OpenCL 来开发,后来为了提高开发效率,借鉴了很多 CUDA 的设计理念,推出了支持HIP( Heterogeneous-Computing Interface for Portability )和 OpenCL 编程的 ROCm 框架。ROCm在设计风格上类似于CUDA,提供了非常丰富的开发工具,包括 HCC编译器,性能分析工具rocProf,数学库如rocBLAS、rocFFT、rocSOLVER、rocSPARSE、rocRand、深度学习库MIopen等。

  HIP 是一个C++运行时API和内核语言,使用HIP编程模型编写的异构程序可以同时在类GPU加速卡平台和 NVIDIA GPU上运行。AMD HIP API接口与CUDA API相似。如图1所示。当涉及在 NVIDIA GPU 编译时HIP充当了CUDA之上的一个精简代理层,且其 Runtime API 兼容 CUDA runtime API。 ROCm 还提供了将CUDA应用程序通过使用HIPIFY工具自动转换为HIP内核语言及运行API的功能,当然,这种转码一般还需要手动进一步调整和优化代码。

图1 HIP栈和CUDA栈的比较

其中,HIP具有以下特点:

  1. 是开源的

  2. 为应用程序提供 API,以利用AMD和CUDA设备的GPU加速。

  3. 在语法上类似于CUDA。大多数CUDA API调用都可以就地转换:cuda -> hip

  4. 支持强大的 CUDA 运行时功能子集。

HIP编程模型

  HIP是一种基于AMD ROCm生态的并行计算平台和编程语言。你可以像编写C或C++语言程序一样实现算法和程序的并行性。同时,你也可以在类GPU加速平台和NVIDIA GPU上运行,本文将通过向量加法这个简单的例子来展示如何编写一个HIP程序,让你快速上手。

HIP编程结构

  通常在主机(CPU)端以串行代码为主,主要控制程序的逻辑。在设备(GPU)端通常以并行代码为主,主要实现代码的快速并行计算,而在设备(GPU)端设计和开发的代码被称之为内核代码,它是运行在设备端(GPU)上的。

一个典型的HIP程序实现流程应该遵循以下模式:

  1. 把数据从CPU内存拷贝到GPU显存;
  2. 调用和核函数对储存在GPU显存中的数据进行操作;
  3. 将数据从GPU显存传送回CPU内存;

  如图2所示。串行代码通常在主机CPU上执行,而并行代码在设备GPU上执行。用户可以将所有的代码统一的放在一个源文件中,然后通过HIP的编译器HCC为主机端和设备端生成可执行的代码。

图2 HIP程序执行流程

  HIP程序实现流程首先要把数据从CPU内存拷贝到GPU显存,数据在GPU操作结束后,又要将数据从GPU显存传送回CPU内存。因此首先要了解的就是内存和显存的管理,即如何将数据在主机和设备端进行传输和通信。以及内存和显存的分配方式。

内存和线程管理

  HIP并行编程语言的内存分配和管理与标准C函数基本相同,只是前面需要加上hip前缀。下面具体的来看一下内存分配和释放API的一个实例,假设要为一个有N个浮点类型元素的数组分配内存,主机端和设备端的分配方式如下代码所示:

size_t size = N * sizeof(float);
float *h_A= NULL, *d_A = NULL;
float *h_A = (float *)malloc(size);  //分配主机端内存h_A
hipMalloc((void **)&d_A, size);  //分配设备端显存d_A

  除了为主机端和设备端的数据分配内存和显存。在执行设备端代码的过程中,往往需要将主机端的数据传输到设备端预先分配好的显存之中。这一操作通过hipMemcpy进行实现,其定义如下所示:

hipError_t hipMemcpy(void *dist, const void* src,size_t count, hipMemcpyKind kind)

  其中此函数从src指向的源储存区复制一定数量的字节到dist指定的目标储存区,复制的方向由kind指定,其中数据传输和通信对应四种kind类型为:

数据通信方向

kind

主机端到设备端

hipMemcpyHostToDevice

设备端到主机端

hipMemcpyDeviceToHost

主机端到主机端

hipMemcpyHostToHost

设备端到设备端

hipMemcpyDeviceToDevice

  将数据从主机端传到设备端和从设备端传到主机端是不同的kind。因此在具体使用的过程中,注意不能混淆顺序。数据从主机端传到设备端从设备端传到主机端的具体代码示例如下所示:

hipMemcpy(d_A,h_A,size,hipMemcpyHostToDevice)
hipMemcpy(h_A,d_A,size,hipMemcpyDeviceToHost);

  上述代码的第一句将h_A中的数据从CPU端传输到设备端的显存d_A上,而第二句将将显存中d_A的数据从GPU端传输到设备端的h_A上,通过以上的数据传输API,便可以很容易的控制数据在主机端与设备端的通信和传输。

  另外,在一个并行HIP程序中,有关内存的操作除了内存分配以及数据传输,程序的最后一定不能忘记释放在程序中申请的内存和显存空间,其中,主机端内存和设备端的显存释放如下代码所示:

hipFree(d_A);
free(h_A);

  当你设计的内核函数在设备端进行计算时,设备中会产生大量的线程,并且每个线程都会按照之前设计好的核函数语法进行计算和控制,HIP沿用了CUDA的线程层次结构设计。将线程的层次进行抽象以便开发者组织线程。具体的,主要是一个三层的线程层次结构。从大到小依次是线程块网络,线程块,线程。其结构如图3所示。

图3  HIP的线程组织结构

  当启动内核函数在设备端进行计算时所产生的所有线程组成线程块网络。线程块网络中的所有线程共享相同的全局内存空间。线程块网络的维度大小由参数hipGirdDim定义。用于表示一个线程块网络中线程块的维度信息。它是一个dim3类型变量,dim3是基于uint定义的整数行向量,用来表示维度。当定义一个dim类型的变量时,所有未制定的元素都被初始化为1。Dim3类型变量中的每个组件可以通过它的x、y、z字段获得,如下所示:

  • hipGirdDim.x, hipGirdDim.y, hipGirdDim.z

  线程块网络是由线程块组成的。线程块的维度由hipBlockDim参数定义。用于表示一个线程块中线程的维度信息。它和girdDim一样是一个dim3类型变量,因此,对于hipBlockDim中的每个组件同样可以通过它的x、y、z字段获得,如下所示:

  • bhpBlockDim.x, hipBlockDim.y, hipBlockDim.z

  另外,并行程序的开发过程中通常需要确定线程块在线程网络中的位置信息,HIP为此提供了相应的API方法,它在线程块中的索引由参数hipBlockIdx决定。该坐标变量是基于uint3定义的内置的向量类型,它是一个包含三个无符号整数的结构,可以通过x、y、z三个字段来指定。即线程块在线程网络中的位置信息由以下三个变量所组合成的坐标确定:

  • (hipBlockIdx.x, hipBlockIdx.y, hipBlockIdx.z)

  而线程块是由多线程组成的。和参数hipBlockIdx类似,一个线程在线程块中的索引由参数hipThreadIdx决定。即线程在线程块中的位置信息由以下三个变量所组合成的坐标确定:

  • (hipThreadIdx.x, hipThreadIdx.y, hipThreadIdx.z)

  另外,在上述的示例中,都是以三维的网络和块进行说明。在实际并行程序开发过程中,可以根据实际情况组织二维的网络和块或一维的网络和块。

  同时,在具体的并行程序开发过程中。通常需要事先指定需要开辟的线程网络的维度和大小以及线程块的维度和大小信息。可以通过以下的方式进行定义。

  • dim3 blockDim(10);
  • dim3 gridDim(10);

  上述示例代码定义了一个一维的网络和块,共有10个Block,每个Block有10个Thread,同样的二维和三维可以通过增加()中的数据维度进行定义,例如dim3 block(10,10)表示每个Block有100个(10× 10)Thread。

核函数的启动和编写

  在传统的C语言编程中,假如你定义了一个函数Function_name(argument list)。那么之后在需要用到这个函数功能的时候,只需要调用即可。调用的形式如下代码所示。

Function_name(argument list)

  而对于HIP来说。它是基于C语言的延伸。因此它的调用语句和C函数相似,具体如下所示。

hipLaunchKernelGGL(argument list);

  其中,参数列表包括核函数名、网络和块布局、共享内存的大小以及核函数所带的参数,在上述的调用语句代码的参数列表中。需要指定girdDim、blockDim这两个参数。第1个参数是网络的维度和大小,也就是需要启动的线程块的数量。第2个参数是线程块的维度和大小,也就是需要启动的每个块中的线程的个数。正如上面提到的在进行实际的并行应用程序开发时,需要事先指定这两个参数的维度和大小。这样可以方便开发者调用和管理线程。
  同一个线程块中的线程往往可以相互协作,不同块之间的线程不能协作。对于一个给定的实际问题,可以使用不同的网络和块布局来组织线程。例如。需要实现256× 4096个元素的计算。每256个元素一个块,启动4096个块。图4表明了上述配置的线程分布。以及线程组织结构中各个参数大小。

图4 线程配置和结构分布图

  在设备端的全局内存中,因为数据是线性存储的。通常使用线程的全局索引来表示线程的全局位置,对于二维网络和块布局来说,此位置的数值可以由下式推出

  • Index_x= hipBlockIdx.x* hipBlockDim.x+ hipThreadIdx.x
  • Index_y= hipBlockIdx.y* hipBlockDim.y+ hipThreadIdx.y

  得到上述线程的全局位置索引后,便可以使用这一索引信息来进行并行程序的算法控制。

  核函数通常是在设备端运行的代码。因此在算法的设计过程中,通常将需要高度并行的相同计算操作设计成核函数。当核函数被调用时,不同的线程同步执行这一过程,从而达到提高计算效率的目的。核函数的定义通常需要用声明。同时核函数必须有一个void返回类型,如下所示:

  • __global__ void KernelFunction_name (argument list)

  下面来考虑一个简单的例子。假如要实现两个大小为numElements的向量相加。如果使用CPU进行串行程序设计。其代码如下所示:

void vectorAdd(float *h_A,float  *h_B,float  *h_C,int numElements)
{for(int i = 0; i < numElements; i++)h_C[i] = h_A[i] + h_B[i];
}

如果使用HIP进行并行程序设计。核函数如下所示:

__global__ void vectorAdd(float *d_A,float  *d_B,float  *d_C,int numElements)
{int i = hipBlockIdx_x * hipBlockDim_x + hipThreadIdx_x;if(i<numElements){d_C[i] = d_A[i] + d_B[i];} 
}

  观察上述两个函数的代码,可以发现在GPU上设计的核函数没有了循环体,向量的索引通过不同线程的全局索引来进行确定,另外,如向量相加GPU核代码第4行所示,当所开辟出来的线程数大于所要计算的数组元素时,需要用if语句来进行判断。否则会导致数组越界的错误。

基于HIP的并行程序设计步骤

  经过上文对HIP编程模型的理解,总结一下,HIP并行程序设计主要可分为以下5个步骤

(1)分配内存(host和device)

  Host:malloc()

  Device:hipMalloc ()

(2)从host将数据拷贝到device上

  hipMemcpy(device_data,host_data,size_data,hipMemcpyHosttoDevice)

(3)调用Hip的核函数在device上完成指定的运算;

  调用:hipLaunchKernelGGL(参数);

  核函数:_global_void 核函数名(参数){函数体}

(4)将device上的运算结果拷贝到host上;

  hipMemcpy(host_data,device_data, size_data,hipMemcpyDevicetoHost)

(5)释放分配的内存(device和host)

  Host:free()

  Device:hipFree()

HIP程序示例-基于HIP的并行矢量相加

  根据前文中的内容,编写一个完整的矢量相加HIP并行代码,实现两个大小为numElements的向量相加的完整HIP并行代码。另外,由于HIP许多调用是异步进行的,所以有时可能很难确定某个错误是由哪一步的程序引起的。所以在代码开发的过程中,可以定义一个错误处理宏CHECK封装所有的HIP API调用。这可以简化错误检查的过程。编写完程序后,将程序文件命名为hip_vectorAdd.cpp,对这个代码文件进行编译和执行,具体的编译指令为hipcc hip_vectorAdd.cpp -o hip_vectorAdd。

  代码如下所示:

#include <stdio.h>
#include <stdlib.h>
#include <hip/hip_runtime.h>
#include <hip/hip_runtime_api.h>__global__ void vectorAdd(float *d_A,float  *d_B,float  *d_C,int numElements){int i = hipBlockIdx_x * hipBlockDim_x + hipThreadIdx_x;if(i<numElements){d_C[i] = d_A[i] + d_B[i];}}int main(int argc,char **argv)
{int numElements = 50000;size_t size = numElements * sizeof(float);printf("[Vector addition of %d elements]\n", numElements);//1.申请Host内存并初始化float *h_A = (float *)malloc(size);float *h_B = (float *)malloc(size);float *h_C = (float *)malloc(size);for (int i = 0; i < numElements; ++i){h_A[i] = rand()/(float)RAND_MAX;h_B[i] = rand()/(float)RAND_MAX;}//1.申请Device内存float *d_A = NULL;hipMalloc((void **)&d_A, size);float *d_B = NULL;hipMalloc((void **)&d_B, size);float *d_C = NULL;hipMalloc((void **)&d_C, size);//2.将两个向量从Host端提交到Device端hipMemcpy(d_A,h_A,size,hipMemcpyHostToDevice);hipMemcpy(d_B,h_B,size,hipMemcpyHostToDevice);//3.调用hip核函数    int threadsPerBlock = 256;int blocksPerGrid =(numElements+ threadsPerBlock - 1) / threadsPerBlock;hipLaunchKernelGGL(vectorAdd,blocksPerGrid, threadsPerBlock,0,0,d_A,d_B,d_C,numElements);printf("HIP kernel launch with %d blocks of %d threads\n", blocksPerGrid, threadsPerBlock);//4.将两个向量相乘的结果从Device端传回Host端hipMemcpy(h_C,d_C,size,hipMemcpyDeviceToHost);//对比CPU和GPU计算结果误差for (int i = 0; i < numElements; ++i){if (fabs(h_A[i] + h_B[i] - h_C[i]) > 1e-8){fprintf(stderr, "Result verification failed at element %d!\n", i);exit(EXIT_FAILURE);}}//5.释放内存hipFree(d_A);hipFree(d_B);hipFree(d_C);free(h_A);free(h_B);free(h_C);return 0;}

HIP时间计时API

  最后,再提一下HIP中的异步记使API,帮助分析程序性能。在并行程序的开发中,往往需要对并行程序的运行时间进行测量从而验证和进一步开发性能,然而对于并行程序来说它是异步执行的,因此,单纯使用标准C语言的time计时往往不准确,HIP为此提供了专门的事件和计时API,HIP事件是hipEvent_t类型,通过hipEventCreate()和hipEventDestroy()进行事件的创建和销毁。事件创建后,就可以使用事件来记录并行程序的运行时间,具体有以下三个过程。

  1. hipEventRecord()记录默认流事件。

  2. hipEventSynchronize ()用来阻塞CPU执行直到指定的事件被记录。

  3. hipEventElapsedTime()的第一个参数返回默认流事件start和默认流事件stop两个记录之间消逝的毫秒时间。


最后的最后,希望本文能为你带来帮助,如果你觉得有用,希望能三连支持,你的鼓励是我持续创作的动力!😁

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

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

相关文章

Gateway一个诡异问题处理过程

一、前言 我们搭好了网关和一个基础微服务&#xff08;含用户体系、门店服务、商品服务、客户服务&#xff09;&#xff0c;然后用APIfox测试过程中发现通过网关入口请求某些接口&#xff0c;一段时间后返回错误&#xff0c;查看系统日志发现除了报There is no session with i…

流程封装与基于加密接口的测试用例设计

接口测试仅仅掌握 Requests 或者其他一些功能强大的库的用法&#xff0c;是远远不够的&#xff0c;还需要具备能根据公司的业务流程以及需求去定制化一个接口自动化测试框架的能力。所以&#xff0c;接下来&#xff0c;我们主要介绍下接口测试用例分析以及通用的流程封装是如何…

并发编程 -常用并发设计模式

1. 优雅终止线程的设计模式 思考&#xff1a;在一个线程 T1 中如何优雅的终止线程 T2&#xff1f; 错误思路1&#xff1a;使用线程对象的 stop() 方法停止线程 stop 方法会真正杀死线程&#xff0c;如果这时线程锁住了共享资源&#xff0c;那么当它被杀死后就再也没有机会释 …

postgresql的windows

1. 资源下载&#xff1a; https://www.postgresql.org/download/windows/ 2. 安装 双击&#xff0c;指定D盘目录&#xff0c;接下来默认安装&#xff0c;一直到出现下面的最后一步。一定要去除勾选复选框。 在最后&#xff0c;点击FINISH。 3. 初始化 4. 检查和修改配置 1&am…

数据结构:优先级队列(堆)

概念 优先级队列是啥&#xff1f; 队列是一种先进先出 (FIFO) 的数据结构 &#xff0c;但有些情况下&#xff0c; 操作的数据可能带有优先级&#xff0c;一般出队 列时&#xff0c;可能需要优先级高的元素先出队列。 在这种情况下&#xff0c; 数据结构应该提供两个最基本的…

converted from warning

converted from warning 关注微信&#xff1a;生信小博士 本地或者其它服务器跑同样的代码是正常的&#xff0c;只是有警告&#xff0c;但是在西柚云服务器上面运行会报错&#xff1f; 这是由于您两个环境使用的包版本不一样导致的&#xff0c;有如下解决方法 或者之前只是告警…

Jetpack Compose | State状态管理及界面刷新

我们知道Jetpack Compose&#xff08;以下简称Compose&#xff09;中的 UI 可组合项是通过Composable 声明的函数来描述的&#xff0c;如&#xff1a; Composable fun Greeting() {Text(text "init",color Color.Red,modifier Modifier.fillMaxWidth()) }上面的代…

MySQL实战1

文章目录 主要内容一.墨西哥和美国第三高峰1.准备工作代码如下&#xff08;示例&#xff09;: 2.目标3.实现代码如下&#xff08;示例&#xff09;: 4.相似例子代码如下&#xff08;示例&#xff09;: 二.用latest_event查找当前打开的页数1.准备工作代码如下&#xff08;示例&…

C++设计模式_20_Composite 组合模式

Composite 组合模式和后面谈到的Iterator&#xff0c;Chain of Resposibility都属于“数据结构”模式。Composite 组合模式核心是通过多态的递归调用解耦内部和外部的依赖关系。 文章目录 1. “数据结构”模式1.1 典型模式 2. 动机( Motivation )3. 模式定义4. Composite 组合模…

科普|电源自动测试系统测试的项目都有哪些?

电源自动测试系统是一种用于电源性能自动测试的集成系统&#xff0c;它可以自动检测电源模块或开关电源的输入、输出、保护等各个方面。该系统通常由数据软件和各类硬件测试仪器共同组成&#xff0c;利用通讯总线、测试夹具以及其它线缆等将仪器进行连接组成整体的系统结构&…

day14_集合

今日内容 零、 复习昨日 一、集合框架体系 二、Collection 三、泛型 四、迭代 五、List(ArrayList、LinkedList) 零、 复习 throw和throws什么区别 throwthrows位置方法里面方法签名上怎么写throw 异常对象throws异常类名(多个)作用真正抛出异常对象声明抛出的异常类型 运行时…

成本预算管理系统

成本预算管理系统 功能介绍&#xff1a; 一 基本信息&#xff1a; 1、产品设置&#xff1a;产品的长、宽、高及面积计算公式的设置。 2、板材设置&#xff1a;板材类别、厚度、尺寸的设置 3、系统名称&#xff1a;风管系统的类别设置 4、公司信息&#xff1a;本公司的信息…

【多线程】线程互斥 {竞态条件,互斥锁的基本用法,pthread_mutex系列函数,互斥锁的原理;死锁;可重入函数和线程安全}

一、进程线程间通信的相关概念 临界资源&#xff1a;多线程执行流共享的资源就叫做临界资源。确切的说&#xff0c;临界资源在同一时刻只能被一个执行流访问。临界区&#xff1a;每个线程内部&#xff0c;访问临界资源的代码&#xff0c;就叫做临界区。互斥&#xff1a;通过互…

基于鸟群算法的无人机航迹规划-附代码

基于鸟群算法的无人机航迹规划 文章目录 基于鸟群算法的无人机航迹规划1.鸟群搜索算法2.无人机飞行环境建模3.无人机航迹规划建模4.实验结果4.1地图创建4.2 航迹规划 5.参考文献6.Matlab代码 摘要&#xff1a;本文主要介绍利用鸟群算法来优化无人机航迹规划。 1.鸟群搜索算法 …

0基础学习PyFlink——用户自定义函数之UDF

大纲 标量函数入参并非表中一行&#xff08;Row&#xff09;入参是表中一行&#xff08;Row&#xff09;alias PyFlink中关于用户定义方法有&#xff1a; UDF&#xff1a;用户自定义函数。UDTF&#xff1a;用户自定义表值函数。UDAF&#xff1a;用户自定义聚合函数。UDTAF&…

vue2+ant-design-vue a-select组件二次封装(支持单选/多选添加全选/分页(多选跨页选中)/自定义label)

一、效果图 二、参数配置 1、代码示例 <t-antd-selectv-model"selectVlaue":optionSource"stepList"change"selectChange" />2、配置参数&#xff08;Attributes&#xff09;继承 a-select Attributes 参数说明类型默认值v-model绑定值…

vivado crash

将增量编译去了

FPGA时序分析与约束(9)——主时钟约束

一、时序约束 时序引擎能够正确分析4种时序路径的前提是&#xff0c;用户已经进行了正确的时序约束。时序约束本质上就是告知时序引擎一些进行时序分析所必要的信息&#xff0c;这些信息只能由用户主动告知&#xff0c;时序引擎对有些信息可以自动推断&#xff0c;但是推断得到…

Sprint Cloud Stream整合RocketMq和websocket实现消息发布订阅

1.引入RocketMQ依赖&#xff1a;首先&#xff0c;在pom.xml文件中添加RocketMQ的依赖&#xff1a; <dependency><groupId>org.apache.rocketmq</groupId><artifactId>rocketmq-spring-boot-starter</artifactId><version>2.2.0</versi…

文件改名,轻松添加前缀顺序编号,文件改名更高效!

您是否曾经需要批量修改文件名&#xff0c;并希望在文件名中添加特定的前缀或顺序编号&#xff1f;现在&#xff0c;我们为您带来了一款全新的文件改名工具&#xff0c;帮助您轻松解决这个问题&#xff01; 第一步&#xff0c;进入文件批量改名高手主页面&#xff0c;在板块栏…