HIP程序在编译完后会得到一个host ELF文件。而device代码在编译完后会被bundle,作.hip_fatbin section
嵌入在该ELF中。那么在运行host ELF之后,HIP是如何将host ELF携带的device代码传递给runtime的呢?它又是如何进行kernel launch的呢?本文将进行解答。
考虑如下hip程序:
#include <hip/hip_runtime.h>
#include <stdio.h>
__global__ void demoKernel(int *a) {
*a += 1;
}
int main() {
int x = 0;
int *d_x;
hipMalloc((void**)&d_x, sizeof(int));
hipMemcpy(d_x, &x, sizeof(int), hipMemcpyHostToDevice);
demoKernel<<<1,2>>>(d_x);
hipMemcpy(&x, d_x, sizeof(int), hipMemcpyDeviceToHost);
}
使用以下命令得到host端的IR:
hipcc -S -emit-llvm --offload-host-only demo.cpp -o demo.ll
在IR中可以首先找到全局构造器llvm.global_ctors:
构造器的相关说明可以查看llvm语言手册。简单来说就是一个全局数组,每项保存一个(优先级,函数指针,关联数据或函数)的三元组,编译完后程序将按照优先级依次调用这些构造函数,全部执行完后才会调用main。c++一般有两种函数会被包含在该全局构造器中:
在本例中,只有__hip_module_ctor这一个构造函数。我们用llvm-extract从demo.ll中提取该函数,观察其依赖的数据变量以及代码逻辑:
/opt/rocm-5.4.6/llvm/bin/llvm-extract -func=__hip_module_ctor demo.ll -S -o hip_module_ctor.ll
首先明确下__hip_module_ctor依赖的几个全局变量以及runtime函数:
@_Z10demoKernelPi是一个void (i32*)的函数指针,在demo.ll中有其完整定义,它指向一个__device_stub__demoKernel的桩函数。该桩函数有以下两个作用:
@0定义了一个kernel名字符串数组。本例为:@0 = private unnamed_addr constant [17 x i8] c”_Z10demoKernelPi\00”, align 1
@__hip_fatbin_wrapper定义了一个fatbin的描述变量。其IR定义在demo.ll中为:
这个描述变量的类型可以在runtime中找到:
我们重点关注其binary成员,该指针指向了__hip_fatbin这个符号,而这个符号就是.hip_fatbin section的起始地址。通过该指针可以访问嵌入在host中的device bundle文件。
@__hip_gpubin_handle 是一个FatBinaryInfo**类型的变量,在调用__hipRegisterFatBinary后,返回一个描述runtime内部保存fatbin信息的变量地址。对于host侧而言,我们只关心该指针是否是null,是null表示还没注册过fatbin,否则反之。__
了解前面提到的变量含义之后,__hip_module_ctor的代码逻辑就非常简单了:
关于这段注册逻辑,hip编程手册有一段精炼紧凑的描述,可以作为以上过程的一个小结:
在理解了demo.ll中各变量以及__hip_module_ctor的逻辑之后,我们可以将其用c++进行描述:
// runtime api原型声明
hipError_t hipLaunchKernel(const void *functioin_address, dim3 numBlocks, dim3 dimBlock, void **args, size_t sharedMemBytes, hipStream_t stream);
hipError_t __hipPopCallConfiguration(dim3 *gridDim, dim3 *blockDim, size_t *sharedMem, hipStream_t *stream);
struct FatbinInfo;
struct __CudaFatBinaryWrapper {
unsigned int magic;
unsigned int version;
void *binary;
void *dummy1;
}
// device_stub
void __device_stub__demoKernel(int *args) {
void **kernel_args = (void **)args;
dim3 grid_dim, block_dim;
size_t sharedMem;
hipStream_t stream;
__hipPopCallConfiguration(&grid_dim, &block_dim, &sharedMem, &stream);
hipLaunchKernel(__device_stub__demoKernel, grid_dim, block_dim, sharedMem, stream);
return;
}
void (*demoKernel)(int *) = __device_stub_demoKernel; // stub函数指针
char kernel_name[] = "_Z10demoKernelpi\00"; // kernel函数名
extern __attribute__((section(".hip_fatbin")) const char __hip_fatbin; // 引用位于.hip_fatbin section的__hip_fatbin符号
// 定义一个位于.hipFatBinSegement中的wrapper变量
static __attribute__((section(".hipFatBinSegment")) const struct __CudaFatBinaryWrapper __hip_fatbin_wrapper = {1212764230, 1, &__hip_fatbin, nullptr};
// 定义一个fatbin注册标记
__attribute__((selectany,visibility("hidden"))) FatbinInfo **__hip_gpubin_handle = nullptr;
// 构造函数
static __attribute__((constructor)) void __hip_module_ctor() {
if (!__hip_gpubin_handle) {
__hip_gpubin_handle = __hipRegisterFatBinary(&__hip_fatbin_wrapper);
}
__hipRegisterFunction(__hip_gpubin_handle, demoKernel, kernel_name, kernel_name, -1, nullptr, nullptr, nullptr, nullptr, nullptr);
atexit(__hip_module_dtor); // 在exit时调用__hip_module_dtor
}
对以上翻译代码做点说明:
一切是那么的朴素而自然,简单的一个__hipPushCallConfiguration,然后调用device_stub函数,桩函数的逻辑可查看第3节中的C++等价代码。