1. 主机内存(Host Memory)
hipHostMalloc
是 HIP API 中的一个函数,它用于在主机上分配特殊的内存,这种内存被称为 "pinned" 或 "page-locked" 内存。这种内存有以下特点和用途:
-
GPU 地址空间映射:分配的内存被映射到系统中所有 GPU 的地址空间。这意味着 GPU 可以直接访问这块内存,而不需要数据复制操作。
-
加速数据传输:使用
hipHostMalloc
分配的内存可以加速主机(Host)到设备(Device)以及设备到主机的数据传输速度。由于这种内存已经被映射到 GPU 的地址空间,因此在数据传输时可以减少一些设置步骤。 -
零拷贝 GPU 访问:GPU 能够直接访问使用
hipHostMalloc
分配的主机内存,无需复制数据到 GPU 的本地设备内存。这种特性称为 "zero-copy" 访问。然而,尽管避免了复制操作,但每次内核访问内存时,数据必须通过 CPU/GPU 互连传输,这可能比直接访问 GPU 的本地内存慢很多。 -
适用场景:
- 当内存访问不频繁时(例如仅访问一次),零拷贝内存是一个不错的选择。
- 它可以用于需要 GPU 直接访问大量数据而不需要频繁复制的场景。
-
一致性与缓存:
- 零拷贝内存通常是 "Coherent"(一致的),意味着它不会被 GPU 缓存。这样可以保证数据的一致性,但可能会影响性能。
- 如果需要,可以覆盖这一行为,允许 GPU 缓存这些内存,但这需要开发者根据具体的性能需求和数据一致性要求来决定。
-
工具使用:可以使用
hipBusBandwidth
工具来测试和比较使用hipHostMalloc
分配的内存与普通内存在数据传输速度上的差异。通过--unpinned
和--pinned
开关来测试不同的内存配置。
总结来说,hipHostMalloc
提供了一种方式来分配可以被 GPU 直接访问的主机内存,这可以用于加速数据传输和实现零拷贝访问,但同时也需要考虑内存访问模式和数据一致性的需求。
2. 内存分配标志
hipHostMallocPortable
和 hipHostMallocMapped
是用于 hipHostMalloc
函数的标志(flags),它们定义了不同但相关的内存特性:
-
hipHostMallocPortable:
- 此标志用于分配可以跨多个 GPU 使用的内存。当内存需要在多个设备之间共享时,使用
hipHostMallocPortable
可以确保分配的内存对所有参与的 GPU 都是可访问的。这种内存通常用于多 GPU 环境,其中不同的 GPU 需要直接访问同一块内存空间。
- 此标志用于分配可以跨多个 GPU 使用的内存。当内存需要在多个设备之间共享时,使用
-
hipHostMallocMapped:
- 此标志用于分配的内存会被映射到所有 GPU 的地址空间,允许 GPU 直接访问这些内存。这种映射通常用于零拷贝操作,其中 GPU 可以直接读取或写入主机内存,无需数据复制。这可以减少数据传输的开销,但可能涉及到跨 CPU 和 GPU 的内存访问延迟。
- hipHostMallocNumaUser:
- 这个标志允许用户指定 NUMA(Non-Uniform Memory Access)策略,根据用户的设置来分配主机内存。NUMA 策略可以影响内存访问的性能,特别是在多处理器系统上。
区别主要在于它们的使用目的和行为:
hipHostMallocPortable
关注的是内存的可移植性和共享性,确保在多 GPU 系统中,所有 GPU 都能够识别和使用同一块内存。hipHostMallocMapped
关注的是内存访问的直接性和性能,允许 GPU 直接与主机内存交互,避免了数据传输的需要。
在实际使用中,hipHostMalloc
通常会同时设置这两个标志,因为它们共同支持了多 GPU 环境下的高性能内存访问模式。这样分配的内存既能够被多个 GPU 共享,又能够被每个 GPU 直接访问,从而实现高效的数据传输和零拷贝操作。
3. NUMA-aware host memory allocation
- NUMA(Non-Uniform Memory Architecture)是一种多处理器系统架构,它允许每个CPU(或CPU核心组)拥有自己的本地内存,并且可以通过互连访问其他CPU的内存。这种架构下,访问本地内存比访问远程内存更快。
- NUMA-aware host memory allocation 是一种在多处理器和多GPU系统中优化内存分配的策略。NUMA策略决定如何分配内存,以及如何选择与每个GPU最近的CPU。这个策略会衡量GPU和CPU设备之间的距离(NUMA距离),并尽量将内存分配在与GPU最近的NUMA节点上。
- 默认情况下,每个GPU会自动选择一个NUMA CPU节点,该节点与GPU之间的NUMA距离最短。这样,主机内存会自动分配在当前GPU设备所在NUMA节点的内存池中。
- 使用
hipSetDevice
API切换到不同的GPU时,可以访问之前分配的主机内存。但是,如果新选择的GPU与该内存的NUMA节点距离较远,可能会增加内存访问延迟。 - 在HIP API中,可以使用
hipHostMallocNumaUser
标志进行NUMA-aware的主机内存分配。这允许开发者根据NUMA策略手动指定内存应该分配在哪个NUMA节点上。
4. 托管内存分配(Managed Memory Allocation)
在 HIP (Heterogeneous-compute Interface for Portability) 中,托管内存分配(Managed Memory Allocation)是一种允许在 GPU 和 CPU 之间共享内存的机制,无需显式地在两者之间复制数据。
在调用托管内存 API hipMallocManaged
之前,HIP 应用程序会执行能力检查,以确保当前设备支持托管内存。
- 如果设备不支持托管内存,
hipMallocManaged
调用将退回到使用系统内存。在这种情况下,其他托管内存 API 调用可能会表现出未定义的行为。 - 如果设备支持托管内存,使用
hipMallocManaged
分配的内存可以提供更好的性能,因为它允许 GPU 和 CPU 透明地访问同一块内存,避免了数据复制的开销。 - 当分配了托管内存后,CPU 和 GPU 都可以透明地访问这块内存,无需显式的数据传输命令。
5. HIP Stream Memory operations
HIP Stream Memory Operations 是一组 HIP API,它们提供对流(stream)内存操作的支持,使得网络节点(如 CPU 或其他 GPU 设备)与 GPU 之间能够直接同步。
-
hipStreamWaitValue32/64:
- 这些函数允许一个流等待一个特定的值出现在内存位置。
hipStreamWaitValue32
用于 32 位值,而hipStreamWaitValue64
用于 64 位值。流将阻塞,直到内存中的值与预期值匹配。
- 这些函数允许一个流等待一个特定的值出现在内存位置。
-
hipStreamWriteValue32/64:
- 这些函数允许一个流写入一个值到特定的内存位置。
hipStreamWriteValue32
用于写入 32 位值,而hipStreamWriteValue64
用于写入 64 位值。这可以用于设置信号或其他同步机制。
- 这些函数允许一个流写入一个值到特定的内存位置。
- 这些操作使得 GPU 能够等待来自网络节点的操作完成,或者让 GPU 通知网络节点某个事件已经发生,从而实现直接同步。
- 这些 API 可用于实现复杂的同步模式,如生产者-消费者问题,其中 GPU 作为生产者生成数据,而 CPU 或其他 GPU 作为消费者使用这些数据。
- 当 CPU 访问用作信号量的内存时,需要使用
volatile
关键字。这是因为编译器默认会对内存访问进行优化,但信号量需要保证每次访问都是直接对内存的操作,而不是从寄存器或缓存中读取。volatile
告诉编译器,该变量可能会在程序的控制之外发生变化,因此每次使用时都必须从内存中读取,而不是假设它的值。
6. 一致性控制(coherency controls)
ROCm defines two coherency options for host memory:
-
一致性内存(Coherent memory):
- 支持在内核运行时进行细粒度同步。
- 内核可以执行对主机 CPU 或其他 GPU 可见的原子操作。
- 支持的同步指令包括
threadfence_system
和 C++11 风格的原子操作。 - 一致性内存不能被 GPU 缓存,可能会导致性能较低。
-
非一致性内存(Non-coherent memory):
- 可以被 GPU 缓存,但在内核运行时不支持同步。
- 只能在命令边界(如内核结束或复制命令)进行同步。
- 当不需要细粒度同步时,适合高性能访问。
HIP 控制逻辑:
- hipHostMalloc 函数通过分配标志提供控制内存一致性的能力。
- HIP_HOST_COHERENT 环境变量用于控制默认行为,但在大多数情况下会被分配标志覆盖。
分配标志和环境变量控制逻辑:
-
未传递标志:
- 主机内存分配是一致性的,忽略
HIP_HOST_COHERENT
环境变量。
- 主机内存分配是一致性的,忽略
-
hipHostMallocCoherent=1:
- 主机内存分配将是一致性的,忽略
HIP_HOST_COHERENT
环境变量。
- 主机内存分配将是一致性的,忽略
-
hipHostMallocMapped=1:
- 主机内存分配将是一致性的,映射到所有 GPU 的地址空间,忽略
HIP_HOST_COHERENT
环境变量。
- 主机内存分配将是一致性的,映射到所有 GPU 的地址空间,忽略
-
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 内核执行期间进行原子操作和同步,应选择一致性内存。如果性能是关键考虑因素且可以避免细粒度同步,则可以选择非一致性内存。通过合理选择内存一致性类型,可以优化应用程序的性能和同步行为。
7. 零拷贝主机内存的可见性
零拷贝主机内存的可见性(visibility)是指在 GPU 执行过程中,对主机内存所做的更改何时对其他设备(如 CPU 或其他 GPU)可见。
(1)hipEventSynchronize
hipEventSynchronize
是 HIP API 中的一个函数,用于确保某个事件(event)在执行特定同步操作之前已经完成。
事件(Event)的作用:
- 在 GPU 编程中,事件通常用于跟踪和控制命令的执行顺序。事件可以记录在特定的时间点,例如,当一个内核开始执行或数据传输完成时。
hipEventSynchronize 的用途:
- 当你调用
hipEventSynchronize(event)
时,你是在告诉 HIP 运行时等待(同步)直到该事件完成。这意味着它将阻塞当前线程,直到事件所标志的操作在 GPU 上执行完毕。
- 开发者可以通过
hipEventSynchronize
控制hipEvents
的释放范围。 - 默认情况下,每当记录事件时,GPU 会执行设备范围的获取和释放操作,这使得主机和设备内存对同一设备上执行的其他命令可见。
同步的级别:
- 设备级别同步:默认情况下,HIP 事件会在设备级别上同步,确保同一设备上的后续命令在事件完成之前不会开始执行。
- 系统级别同步:通过
hipEventCreateWithFlags
使用hipEventReleaseToSystem
标志,可以创建一个在系统级别上同步的事件。这种类型的事件在记录时会使所有主机内存(无论是一致性还是非一致性)对系统中的其他代理(如 CPU 或其他 GPU)可见。
为什么使用 hipEventSynchronize:
- 确保数据一致性:在需要确保 GPU 上的操作在进一步处理之前已经完成的情况下使用。
- 性能分析:在性能基准测试中,事件可以用来测量命令执行所需的时间,
hipEventSynchronize
可以确保测量的准确性。
性能影响:
- 使用
hipEventSynchronize
可能会导致性能下降,因为它强制执行线程等待 GPU 完成指定的操作。因此,它应该谨慎使用,特别是在性能敏感的应用程序中。
hipEventCreateWithFlags 选项:
hipEventReleaseToSystem
:当事件被记录时执行系统范围的释放操作。这将使一致性和非一致性主机内存对系统中的其他代理可见,但可能涉及重量级操作,如缓存刷新。hipEventDisableTiming
:使用此标志创建的事件不记录分析数据,因此,如果用于同步,可以提供最佳性能。
内存一致性与事件:
- 一致性内存通常使用轻量级的内核同步机制,如原子操作,因此在大多数情况下不需要使用
hipEventReleaseToSystem
。 - 非一致性内存可以被 GPU 缓存,但不能在内核运行时进行同步,可能需要在事件记录时使用系统级同步。
注意事项:
- 在使用
hipExtLaunchKernelGGL
或hipExtLaunchKernel
进行内核调度时,通过 API 传递的事件不会被显式记录,并且应该仅用于获取特定启动的经过时间。 - 如果尝试在多个调度之间使用事件,例如,使用不同
hipExtLaunchKernelGGL
/hipExtLaunchKernel
调用的开始和停止事件,这些未记录的事件将被视为无效,HIP 可能会从hipEventElapsedTime
显示 "hipErrorInvalidHandle" 错误。
总结和建议:
- 一致性主机内存是默认选项,也是最易于使用的,因为它在特定的同步点对 CPU 可见,并允许内核内的同步命令(如
threadfence_system
)透明地工作。 - ROCm 还支持使用 "非一致性" 主机内存分配在 GPU 中缓存主机内存。这可以提供性能优势,但必须小心使用正确的同步机制。
(2)Direct Dispatch
Direct Dispatch 是 HIP 运行时的一个特性,它改变了传统的生产者-消费者模型,提高了 HIP 流的命令调度效率。
- 在 HIP 运行时,默认启用 Direct Dispatch。
传统调度模型:
- 在传统的 GPU 编程模型中,CPU(作为生产者)将命令放入一个队列中,然后 GPU(作为消费者)从这个队列中取出命令并执行。
- 这种模型通常涉及到在 CPU 端创建额外的线程来管理命令队列,这些线程负责将命令异步地提交给 GPU。
Direct Dispatch 模型:
- 直接排队:Direct Dispatch 允许 HIP 运行时直接将命令(或“数据包”)发送到 GPU,而不需要通过额外的线程或队列。这意味着命令几乎直接从发出它们的 CPU 线程传递到 GPU。
- 减少延迟:由于减少了线程调度和同步机制的开销,Direct Dispatch 可以减少从发出命令到 GPU 开始执行这些命令的总延迟。
- 减少线程开销:由于不需要额外的线程来管理命令队列,Direct Dispatch 减少了线程创建和调度的开销,这有助于降低运行时的资源消耗。
Direct Dispatch 的优势:
- 性能提升:Direct Dispatch 可以提高应用程序的性能,因为它减少了命令调度的延迟。
- 简化编程模型:它简化了生产者-消费者模型,因为不需要为每个流创建和管理额外的线程。
- 减少同步开销:减少了由于线程同步(如锁和原子操作)引起的延迟。
如何禁用 Direct Dispatch:
如果出于某些原因需要禁用 Direct Dispatch(例如,与现有应用程序的兼容性问题),可以通过设置环境变量 AMD_DIRECT_DISPATCH=0
来实现。
结论:
Direct Dispatch 是 HIP 为了提高 GPU 计算任务的执行效率而设计的特性。它通过减少 CPU 和 GPU 之间的调度延迟,使得 GPU 任务的启动更加迅速,从而提高整体性能。然而,是否使用 Direct Dispatch 应根据应用程序的具体需求和性能测试结果来决定。
(3)HIP Runtime Compilation
HIP Runtime Compilation(简称 hipRTC)是 HIP API 的一个特性,它支持在程序运行时编译 GPU 执行的内核代码。
运行时编译的优势:
-
即时优化:
- hipRTC 可以在程序运行时根据实际的 GPU 硬件特性进行即时优化,这可能比静态编译时的优化更加精确和高效。
-
适应不同硬件:
- 由于代码是在运行时编译的,开发者可以为不同的 GPU 设备或不同的运行时条件定制优化策略。
-
简化开发流程:
- 开发者可以使用 HIP 源代码字符串,而不需要在开发过程中管理二进制内核对象文件。
hipRTC 的基本工作流程:
-
创建 hipRTC 程序:
- 使用
hiprtcCreateProgram
创建一个 hipRTC 程序对象。
- 使用
-
编译 HIP 源码:
- 将 HIP 源代码作为字符串传递给
hiprtcCompileProgram
函数进行编译。
- 将 HIP 源代码作为字符串传递给
-
获取编译结果:
- 编译成功后,可以从 hipRTC 程序对象中获取编译后的二进制代码。
-
加载和执行:
- 使用 HIP 的常规 API 将编译后的二进制代码加载到 GPU 上,并像普通的内核一样执行它们。
-
销毁 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 可能会增加程序的启动时间,因为需要在程序运行时进行编译。
- 运行时编译的代码仍然需要遵循 HIP 的编程模型和约束。
hipRTC 提供了一个灵活的方式来编译和执行 GPU 代码,允许在不同的运行时条件下进行优化,并且可以简化开发流程,因为不需要在编译时生成和处理二进制内核对象文件。
(4)HIP Graph
HIP Graph 是一种在 HIP (Heterogeneous-compute Interface for Portability) 中用于构建和管理复杂的 GPU 执行依赖关系的机制。使用 HIP Graph,开发者可以创建一个包含多个执行节点(如内核执行、内存复制等)的图形化任务依赖结构,然后提交给 GPU 执行,从而实现更细粒度的性能优化和资源管理。
- HIP Graph 允许开发者定义任务之间的依赖关系,例如,一个内核执行可能依赖于之前的数据复制操作完成。
- 图中的每个节点代表一个执行任务,节点之间的边代表任务的先后依赖关系。
- 可以在图的构建过程中捕获错误,而不必等到所有任务都提交给 GPU。
- 在 HIP 中,Graph 通常与流(Stream)概念结合使用,流可以看作是执行图中任务的线性序列。
示例代码
#include <hip/hip_runtime.h>
#include <hip/hip_runtime_api.h>
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;
}
(5)long double类型的使用
在 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
)类型。
(6)FMA and Contractions
Fused Multiply-Add (FMA) 是一种在现代处理器中支持的指令,它允许将乘法和加法操作融合为一个单独的指令,从而提高性能并减少可能的数值误差。在 HIP-Clang 编译器中,FMA 和收缩(contractions)的使用取决于编译器的设置和目标硬件的特性。
- 默认情况下,HIP-Clang 假设使用
-ffp-contract=fast
参数,这可以启用浮点运算的融合,提高性能。对于 x86_64 架构,由于通用目标默认不支持 FMA(Fused Multiply-Add,即融合乘加)指令集,所以 FMA 默认是关闭的。如果您希望在 x86_64 架构上启用 FMA,可以通过使用-march=native
指令或者mfma
来指定,这样编译器就会生成针对支持 FMA 指令集的 CPU 的代码 。 - 此外,当启用了收缩(contractions)而 CPU 没有启用 FMA 指令集时,GPU 可能会产生与 CPU 不同的数值结果。这是因为不同的硬件平台可能使用不同的算法或硬件特性来执行相同的运算,这可能导致最终结果存在差异 。
- 在编译代码时,如果想要检测 CPU 是否支持 FMA 指令集,可以通过查看
/proc/cpuinfo
文件来获取 CPU 支持的指令集信息。如果输出中包含fma
标志,那么 CPU 支持 FMA 指令集 。在某些情况下,编译器也会定义特定的宏,如__FMA__
,来指示 FMA 指令集的支持 。 - 在处理数值计算时,需要注意 GPU 和 CPU 可能会因为硬件架构、浮点数精度、运算优化等不同而产生不同的结果。尤其是在进行大量计算或使用高级数学函数时,这种差异可能会更加明显 。因此,如果对数值精度有严格要求,可能需要在相同的硬件平台上执行计算,或者采取其他措施来确保结果的一致性。
(7)_Float16类型的使用
_Float16
类型是一种半精度浮点数类型,它在某些编译器和平台上可能没有稳定的 ABI(应用程序二进制接口)。这意味着如果在 x86_64 架构上使用 Clang(或 hipcc
)和 gcc 编译器,并且一个编译器编译了宿主函数的定义,而另一个编译器编译了调用者,那么作为函数参数或返回类型使用 _Float16
或包含 _Float16
的聚合类型可能会导致未定义的行为。这是因为 x86_64 上的通用目标默认不支持 _Float16
,并且 _Float16
在 x86_64 上缺乏稳定的 ABI 支持。
(8)数学函数的特殊舍入模式
HIP 并不支持带有特定舍入模式的数学函数,这些模式包括向上舍入(ru)、向下舍入(rd)和向零舍入(rz)。HIP 仅支持最接近舍入模式(rn)。尽管如此,带有后缀 _ru、_rd 和 _rz 的数学函数实际上和带有 _rn 后缀的数学函数以相同的方式实现。这些后缀的存在主要是作为一个权宜之计,以确保使用这些特定舍入模式的程序能够被编译。
这意味着,如果你在 HIP 程序中使用这些特定的舍入模式,实际上它们并不会改变函数的舍入行为,而是会采用默认的最接近舍入模式。
(9)静态库创建
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 环境下编译和链接的场合。开发者可以根据项目需求和编译环境选择合适的方法来创建和使用静态库。