背景
我们经常会用nvcc直接编译一个.cu文件到binary来执行。执行时,nvcc生成的其他代码帮我们做了很多事情来实现加载PTX ELF binary等事情。现在我们需要拦截nvcc生成的代码所做的这些事情,所以写一篇文章分析一下。
细节
#include <cstddef>
#include <cstdint>
#include <cstdio>
#include <iostream>
#include <ostream>
#include <stdio.h>
#include <stdlib.h>
#include <cuda_runtime.h>
#include <vector>
__global__ void sumArray(uint64_t *input, uint64_t *output, size_t size)
{
for (size_t i = 0; i < size; i++)
*output += input[i];
printf("From device side: sum = %lu\n", (unsigned long)*output);
}
constexpr size_t ARR_SIZE = 1000;
int main()
{
std::vector<uint64_t> arr;
for (size_t i = 1; i <= ARR_SIZE; i++) {
arr.push_back(i);
}
auto data_size = sizeof(arr[0]) * arr.size();
uint64_t *d_input, *d_output;
cudaMalloc(&d_input, data_size);
cudaMalloc(&d_output, sizeof(arr[0]));
cudaMemcpy(d_input, arr.data(), data_size, cudaMemcpyHostToDevice);
sumArray<<<1, 1, 1>>>(d_input, d_output, arr.size());
uint64_t host_sum;
cudaMemcpy(&host_sum, d_output, sizeof(arr[0]), cudaMemcpyDeviceToHost);
cudaDeviceSynchronize();
std::cout << "Sum is " << host_sum << std::endl;
cudaFree(d_input);
cudaFree(d_output);
return 0;
}
这是一个很简单的CUDA程序。传一个数组到device上,device求和并写回内存,而后host读取结果。
使用 nvcc -cuda victim.cu -o victim.cpp
来生成对应展开后的cpp文件。生成后的代码过大,这里就不粘贴了。
初始化过程
nvcc生成的代码里调用了大量内部API来实现加载。这些API的工作逻辑和driverAPI并不一致。
static void __sti____cudaRegisterAll(void) __attribute__((__constructor__));
static void __sti____cudaRegisterAll(void)
{
__cudaFatCubinHandle =
__cudaRegisterFatBinary((void *)&__fatDeviceText);
{
void (*callback_fp)(void **) =
(void (*)(void **))(__nv_cudaEntityRegisterCallback);
(*callback_fp)(__cudaFatCubinHandle);
__cudaRegisterFatBinaryEnd(__cudaFatCubinHandle);
}
atexit(__cudaUnregisterBinaryUtil);
}
可以看到,nvcc生成的代码里给__sti____cudaRegisterAll打了constructor标记,也就是会先执行这个初始化binary的函数。我们可以看到依次调用了这些函数来初始化binary
- __cudaRegisterFatBinary:加载一份binary。binary的内容是用内联汇编嵌入在源代码里的ELF
- __nv_cudaEntityRegisterCallback:注册使用到的kernel函数。这个函数接下来会解释。
- __cudaRegisterFatBinaryEnd:结束函数注册?(我猜的,这是没有文档的东西
同时注册了一个atexit函数__cudaUnregisterBinaryUtil
核函数注册
让我们看__nv_cudaEntityRegisterCallback。
static void __nv_cudaEntityRegisterCallback(void **__T5)
{
{
volatile static void **__ref __attribute__((unused));
__ref = (volatile void **)__T5;
};
__nv_save_fatbinhandle_for_managed_rt(__T5);
__cudaRegisterFunction(__T5,
(const char *)((void (*)(uint64_t *, uint64_t *,
size_t))sumArray),
(char *)"_Z8sumArrayPmS_m", "_Z8sumArrayPmS_m",
-1, (uint3 *)0, (uint3 *)0, (dim3 *)0, (dim3 *)0,
(int *)0);
}
注意到这里会调用__cudaRegisterFunction来将kernel函数的符号名(定义在PTX ELF binary中)和host上的包装函数的地址关联起来。
host上的包装函数
void __device_stub__Z8sumArrayPmS_m(uint64_t *__par0, uint64_t *__par1,
size_t __par2)
{
void *__args_arr[3];
int __args_idx = 0;
__args_arr[__args_idx] = (void *)(char *)&__par0;
++__args_idx;
__args_arr[__args_idx] = (void *)(char *)&__par1;
++__args_idx;
__args_arr[__args_idx] = (void *)(char *)&__par2;
++__args_idx;
{
volatile static char *__f __attribute__((unused));
__f = ((char *)((
void (*)(uint64_t *, uint64_t *, size_t))sumArray));
dim3 __gridDim, __blockDim;
size_t __sharedMem;
cudaStream_t __stream;
if (__cudaPopCallConfiguration(&__gridDim, &__blockDim,
&__sharedMem,
&__stream) != cudaSuccess)
return;
if (__args_idx == 0) {
(void)cudaLaunchKernel(
((char *)((void (*)(uint64_t *, uint64_t *,
size_t))sumArray)),
__gridDim, __blockDim, &__args_arr[__args_idx],
__sharedMem, __stream);
} else {
(void)cudaLaunchKernel(
((char *)((void (*)(uint64_t *, uint64_t *,
size_t))sumArray)),
__gridDim, __blockDim, &__args_arr[0],
__sharedMem, __stream);
}
};
}
void sumArray(uint64_t *__cuda_0, uint64_t *__cuda_1, size_t __cuda_2)
{
__device_stub__Z8sumArrayPmS_m(__cuda_0, __cuda_1, __cuda_2);
}
包装函数内的核心是cudaLaunchKernel,直接传入了host上的函数地址来执行kernel。由于先前已经关联过了host上的包装函数的地址和kernel的符号名,所以只需要提供host上的包装函数的地址即可。
main函数
int main()
{
std::vector<unsigned long> arr;
for (size_t i = (1); i <= ARR_SIZE; i++) {
arr.push_back(i);
}
auto data_size = sizeof arr[0] * arr.size();
uint64_t *d_input, *d_output;
cudaMalloc(&d_input, data_size);
cudaMalloc(&d_output, sizeof arr[0]);
cudaMemcpy(d_input, arr.data(), data_size, cudaMemcpyHostToDevice);
(__cudaPushCallConfiguration(1, 1, 1)) ?
(void)0 :
sumArray(d_input, d_output, arr.size());
uint64_t host_sum;
cudaMemcpy(&host_sum, d_output, sizeof arr[0], cudaMemcpyDeviceToHost);
cudaDeviceSynchronize();
(((((std::cout << ("Sum is "))) << host_sum)) << (std::endl));
cudaFree(d_input);
cudaFree(d_output);
return 0;
}
可以看到,main函数直接执行host上的包装函数即可完成核函数运行。
反注册binary
注意到我们之前提到的atexit里面的退出函数。
static void __cudaUnregisterBinaryUtil(void)
{
____nv_dummy_param_ref((void *)&__cudaFatCubinHandle);
__cudaUnregisterFatBinary(__cudaFatCubinHandle);
}
这里调用一个内部函数来销毁binary handle。
发表回复