hipHostMalloc
是 HIP API 中的一个函数,它用于在主机上分配特殊的内存,这种内存被称为 "pinned" 或 "page-locked" 内存。这种内存有以下特点和用途:
GPU 地址空间映射:分配的内存被映射到系统中所有 GPU 的地址空间。这意味着 GPU 可以直接访问这块内存,而不需要数据复制操作。
加速数据传输:使用 hipHostMalloc
分配的内存可以加速主机(Host)到设备(Device)以及设备到主机的数据传输速度。由于这种内存已经被映射到 GPU 的地址空间,因此在数据传输时可以减少一些设置步骤。
零拷贝 GPU 访问:GPU 能够直接访问使用 hipHostMalloc
分配的主机内存,无需复制数据到 GPU 的本地设备内存。这种特性称为 "zero-copy" 访问。然而,尽管避免了复制操作,但每次内核访问内存时,数据必须通过 CPU/GPU 互连传输,这可能比直接访问 GPU 的本地内存慢很多。
适用场景:
一致性与缓存:
工具使用:可以使用 hipBusBandwidth
工具来测试和比较使用 hipHostMalloc
分配的内存与普通内存在数据传输速度上的差异。通过 --unpinned
和 --pinned
开关来测试不同的内存配置。
总结来说,hipHostMalloc
提供了一种方式来分配可以被 GPU 直接访问的主机内存,这可以用于加速数据传输和实现零拷贝访问,但同时也需要考虑内存访问模式和数据一致性的需求。
hipHostMallocPortable
和 hipHostMallocMapped
是用于 hipHostMalloc
函数的标志(flags),它们定义了不同但相关的内存特性:
hipHostMallocPortable:
hipHostMallocPortable
可以确保分配的内存对所有参与的 GPU 都是可访问的。这种内存通常用于多 GPU 环境,其中不同的 GPU 需要直接访问同一块内存空间。hipHostMallocMapped:
区别主要在于它们的使用目的和行为:
hipHostMallocPortable
关注的是内存的可移植性和共享性,确保在多 GPU 系统中,所有 GPU 都能够识别和使用同一块内存。hipHostMallocMapped
关注的是内存访问的直接性和性能,允许 GPU 直接与主机内存交互,避免了数据传输的需要。在实际使用中,hipHostMalloc
通常会同时设置这两个标志,因为它们共同支持了多 GPU 环境下的高性能内存访问模式。这样分配的内存既能够被多个 GPU 共享,又能够被每个 GPU 直接访问,从而实现高效的数据传输和零拷贝操作。
hipSetDevice
API切换到不同的GPU时,可以访问之前分配的主机内存。但是,如果新选择的GPU与该内存的NUMA节点距离较远,可能会增加内存访问延迟。hipHostMallocNumaUser
标志进行NUMA-aware的主机内存分配。这允许开发者根据NUMA策略手动指定内存应该分配在哪个NUMA节点上。在 HIP (Heterogeneous-compute Interface for Portability) 中,托管内存分配(Managed Memory Allocation)是一种允许在 GPU 和 CPU 之间共享内存的机制,无需显式地在两者之间复制数据。
在调用托管内存 API hipMallocManaged
之前,HIP 应用程序会执行能力检查,以确保当前设备支持托管内存。
hipMallocManaged
调用将退回到使用系统内存。在这种情况下,其他托管内存 API 调用可能会表现出未定义的行为。hipMallocManaged
分配的内存可以提供更好的性能,因为它允许 GPU 和 CPU 透明地访问同一块内存,避免了数据复制的开销。HIP Stream Memory Operations 是一组 HIP API,它们提供对流(stream)内存操作的支持,使得网络节点(如 CPU 或其他 GPU 设备)与 GPU 之间能够直接同步。
hipStreamWaitValue32/64:
hipStreamWaitValue32
用于 32 位值,而 hipStreamWaitValue64
用于 64 位值。流将阻塞,直到内存中的值与预期值匹配。hipStreamWriteValue32/64:
hipStreamWriteValue32
用于写入 32 位值,而 hipStreamWriteValue64
用于写入 64 位值。这可以用于设置信号或其他同步机制。volatile
关键字。这是因为编译器默认会对内存访问进行优化,但信号量需要保证每次访问都是直接对内存的操作,而不是从寄存器或缓存中读取。volatile
告诉编译器,该变量可能会在程序的控制之外发生变化,因此每次使用时都必须从内存中读取,而不是假设它的值。ROCm defines two coherency options for host memory:
一致性内存(Coherent memory):
threadfence_system
和 C++11 风格的原子操作。非一致性内存(Non-coherent memory):
HIP 控制逻辑:
分配标志和环境变量控制逻辑:
未传递标志:
HIP_HOST_COHERENT
环境变量。hipHostMallocCoherent=1:
HIP_HOST_COHERENT
环境变量。hipHostMallocMapped=1:
HIP_HOST_COHERENT
环境变量。hipHostMallocNonCoherent=1, hipHostMallocCoherent=0, hipHostMallocMapped=0:
HIP_HOST_COHERENT
环境变量。其他 HostMalloc 标志设置,但未设置 hipHostMallocCoherent 或 hipHostMallocNonCoherent:
HIP_HOST_COHERENT
被定义为 1,则主机内存分配是一致性的。HIP_HOST_COHERENT
未定义或定义为 0,则主机内存分配是非一致性的。hipHostMallocCoherent=1 和 hipHostMallocNonCoherent=1:
开发者可以根据应用程序的需求选择适当的内存一致性类型。如果需要在 GPU 内核执行期间进行原子操作和同步,应选择一致性内存。如果性能是关键考虑因素且可以避免细粒度同步,则可以选择非一致性内存。通过合理选择内存一致性类型,可以优化应用程序的性能和同步行为。
零拷贝主机内存的可见性(visibility)是指在 GPU 执行过程中,对主机内存所做的更改何时对其他设备(如 CPU 或其他 GPU)可见。
hipEventSynchronize
是 HIP API 中的一个函数,用于确保某个事件(event)在执行特定同步操作之前已经完成。
事件(Event)的作用:
hipEventSynchronize 的用途:
hipEventSynchronize(event)
时,你是在告诉 HIP 运行时等待(同步)直到该事件完成。这意味着它将阻塞当前线程,直到事件所标志的操作在 GPU 上执行完毕。hipEventSynchronize
控制 hipEvents
的释放范围。同步的级别:
hipEventCreateWithFlags
使用 hipEventReleaseToSystem
标志,可以创建一个在系统级别上同步的事件。这种类型的事件在记录时会使所有主机内存(无论是一致性还是非一致性)对系统中的其他代理(如 CPU 或其他 GPU)可见。为什么使用 hipEventSynchronize:
hipEventSynchronize
可以确保测量的准确性。性能影响:
hipEventSynchronize
可能会导致性能下降,因为它强制执行线程等待 GPU 完成指定的操作。因此,它应该谨慎使用,特别是在性能敏感的应用程序中。hipEventCreateWithFlags 选项:
hipEventReleaseToSystem
:当事件被记录时执行系统范围的释放操作。这将使一致性和非一致性主机内存对系统中的其他代理可见,但可能涉及重量级操作,如缓存刷新。hipEventDisableTiming
:使用此标志创建的事件不记录分析数据,因此,如果用于同步,可以提供最佳性能。内存一致性与事件:
hipEventReleaseToSystem
。注意事项:
hipExtLaunchKernelGGL
或 hipExtLaunchKernel
进行内核调度时,通过 API 传递的事件不会被显式记录,并且应该仅用于获取特定启动的经过时间。hipExtLaunchKernelGGL
/hipExtLaunchKernel
调用的开始和停止事件,这些未记录的事件将被视为无效,HIP 可能会从 hipEventElapsedTime
显示 "hipErrorInvalidHandle" 错误。总结和建议:
threadfence_system
)透明地工作。Direct Dispatch 是 HIP 运行时的一个特性,它改变了传统的生产者-消费者模型,提高了 HIP 流的命令调度效率。
传统调度模型:
Direct Dispatch 模型:
Direct Dispatch 的优势:
如何禁用 Direct Dispatch:
如果出于某些原因需要禁用 Direct Dispatch(例如,与现有应用程序的兼容性问题),可以通过设置环境变量 AMD_DIRECT_DISPATCH=0
来实现。
结论:
Direct Dispatch 是 HIP 为了提高 GPU 计算任务的执行效率而设计的特性。它通过减少 CPU 和 GPU 之间的调度延迟,使得 GPU 任务的启动更加迅速,从而提高整体性能。然而,是否使用 Direct Dispatch 应根据应用程序的具体需求和性能测试结果来决定。
HIP Runtime Compilation(简称 hipRTC)是 HIP API 的一个特性,它支持在程序运行时编译 GPU 执行的内核代码。
运行时编译的优势:
即时优化:
适应不同硬件:
简化开发流程:
hipRTC 的基本工作流程:
创建 hipRTC 程序:
hiprtcCreateProgram
创建一个 hipRTC 程序对象。编译 HIP 源码:
hiprtcCompileProgram
函数进行编译。获取编译结果:
加载和执行:
销毁 hipRTC 程序:
hiprtcDestroyProgram
销毁 hipRTC 程序对象,释放相关资源。示例代码
// 假设我们有一个名为 "kernel.cu" 的 HIP 源文件,内容存储在 hipSource 字符串中
const char* hipSource = "-- Your HIP kernel code here --";
// 创建 hipRTC 程序
hiprtcProgram program;
hiprtcCreateProgram(&program, hipSource, "kernel.cu", 0, NULL, NULL);
// 编译源代码
hiprtcResult compileResult = hiprtcCompileProgram(program);
if (compileResult != HIPRTC_SUCCESS) {
// 处理编译错误
size_t logSize;
hiprtcGetProgramLogSize(program, &logSize);
char* log = new char[logSize];
hiprtcGetProgramLog(program, log);
std::cerr << "HIP RTC compile log:\n" << log << std::endl;
delete[] log;
// 清理资源并退出
hiprtcDestroyProgram(program);
return -1;
}
// 获取编译后的二进制大小
size_t binarySize;
hiprtcGetBinarySize(program, &binarySize);
// 创建足够大的缓冲区来存储编译后的二进制代码
char* binary = new char[binarySize];
// 从 hipRTC 程序获取编译后的二进制代码
hiprtcGetCode(program, binary);
// 加载编译后的二进制代码到 GPU
hipFunction_t function;
hipModule_t module;
hipGetModuleAndFunction(&module, &function, binary);
// 准备内核参数
float *device_vector; // 假设已经分配并初始化了 GPU 内存
size_t size = ...; // 向量的大小
void* kernelArgs[] = {&device_vector, &size};
// 执行内核
hipModuleLaunchKernel(function,
gridDim, // 网格的维度
blockDim, // 块的维度
0, // 共享内存的大小,用 0 表示不需要共享内存
0, // 流(stream)的句柄,用 0 表示默认流
kernelArgs);
// 可选:等待默认流完成所有工作
hipStreamSynchronize(0);
// 清理资源
delete[] binary;
hiprtcDestroyProgram(program);
注意事项:
hipRTC 提供了一个灵活的方式来编译和执行 GPU 代码,允许在不同的运行时条件下进行优化,并且可以简化开发流程,因为不需要在编译时生成和处理二进制内核对象文件。
HIP Graph 是一种在 HIP (Heterogeneous-compute Interface for Portability) 中用于构建和管理复杂的 GPU 执行依赖关系的机制。使用 HIP Graph,开发者可以创建一个包含多个执行节点(如内核执行、内存复制等)的图形化任务依赖结构,然后提交给 GPU 执行,从而实现更细粒度的性能优化和资源管理。
示例代码
#include
#include
int main() {
// 初始化 HIP
hipSetDevice(0);
// 定义内核函数和其他相关变量
// ...
// 创建一个新的图对象
hipGraph_t graph;
hipGraphCreate(&graph, 0);
// 创建内核节点
hipKernelNodeParams kernelNodeParams1 = {0};
kernelNodeParams1.func = myKernel1; // 假设 myKernel1 是一个已定义的内核函数
// 设置 kernelNodeParams1 的其他参数...
hipGraphNode_t kernelNode1;
hipGraphAddKernelNode(&kernelNode1, graph, NULL, &kernelNodeParams1);
hipKernelNodeParams kernelNodeParams2 = {0};
kernelNodeParams2.func = myKernel2; // 假设 myKernel2 是另一个内核函数
// 设置 kernelNodeParams2 的其他参数...
hipGraphNode_t kernelNode2;
hipGraphAddKernelNode(&kernelNode2, graph, NULL, &kernelNodeParams2);
// 创建内存复制节点
hipMemcpy3DParms memcpyParams = {0};
// 设置 memcpyParams 来定义内存复制操作...
hipGraphNode_t memcpyNode;
hipGraphAddMemcpyNode(&memcpyNode, graph, NULL, &memcpyParams);
// 设置依赖关系:kernelNode2 依赖于 kernelNode1,memcpyNode 依赖于 kernelNode2
hipGraphNode_t dependencies1[] = {kernelNode1};
hipGraphAddDependencies(graph, kernelNode2, dependencies1, 1);
hipGraphNode_t dependencies2[] = {kernelNode2};
hipGraphAddDependencies(graph, memcpyNode, dependencies2, 1);
// 实例化图
hipGraphExec_t graphExec;
hipGraphInstantiate(&graphExec, graph, NULL, NULL, 0);
// 执行图
hipGraphLaunch(graphExec, 0);
// 同步默认流,等待图执行完成
hipStreamSynchronize(0);
// 销毁图和图实例
hipGraphDestroy(graph);
hipGraphExecDestroy(graphExec);
return 0;
}
在 HIP-Clang 中,long double
类型在 x86_64 架构上通常是一个 80 位的扩展精度格式,这种格式提供了比标准的 IEEE 双精度浮点数(double
)更高的精度。然而,这种 80 位的扩展精度格式并不被 AMD GPU 支持,因为 AMD GPU 通常只支持 IEEE 标准的单精度(float
)和双精度(double
)浮点数。
HIP-Clang 为了保持兼容性,当目标是 AMD GPU 时,会将 long double
类型视为 IEEE 双精度(double
)类型。
Fused Multiply-Add (FMA) 是一种在现代处理器中支持的指令,它允许将乘法和加法操作融合为一个单独的指令,从而提高性能并减少可能的数值误差。在 HIP-Clang 编译器中,FMA 和收缩(contractions)的使用取决于编译器的设置和目标硬件的特性。
-ffp-contract=fast
参数,这可以启用浮点运算的融合,提高性能。对于 x86_64 架构,由于通用目标默认不支持 FMA(Fused Multiply-Add,即融合乘加)指令集,所以 FMA 默认是关闭的。如果您希望在 x86_64 架构上启用 FMA,可以通过使用 -march=native
指令或者 mfma
来指定,这样编译器就会生成针对支持 FMA 指令集的 CPU 的代码 。/proc/cpuinfo
文件来获取 CPU 支持的指令集信息。如果输出中包含 fma
标志,那么 CPU 支持 FMA 指令集 。在某些情况下,编译器也会定义特定的宏,如 __FMA__
,来指示 FMA 指令集的支持 。_Float16
类型是一种半精度浮点数类型,它在某些编译器和平台上可能没有稳定的 ABI(应用程序二进制接口)。这意味着如果在 x86_64 架构上使用 Clang(或 hipcc
)和 gcc 编译器,并且一个编译器编译了宿主函数的定义,而另一个编译器编译了调用者,那么作为函数参数或返回类型使用 _Float16
或包含 _Float16
的聚合类型可能会导致未定义的行为。这是因为 x86_64 上的通用目标默认不支持 _Float16
,并且 _Float16
在 x86_64 上缺乏稳定的 ABI 支持。
HIP 并不支持带有特定舍入模式的数学函数,这些模式包括向上舍入(ru)、向下舍入(rd)和向零舍入(rz)。HIP 仅支持最接近舍入模式(rn)。尽管如此,带有后缀 _ru、_rd 和 _rz 的数学函数实际上和带有 _rn 后缀的数学函数以相同的方式实现。这些后缀的存在主要是作为一个权宜之计,以确保使用这些特定舍入模式的程序能够被编译。
这意味着,如果你在 HIP 程序中使用这些特定的舍入模式,实际上它们并不会改变函数的舍入行为,而是会采用默认的最接近舍入模式。
HIP-Clang 支持生成两种类型的静态库:
第一种类型的静态库:这种库不导出设备(GPU)函数,只导出和启动库中的宿主(CPU)函数。它的优点是可以与非 hipcc 编译器(例如 gcc)链接。此外,这种类型的库包含带有嵌入式设备代码的宿主对象,这些设备代码以胖二进制(fat binaries)的形式存在。这种库是通过使用 --emit-static-lib
标志来生成的。
创建和使用这种类型静态库的示例命令如下:
hipcc hipOptLibrary.cpp --emit-static-lib -fPIC -o libHipOptLibrary.a
gcc test.cpp -L. -lhipOptLibrary -L/path/to/hip/lib -lamdhip64 -o test.out
这里,hipcc
用于编译生成 .a
静态库文件,而 gcc
用于链接该静态库并编译测试程序。
第二种类型的静态库:这种库导出设备函数以供其他代码对象链接。不过,这需要使用 hipcc
作为链接器。
创建这种类型静态库的示例命令如下:
hipcc hipDevice.cpp -c -fgpu-rdc -o hipDevice.o
ar rcsD libHipDevice.a hipDevice.o
hipcc libHipDevice.a test.cpp -fgpu-rdc -o test.out
在这个例子中,首先使用 hipcc
编译 hipDevice.cpp
并生成对象文件 .o
,然后使用 ar
命令创建静态库。最后,使用 hipcc
链接该静态库和其他源文件,生成最终的可执行文件。
这两种方法各有用途,第一种方法提供了与非 HIP 编译器链接的能力,而第二种方法适用于完全在 HIP 环境下编译和链接的场合。开发者可以根据项目需求和编译环境选择合适的方法来创建和使用静态库。