CUDA (.cu文件) 通过runtime API加载PTX binary的过程分析

背景

我们经常会用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。

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理