Device Kernel Visibility Issue#
Consider the following simple translation unit (TU):
template <class T>
__global__ void kernel(T *val) {
::printf("kernel: set val = 42\n");
*val = 42;
}
int main() {
int *ptr{};
kernel<<<1, 1>>>(ptr);
}
The cuda compiler frontend will turn this into:
template< class T>
static void __wrapper__device_stub_kernel(T *&ptr) {
::cudaLaunchKernel(0, 0, 0, 0, 0, 0);
}
// stub host function
template< class T>
void kernel(T *ptr) {
__wrapper__device_stub_kernel<T>(ptr);
}
int main() {
int *ptr{};
(__cudaPushCallConfiguration(1, 1)) ? (void)0 : kernel(ptr);
}
static void __device_stub__Z6kernelIiEvPT_(int *__par0) {
__cudaLaunchPrologue(1);
__cudaSetupArgSimple(__par0, 0UL);
__cudaLaunch(((char *)((void ( *)(int *))kernel )));
}
template<> void __wrapper__device_stub_kernel(int *&__cuda_0) {
__device_stub__Z6kernelIiEvPT_( (int *&)__cuda_0);
}
The CUDA runtime is going to use the address of template<> void kernel(T *ptr) (in the following h_kernel)
as a key in the host stub function (h_kernel) - device function (d_kernel) mapping. This works fine if
there is only a single source of truth for the stub function h_kernel.
However, imagine that there are two shared libraries: lib_a and lib_b both instantiating different kernel
instances, e.g d_kernel<int> and d_kernel<size_t>.
project(DeviceKernelVisibility CUDA CXX)
add_executable(device_kernel_visibility main.cu)
add_library(lib_a SHARED tu_a.cu)
add_library(lib_b SHARED tu_b.cu)
target_link_libraries(device_kernel_visibility PRIVATE lib_a lib_b)
Each library will have it’s own fatbinary: d_kernel<int>_a and d_kernel<size_t>_b as well as host stub functions
h_kernel<int>_a and h_kernel<size_t>_b.
lib |
host |
device |
|---|---|---|
a |
0xh_kernel_a |
0xd_kernel_a |
b |
0xh_kernel_b |
0xd_kernel_b |
In contrast to Problem 1 the host stubs will get a different mangled name and so the right stub function will always be selected.
Now imagine that both libraries are going to defer launching of their kernels to a function foo common to both
lib_a and lib_b, which has weak external linkage. This might happen in CUB, because it launches
kernels through the thrust::triple_chevron helper.
Similar to Problem 1 the linker will pick one of the two
weak symbols and subsequently lib_a will try to pass its own kernel d_kernel<int>_a to lib_b::foo.
However, the CUDA runtime in lib_b will not find any kernel registered at the address of d_kernel<int>_a and
will fail to launch the kernel.
A simple example program that exemplifies this can be found on github
./device_kernel_visibility/device_kernel_visibility
a: kernel stub address: 0x7fdec19e13eb <== launching kernel_a from a
a: kernel is in mapping: no error
b: launched kernel
a: kernel: set val = 42
a: synchronized stream
a: copied from device to host
a: out: 42
a: kernel was launched: out == 42 <== everything is fine
a: defers launch to b
b: kernel stub address: 0x7fdec19e13eb <== launch kernel_a from b
b: kernel NOT found in mapping: invalid device function <== kernel_a is not found in b mapping
b: FAILED to launch kernel <== unable to launch the kernel from b
b: synchronized stream
b: copied from device to host
b: out: 0
b: kernel was NOT actually launched: out != 42
b: kernel stub address: 0x7fdec19333eb <== launch kernel_b from b
b: kernel is in mapping: no error
b: launched kernel
b: kernel: set val = 42
b: synchronized stream
b: copied from device to host
b: out: 42
b: kernel was launched: out == 42 <== everything is fine
b: defers launch to a
a: kernel stub address: 0x7fdec19333eb <== launching kernel_b from a
a: kernel NOT found in mapping: invalid device function <== same issue as above
a: FAILED to launch kernel
b: kernel: set val = 42
a: synchronized stream
a: copied from device to host
a: out: 42
a: kernel was launched: out == 42 <== kernel launch somehow succeeded