feat(cccl): integrate missing CCCL directories — python/, ci/, .agent/, docs/, test/
Sparse-checkout from NVIDIA/cccl main branch to complete cccl_upstream: Added: - python/cuda_cccl/ (226 files) — Python bindings for device-level algorithms Critical for muh toolchain: cuda.compute.reduce_into, scan, radix_sort, etc. Includes 204 .py files with full test coverage for all 27 algorithms - ci/ (163 files) — Build/test infrastructure build_cub.sh, test_cub.sh, build_and_test_targets.sh, matrix.yaml Directly maps to our [INFRA-CI] and [INFRA-BUILD] items - .agent/skills/ (7 files) — NVIDIA's own agent skills for CCCL cccl-style/SKILL.md, cccl-test/SKILL.md, sass-diff/SKILL.md - docs/ (491 files) — Official CCCL documentation CI references, CMake guides, Python compute docs, libcudacxx PTX docs - test/ (12 files) — Top-level integration tests (cuda_smoke, stdpar) - Root configs: .clang-format, .clang-tidy, CONTRIBUTING.md, pyproject.toml - CLAUDE.md symlink → AGENTS.md (NVIDIA's standard) cccl_upstream now mirrors full NVIDIA/cccl structure: Before: 42M (cub + thrust + libcudacxx + cudax + c + examples + benchmarks) After: 53M (+python +ci +docs +.agent +test +configs) This completes the CCCL base needed for: - [muh-bench] items: ci/util/build_and_test_targets.sh for targeted builds - [CCCL-verify] items: python/cuda_cccl/tests/ as reference implementations - [CCCL-test] items: ci/test_cub.sh, ci/test_thrust.sh - Agent workflow: .agent/skills/ for consistent style and test patterns
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
.. _cccl-development-visibility-device-kernel-visibility:
|
||||
|
||||
|
||||
Device Kernel Visibility Issue
|
||||
-------------------------------
|
||||
|
||||
Consider the following simple translation unit (TU):
|
||||
|
||||
.. code-block:: cpp
|
||||
|
||||
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:
|
||||
|
||||
.. code-block:: cpp
|
||||
|
||||
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>``.
|
||||
|
||||
.. code-block:: cmake
|
||||
|
||||
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
|
||||
:ref:`Problem 1 <cccl-development-visibility-host-stub-visibility>` 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 :ref:`Problem 1 <cccl-development-visibility-host-stub-visibility>` 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 <https://github.com/NVIDIA/cccl/tree/main/docs/cub/developer/visibility/examples/device_kernel_visibility>`_
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
./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
|
||||
@@ -0,0 +1,186 @@
|
||||
.. _cccl-development-visibility-different-architectures:
|
||||
|
||||
Linking TUs compiled with different architectures
|
||||
--------------------------------------------------
|
||||
|
||||
Consider the following simple library:
|
||||
|
||||
.. code-block:: cpp
|
||||
|
||||
template <int... Archs>
|
||||
__host__ __device__ constexpr int sum_archs() noexcept {
|
||||
return (Archs + ... + 0);
|
||||
}
|
||||
|
||||
// kernel with architecture dependent symbol name and functionality
|
||||
template <class T, auto Archs = sum_archs<__CUDA_ARCH_LIST__>()>
|
||||
__global__ void kernel(T *val) {
|
||||
*val = sum_archs<__CUDA_ARCH_LIST__>();
|
||||
}
|
||||
|
||||
__attribute__((visibility("hidden"))) inline int use_kernel() {
|
||||
int *d_val{};
|
||||
cudaMalloc(&d_val, sizeof(d_val));
|
||||
kernel<<<1, 1>>>(d_val);
|
||||
int ret;
|
||||
if (cudaMemcpy(&ret, d_val, sizeof(size_t), cudaMemcpyDeviceToHost) !=
|
||||
cudaSuccess) {
|
||||
std::printf("c: FAILED to copy from device to host\n");
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
template <class T = int>
|
||||
struct some_class_with_kernel {
|
||||
T val_;
|
||||
|
||||
some_class_with_kernel();
|
||||
__forceinline__ some_class_with_kernel(T) { val_ = use_kernel(); }
|
||||
};
|
||||
|
||||
We have a kernel that does some architecture dependent work. This could be relying on some hardware feature that is
|
||||
dependent on the current architecture.
|
||||
|
||||
.. code-block:: cpp
|
||||
|
||||
#include "kernel.cuh"
|
||||
|
||||
int main() {
|
||||
some_class_with_kernel with_inline{1};
|
||||
std::printf("a: value of class with inlined constructor: %d\n",
|
||||
with_inline.val_);
|
||||
|
||||
some_class_with_kernel from_library{};
|
||||
std::printf("a: value of class with constructor from library: %d\n",
|
||||
from_library.val_);
|
||||
}
|
||||
|
||||
Importantly, one of the constructors for that class is put into a shared library, whereas the other one happens to be
|
||||
inlined. If a user now links two different libraries, the outcome of the initialization of ``some_class_with_kernel``
|
||||
will depend on whether the inlined constructor is called and which of the libraries is loaded first by the linker.
|
||||
|
||||
Even worse, the state of a class depends on whether the constructor has been inlined or not and the order in which
|
||||
the linker loads the libraries.
|
||||
|
||||
.. code-block:: cmake
|
||||
|
||||
project(CUBVisDifferentArchitectures CUDA CXX)
|
||||
|
||||
add_library(cubvis_different_architectures_lib_a SHARED tu_a.cu)
|
||||
set_target_properties(cubvis_different_architectures_lib_a PROPERTIES CUDA_ARCHITECTURES "86;90a")
|
||||
|
||||
add_library(cubvis_different_architectures_lib_b SHARED tu_b.cu)
|
||||
set_target_properties(cubvis_different_architectures_lib_b PROPERTIES CUDA_ARCHITECTURES "75;86;90a")
|
||||
|
||||
add_executable(cubvis_different_architectures main.cu)
|
||||
set_target_properties(cubvis_different_architectures PROPERTIES CUDA_ARCHITECTURES "75;86")
|
||||
|
||||
target_link_libraries(cubvis_different_architectures PRIVATE
|
||||
cubvis_different_architectures_lib_a
|
||||
cubvis_different_architectures_lib_b)
|
||||
|
||||
add_executable(cubvis_different_architectures_switched main.cu)
|
||||
set_target_properties(cubvis_different_architectures_switched PROPERTIES CUDA_ARCHITECTURES "75;86")
|
||||
|
||||
target_link_libraries(cubvis_different_architectures_switched PRIVATE
|
||||
cubvis_different_architectures_lib_b
|
||||
cubvis_different_architectures_lib_a)
|
||||
|
||||
Execution the two libraries will result in the following:
|
||||
|
||||
.. code-block::
|
||||
|
||||
./different_architectures/different_architectures
|
||||
a: value of class with inlined constructor: 1610 <<<--- from main
|
||||
a: value of class with constructor from library: 1760 <<<--- from lib_a
|
||||
|
||||
./different_architectures/different_architectures_switched
|
||||
a: value of class with inlined constructor: 1610 <<<--- from main
|
||||
a: value of class with constructor from library: 2510 <<<--- from lib_b
|
||||
|
||||
|
||||
One solution would be to bake the architectures into the symbol name of the class, either via a defaulted template
|
||||
argument or an inline namespace. That way the usage of the non-inlined kernel would result in a linker error, because
|
||||
we did not provide a matching implementation.
|
||||
|
||||
.. code-block::
|
||||
|
||||
tmpxft_00048dff_00000000-6_main.compute_86.cudafe1.cpp:(.text.startup+0xc0):
|
||||
undefined reference to `some_class_with_kernel<int, 5120ul>::some_class_with_kernel()'
|
||||
|
||||
However, if all the functionality is within a non-inlined function we would still get different results, because all
|
||||
kernel definitions would be internal to the respective library.
|
||||
|
||||
.. code-block:: cpp
|
||||
|
||||
// In tu_a.cu and tu_b.cu
|
||||
void non_inlined_function() {
|
||||
some_class_with_kernel with_inline{1};
|
||||
std::printf("a: value of class with inlined constructor: %d\n",
|
||||
with_inline.val_);
|
||||
|
||||
some_class_with_kernel from_library{};
|
||||
std::printf("a: value of class with constructor from library: %d\n",
|
||||
from_library.val_);
|
||||
}
|
||||
|
||||
// In main.cu
|
||||
#include "kernel.cuh"
|
||||
|
||||
void non_inlined_function();
|
||||
|
||||
int main() {
|
||||
some_class_with_kernel with_inline{1};
|
||||
std::printf("a: value of class with inlined constructor: %d\n",
|
||||
with_inline.val_);
|
||||
|
||||
non_inlined_function();
|
||||
}
|
||||
|
||||
Executing this binary will give us again:
|
||||
|
||||
.. code-block::
|
||||
|
||||
./different_architectures/different_architectures
|
||||
a: value of class with inlined constructor: 1610 <<<--- from main
|
||||
a: value of class with inlined constructor: 1760 <<<--- from lib_a
|
||||
a: value of class with constructor from library: 1760 <<<--- from lib_a
|
||||
|
||||
./different_architectures/different_architectures_switched
|
||||
a: value of class with inlined constructor: 1610 <<<--- from main
|
||||
a: value of class with inlined constructor: 2510 <<<--- from lib_a
|
||||
a: value of class with constructor from library: 2510 <<<--- from lib_b
|
||||
|
||||
So there is not functional way we can solve this problem generically, because the moment a user actually uses any type
|
||||
of function that executes a kernel and puts that function into a shared library there is no guarantee which function
|
||||
is selected. The same happens if the user builds a type
|
||||
|
||||
.. code-block:: cpp
|
||||
|
||||
class user_defined_with_kernel {
|
||||
some_class_with_kernel val;
|
||||
|
||||
user_defined_with_kernel();
|
||||
__forceinline__ user_defined_with_kernel(T input) : val(input)
|
||||
{}
|
||||
};
|
||||
|
||||
void function_that_uses_kernel_inside();
|
||||
|
||||
If ``user_defined_with_kernel`` is ever baked into a library we would be back with the same exact problem,
|
||||
just one layer up. The user would need to know that ``some_class_with_kernel`` uses a kernel and then annotate *their*
|
||||
classes and functions appropriately. This is neither realistic nor feasible.
|
||||
|
||||
Lets circle back to the previous statement: ``This is bad.`` Is it really though?
|
||||
|
||||
Lets look at the prime example ``thrust::device_vector``, which uses a kernel for initialization. What happens if we
|
||||
accidentally run the kernel from another shared library compiled with different architectures? Worst case we are
|
||||
eating some performance regressions because the kernel will not utilize advanced features of a new architecture,
|
||||
but in the end the result of calling that kernel will not change the outcome.
|
||||
|
||||
This is because the kernel call is consistent *within* each library. As long as the user facing API does not rely on
|
||||
specific internals of a kernel to be called -which it should not-, then any of the two libraries will do.
|
||||
|
||||
Finally, the architectures that are passed around in ``__CUDA_ARCH_LIST__`` do *not* discriminate architecture families.
|
||||
There is currently no programmatic way to discriminate a library that has been compiled for ``SM90a`` from one that was
|
||||
compiled for ``SM90``. This is because the architecture specific macros are only available on device not on host.
|
||||
@@ -0,0 +1,10 @@
|
||||
project(DeveloperGuideDeviceKernelVisibility CUDA CXX)
|
||||
|
||||
add_executable(device_kernel_visibility main.cu)
|
||||
add_library(device_kernel_visibility_liba SHARED tu_a.cu)
|
||||
add_library(device_kernel_visibility_lib_b SHARED tu_b.cu)
|
||||
|
||||
target_link_libraries(
|
||||
device_kernel_visibility
|
||||
PRIVATE device_kernel_visibility_lib_a device_kernel_visibility_lib_b
|
||||
)
|
||||
@@ -0,0 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdio>
|
||||
|
||||
template <class T>
|
||||
__global__ void kernel(char ln, T* val)
|
||||
{
|
||||
printf("%c: kernel: set val = 42\n", ln);
|
||||
*val = 42;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
void a();
|
||||
void b();
|
||||
|
||||
int main()
|
||||
{
|
||||
a();
|
||||
b();
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
#include "kernel.cuh"
|
||||
|
||||
void b_launch(void (*k)(char, size_t*), char c, size_t* d_out);
|
||||
|
||||
void a_launch(void (*k)(char, int*), char c, int* d_out)
|
||||
{
|
||||
void* ptr = reinterpret_cast<void*>(k);
|
||||
|
||||
printf("a: kernel stub address: %p\n", ptr);
|
||||
|
||||
cudaFunction_t func{};
|
||||
if (cudaError_t error = cudaGetFuncBySymbol(&func, ptr))
|
||||
{
|
||||
printf("a: kernel NOT found in mapping: %s\n", cudaGetErrorString(error));
|
||||
}
|
||||
else
|
||||
{
|
||||
printf("a: kernel is in mapping: %s\n", cudaGetErrorString(error));
|
||||
}
|
||||
|
||||
cudaMemset(d_out, 0, sizeof(int));
|
||||
k<<<1, 1>>>(c, d_out);
|
||||
|
||||
if (cudaPeekAtLastError() != cudaSuccess)
|
||||
{
|
||||
printf("a: FAILED to launch kernel\n");
|
||||
}
|
||||
else
|
||||
{
|
||||
printf("a: launched kernel\n");
|
||||
}
|
||||
|
||||
if (cudaStreamSynchronize(0) != cudaSuccess)
|
||||
{
|
||||
printf("a: FAILED to synchronize stream\n");
|
||||
}
|
||||
else
|
||||
{
|
||||
printf("a: synchronized stream\n");
|
||||
}
|
||||
|
||||
int h_out{};
|
||||
if (cudaMemcpy(&h_out, d_out, sizeof(int), cudaMemcpyDeviceToHost) != cudaSuccess)
|
||||
{
|
||||
printf("a: FAILED to copy from device to host\n");
|
||||
}
|
||||
else
|
||||
{
|
||||
printf("a: copied from device to host\n");
|
||||
}
|
||||
|
||||
printf("a: out: %d\n", h_out);
|
||||
if (h_out != 42)
|
||||
{
|
||||
printf("a: kernel was NOT actually launched: out != 42\n");
|
||||
}
|
||||
else
|
||||
{
|
||||
printf("a: kernel was launched: out == 42\n");
|
||||
}
|
||||
|
||||
printf("\n");
|
||||
}
|
||||
|
||||
void a()
|
||||
{
|
||||
cudaGetLastError();
|
||||
|
||||
size_t* d_out{};
|
||||
cudaMalloc(&d_out, sizeof(size_t));
|
||||
cudaMemset(d_out, 0, sizeof(size_t));
|
||||
|
||||
void* ptr = reinterpret_cast<void*>(kernel<size_t>);
|
||||
|
||||
printf("a: kernel stub address: %p\n", ptr);
|
||||
|
||||
cudaFunction_t func{};
|
||||
if (cudaError_t error = cudaGetFuncBySymbol(&func, ptr))
|
||||
{
|
||||
printf("a: kernel NOT found in mapping: %s\n", cudaGetErrorString(error));
|
||||
}
|
||||
else
|
||||
{
|
||||
printf("a: kernel is in mapping: %s\n", cudaGetErrorString(error));
|
||||
}
|
||||
|
||||
kernel<<<1, 1>>>('a', d_out);
|
||||
|
||||
if (cudaPeekAtLastError() != cudaSuccess)
|
||||
{
|
||||
printf("b: FAILED to launch kernel\n");
|
||||
}
|
||||
else
|
||||
{
|
||||
printf("b: launched kernel\n");
|
||||
}
|
||||
|
||||
if (cudaStreamSynchronize(0) != cudaSuccess)
|
||||
{
|
||||
printf("a: FAILED to synchronize stream\n");
|
||||
}
|
||||
else
|
||||
{
|
||||
printf("a: synchronized stream\n");
|
||||
}
|
||||
|
||||
size_t h_out{};
|
||||
if (cudaMemcpy(&h_out, d_out, sizeof(size_t), cudaMemcpyDeviceToHost) != cudaSuccess)
|
||||
{
|
||||
printf("a: FAILED to copy from device to host\n");
|
||||
}
|
||||
else
|
||||
{
|
||||
printf("a: copied from device to host\n");
|
||||
}
|
||||
|
||||
printf("a: out: %d\n", static_cast<int>(h_out));
|
||||
if (h_out != 42)
|
||||
{
|
||||
printf("a: kernel was NOT actually launched: out != 42\n");
|
||||
}
|
||||
else
|
||||
{
|
||||
printf("a: kernel was launched: out == 42\n");
|
||||
}
|
||||
|
||||
cudaMemset(d_out, 0, sizeof(size_t));
|
||||
printf("\n");
|
||||
|
||||
printf("a: defers launch to b\n");
|
||||
b_launch(kernel<size_t>, 'b', d_out);
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
#include "kernel.cuh"
|
||||
|
||||
void a_launch(void (*k)(char, int*), char c, int* d_out);
|
||||
|
||||
void b_launch(void (*k)(char, size_t*), char c, size_t* d_out)
|
||||
{
|
||||
void* ptr = reinterpret_cast<void*>(k);
|
||||
|
||||
printf("b: kernel stub address: %p\n", ptr);
|
||||
|
||||
cudaFunction_t func{};
|
||||
if (cudaError_t error = cudaGetFuncBySymbol(&func, ptr))
|
||||
{
|
||||
printf("b: kernel NOT found in mapping: %s\n", cudaGetErrorString(error));
|
||||
}
|
||||
else
|
||||
{
|
||||
printf("b: kernel is in mapping: %s\n", cudaGetErrorString(error));
|
||||
}
|
||||
|
||||
k<<<1, 1>>>(c, d_out);
|
||||
|
||||
if (cudaPeekAtLastError() != cudaSuccess)
|
||||
{
|
||||
printf("b: FAILED to launch kernel\n");
|
||||
}
|
||||
else
|
||||
{
|
||||
printf("b: launched kernel\n");
|
||||
}
|
||||
|
||||
if (cudaStreamSynchronize(0) != cudaSuccess)
|
||||
{
|
||||
printf("b: FAILED to synchronize stream\n");
|
||||
}
|
||||
else
|
||||
{
|
||||
printf("b: synchronized stream\n");
|
||||
}
|
||||
|
||||
size_t h_out{};
|
||||
if (cudaMemcpy(&h_out, d_out, sizeof(size_t), cudaMemcpyDeviceToHost) != cudaSuccess)
|
||||
{
|
||||
printf("b: FAILED to copy from device to host\n");
|
||||
}
|
||||
else
|
||||
{
|
||||
printf("b: copied from device to host\n");
|
||||
}
|
||||
|
||||
printf("b: out: %d\n", static_cast<int>(h_out));
|
||||
if (h_out != 42)
|
||||
{
|
||||
printf("b: kernel was NOT actually launched: out != 42\n");
|
||||
}
|
||||
else
|
||||
{
|
||||
printf("b: kernel was launched: out == 42\n");
|
||||
}
|
||||
|
||||
printf("\n");
|
||||
}
|
||||
|
||||
void b()
|
||||
{
|
||||
cudaGetLastError();
|
||||
|
||||
int* d_out{};
|
||||
cudaMalloc(&d_out, sizeof(int));
|
||||
cudaMemset(d_out, 0, sizeof(int));
|
||||
|
||||
void* ptr = reinterpret_cast<void*>(kernel<int>);
|
||||
|
||||
printf("b: kernel stub address: %p\n", ptr);
|
||||
|
||||
cudaFunction_t func{};
|
||||
if (cudaError_t error = cudaGetFuncBySymbol(&func, ptr))
|
||||
{
|
||||
printf("b: kernel NOT found in mapping: %s\n", cudaGetErrorString(error));
|
||||
}
|
||||
else
|
||||
{
|
||||
printf("b: kernel is in mapping: %s\n", cudaGetErrorString(error));
|
||||
}
|
||||
|
||||
kernel<<<1, 1>>>('b', d_out);
|
||||
|
||||
if (cudaPeekAtLastError() != cudaSuccess)
|
||||
{
|
||||
printf("b: FAILED to launch kernel\n");
|
||||
}
|
||||
else
|
||||
{
|
||||
printf("b: launched kernel\n");
|
||||
}
|
||||
|
||||
if (cudaStreamSynchronize(0) != cudaSuccess)
|
||||
{
|
||||
printf("b: FAILED to synchronize stream\n");
|
||||
}
|
||||
else
|
||||
{
|
||||
printf("b: synchronized stream\n");
|
||||
}
|
||||
|
||||
int h_out{};
|
||||
if (cudaMemcpy(&h_out, d_out, sizeof(int), cudaMemcpyDeviceToHost) != cudaSuccess)
|
||||
{
|
||||
printf("b: FAILED to copy from device to host\n");
|
||||
}
|
||||
else
|
||||
{
|
||||
printf("b: copied from device to host\n");
|
||||
}
|
||||
|
||||
printf("b: out: %d\n", h_out);
|
||||
|
||||
if (h_out != 42)
|
||||
{
|
||||
printf("b: kernel was NOT actually launched: out != 42\n");
|
||||
}
|
||||
else
|
||||
{
|
||||
printf("b: kernel was launched: out == 42\n");
|
||||
}
|
||||
|
||||
cudaMemset(d_out, 0, sizeof(int));
|
||||
printf("\n");
|
||||
|
||||
printf("b: defers launch to a\n");
|
||||
a_launch(kernel<int>, 'b', d_out);
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
project(DeveloperGuideDifferentArchitectures CUDA CXX)
|
||||
|
||||
add_library(different_architectures_lib_a SHARED tu_a.cu)
|
||||
set_target_properties(
|
||||
different_architectures_lib_a
|
||||
PROPERTIES CUDA_ARCHITECTURES "86;90a"
|
||||
)
|
||||
|
||||
add_library(different_architectures_lib_b SHARED tu_b.cu)
|
||||
set_target_properties(
|
||||
different_architectures_lib_b
|
||||
PROPERTIES CUDA_ARCHITECTURES "75;86;90a"
|
||||
)
|
||||
|
||||
add_executable(different_architectures main.cu)
|
||||
set_target_properties(
|
||||
different_architectures
|
||||
PROPERTIES CUDA_ARCHITECTURES "75;86"
|
||||
)
|
||||
|
||||
target_link_libraries(
|
||||
different_architectures
|
||||
PRIVATE different_architectures_lib_a different_architectures_lib_b
|
||||
)
|
||||
|
||||
add_executable(different_architectures_switched main.cu kernel.cu)
|
||||
set_target_properties(
|
||||
different_architectures_switched
|
||||
PROPERTIES CUDA_ARCHITECTURES "75;86"
|
||||
)
|
||||
|
||||
target_link_libraries(
|
||||
different_architectures_switched
|
||||
PRIVATE different_architectures_lib_b different_architectures_lib_a
|
||||
)
|
||||
@@ -0,0 +1,42 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
|
||||
template <int... Archs>
|
||||
__attribute__((visibility("hidden"))) __host__ __device__ constexpr int sum_archs()
|
||||
{
|
||||
return (Archs + ... + 0);
|
||||
}
|
||||
|
||||
template <class T, auto Archs = sum_archs<__CUDA_ARCH_LIST__>()>
|
||||
__attribute__((visibility("hidden"))) __global__ void kernel(char ln, T* val)
|
||||
{
|
||||
printf("%c: kernel: set val = %i\n", ln, sum_archs<__CUDA_ARCH_LIST__>());
|
||||
*val = sum_archs<__CUDA_ARCH_LIST__>();
|
||||
}
|
||||
|
||||
__attribute__((visibility("hidden"))) __forceinline__ int use_kernel()
|
||||
{
|
||||
int* d_val{};
|
||||
cudaMalloc(&d_val, sizeof(size_t));
|
||||
kernel<<<1, 1>>>(d_val);
|
||||
int ret;
|
||||
if (cudaMemcpy(&ret, d_val, sizeof(size_t), cudaMemcpyDeviceToHost) != cudaSuccess)
|
||||
{
|
||||
printf("c: FAILED to copy from device to host\n");
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
template <class T = int>
|
||||
struct some_class_with_kernel
|
||||
{
|
||||
T val_;
|
||||
|
||||
some_class_with_kernel();
|
||||
__forceinline__ some_class_with_kernel(T)
|
||||
{
|
||||
val_ = use_kernel();
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,11 @@
|
||||
#include "kernel.cuh"
|
||||
|
||||
void non_inlined_function();
|
||||
|
||||
int main()
|
||||
{
|
||||
some_class_with_kernel with_inline{1};
|
||||
printf("a: value of class with inlined constructor: %d\n", with_inline.val_);
|
||||
|
||||
non_inlined_function();
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
#include "kernel.cuh"
|
||||
|
||||
template <class T>
|
||||
some_class_with_kernel<T>::some_class_with_kernel()
|
||||
{
|
||||
val_ = use_kernel();
|
||||
}
|
||||
|
||||
void non_inlined_function()
|
||||
{
|
||||
some_class_with_kernel with_inline{1};
|
||||
printf("a: value of class with inlined constructor: %d\n", with_inline.val_);
|
||||
|
||||
some_class_with_kernel from_library{};
|
||||
printf("a: value of class with constructor from library: %d\n", from_library.val_);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
#include "kernel.cuh"
|
||||
|
||||
template <class T>
|
||||
some_class_with_kernel<T>::some_class_with_kernel()
|
||||
{
|
||||
val_ = use_kernel();
|
||||
}
|
||||
|
||||
void non_inlined_function()
|
||||
{
|
||||
some_class_with_kernel with_inline{1};
|
||||
printf("a: value of class with inlined constructor: %d\n", with_inline.val_);
|
||||
|
||||
some_class_with_kernel from_library{};
|
||||
printf("a: value of class with constructor from library: %d\n", from_library.val_);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
project(DeveloperGuideHostStubVisibility CUDA CXX)
|
||||
|
||||
add_executable(host_stub_visibility main.cu)
|
||||
add_library(host_stub_visibility_lib_a SHARED tu_a.cu)
|
||||
add_library(host_stub_visibility_lib_b SHARED tu_b.cu)
|
||||
|
||||
target_link_libraries(
|
||||
host_stub_visibility
|
||||
PRIVATE host_stub_visibility_lib_a host_stub_visibility_lib_b
|
||||
)
|
||||
@@ -0,0 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdio>
|
||||
|
||||
template <class T>
|
||||
__global__ void kernel(char ln, T* val)
|
||||
{
|
||||
printf("%c: kernel: set val = 42\n", ln);
|
||||
*val = 42;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
void a();
|
||||
void b();
|
||||
|
||||
int main()
|
||||
{
|
||||
a();
|
||||
b();
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
#include "kernel.cuh"
|
||||
|
||||
void a()
|
||||
{
|
||||
int* d_out{};
|
||||
cudaMalloc(&d_out, sizeof(int));
|
||||
cudaMemset(d_out, 0, sizeof(int));
|
||||
|
||||
void* ptr = reinterpret_cast<void*>(kernel<int>);
|
||||
|
||||
printf("a: kernel stub address: %p\n", ptr);
|
||||
|
||||
cudaFunction_t func{};
|
||||
if (cudaError_t error = cudaGetFuncBySymbol(&func, ptr))
|
||||
{
|
||||
printf("a: kernel NOT found in mapping: %s\n", cudaGetErrorString(error));
|
||||
}
|
||||
else
|
||||
{
|
||||
printf("a: kernel is in mapping: %s\n", cudaGetErrorString(error));
|
||||
}
|
||||
|
||||
kernel<<<1, 1>>>('a', d_out);
|
||||
|
||||
if (cudaPeekAtLastError() != cudaSuccess)
|
||||
{
|
||||
printf("b: FAILED to launch kernel\n");
|
||||
}
|
||||
else
|
||||
{
|
||||
printf("b: launched kernel\n");
|
||||
}
|
||||
|
||||
if (cudaStreamSynchronize(0) != cudaSuccess)
|
||||
{
|
||||
printf("a: FAILED to synchronize stream\n");
|
||||
}
|
||||
else
|
||||
{
|
||||
printf("a: synchronized stream\n");
|
||||
}
|
||||
|
||||
int h_out{};
|
||||
if (cudaMemcpy(&h_out, d_out, sizeof(int), cudaMemcpyDeviceToHost) != cudaSuccess)
|
||||
{
|
||||
printf("a: FAILED to copy from device to host\n");
|
||||
}
|
||||
else
|
||||
{
|
||||
printf("a: copied from device to host\n");
|
||||
}
|
||||
|
||||
printf("a: out: %d\n", h_out);
|
||||
if (h_out != 42)
|
||||
{
|
||||
printf("a: kernel was NOT actually launched: out != 42\n");
|
||||
}
|
||||
else
|
||||
{
|
||||
printf("a: kernel was launched: out == 42\n");
|
||||
}
|
||||
|
||||
printf("\n");
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
#include "kernel.cuh"
|
||||
|
||||
void b()
|
||||
{
|
||||
int* d_out{};
|
||||
cudaMalloc(&d_out, sizeof(int));
|
||||
cudaMemset(d_out, 0, sizeof(int));
|
||||
|
||||
void* ptr = reinterpret_cast<void*>(kernel<int>);
|
||||
|
||||
printf("b: kernel stub address: %p\n", ptr);
|
||||
|
||||
cudaFunction_t func{};
|
||||
if (cudaError_t error = cudaGetFuncBySymbol(&func, ptr))
|
||||
{
|
||||
printf("b: kernel NOT found in mapping: %s\n", cudaGetErrorString(error));
|
||||
}
|
||||
else
|
||||
{
|
||||
printf("b: kernel is in mapping: %s\n", cudaGetErrorString(error));
|
||||
}
|
||||
|
||||
kernel<<<1, 1>>>('b', d_out);
|
||||
|
||||
if (cudaPeekAtLastError() != cudaSuccess)
|
||||
{
|
||||
printf("b: FAILED to launch kernel\n");
|
||||
}
|
||||
else
|
||||
{
|
||||
printf("b: launched kernel\n");
|
||||
}
|
||||
|
||||
if (cudaStreamSynchronize(0) != cudaSuccess)
|
||||
{
|
||||
printf("b: FAILED to synchronize stream\n");
|
||||
}
|
||||
else
|
||||
{
|
||||
printf("b: synchronized stream\n");
|
||||
}
|
||||
|
||||
int h_out{};
|
||||
if (cudaMemcpy(&h_out, d_out, sizeof(int), cudaMemcpyDeviceToHost) != cudaSuccess)
|
||||
{
|
||||
printf("b: FAILED to copy from device to host\n");
|
||||
}
|
||||
else
|
||||
{
|
||||
printf("b: copied from device to host\n");
|
||||
}
|
||||
|
||||
printf("b: out: %d\n", h_out);
|
||||
|
||||
if (h_out != 42)
|
||||
{
|
||||
printf("b: kernel was NOT actually launched: out != 42\n");
|
||||
}
|
||||
else
|
||||
{
|
||||
printf("b: kernel was launched: out == 42\n");
|
||||
}
|
||||
|
||||
printf("\n");
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
.. _cccl-development-visibility-host-stub-visibility:
|
||||
|
||||
|
||||
Host Stub Visibility Issue
|
||||
---------------------------
|
||||
|
||||
Consider the following simple translation unit (TU):
|
||||
|
||||
.. code-block:: cpp
|
||||
|
||||
#include <cstdio>
|
||||
#include <cuda/memory>
|
||||
|
||||
template <class T>
|
||||
__global__ void kernel(T *val) {
|
||||
printf("kernel: set val = 42\n");
|
||||
*val = 42;
|
||||
}
|
||||
|
||||
__device__ int val;
|
||||
|
||||
int main() {
|
||||
|
||||
kernel<<<1, 1>>>(cuda::get_device_address(val));
|
||||
}
|
||||
|
||||
The CUDA compiler frontend will turn this into:
|
||||
|
||||
.. code-block:: cpp
|
||||
|
||||
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 using the same ``kernel`` instance.
|
||||
|
||||
.. code-block:: cmake
|
||||
|
||||
project(HostStubVisibility CUDA CXX)
|
||||
|
||||
add_executable(host_stub_visibility main.cu)
|
||||
add_library(lib_a SHARED tu_a.cu)
|
||||
add_library(lib_b SHARED tu_b.cu)
|
||||
target_link_libraries(host_stub_visibility PRIVATE lib_a lib_b)
|
||||
|
||||
Each library will have its own fatbinary: ``d_kernel_a`` and ``d_kernel_b``, but the compiler
|
||||
generated host stub function ``h_kernel`` has weak external linkage, so after dynamic linkage, we'll end up having
|
||||
only one of them.
|
||||
|
||||
=== ===================== ============
|
||||
lib host device
|
||||
=== ===================== ============
|
||||
a 0xh_kernel_a 0xd_kernel_a
|
||||
b 0xh_kernel_a <- issue 0xd_kernel_b
|
||||
=== ===================== ============
|
||||
|
||||
Since there's a clash of stub function addresses, only one entry stored. When ``lib_b`` queries for the
|
||||
kernel using its address of ``h_kernel``, it's visible, although it might point to ``lib_a``'s fatbinary.
|
||||
The opposite case might happen as well, depending on loading order, linker etc and is undefined behavior.
|
||||
|
||||
Launching ``d_kernel`` from ``lib_b`` is not possible and leads to random errors. For instance, there seems to be
|
||||
some per CUDART global state. When the ``__cudaPushCallConfiguration`` is called in ``lib_b``, it affects the state of
|
||||
``cudart_b``, but the launch happens through ``h_kernel``, which is in ``lib_a``.
|
||||
|
||||
This sometimes leads to ``__global__ function call is not configured``. However, there might also be no error at all,
|
||||
and the kernel launch is silently skipped.
|
||||
|
||||
A simple example program that exemplifies this can be found
|
||||
`on github <https://github.com/NVIDIA/cccl/tree/main/docs/cub/developer/visibility/examples/host_stub_visibility>`_
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
:./host_stub_visibility/host_stub_visibility
|
||||
a: kernel stub address: 0x7f43318a415d <== same address as in B
|
||||
a: kernel is in mapping: no error <== kernel is found in the mapping
|
||||
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
|
||||
|
||||
b: kernel stub address: 0x7f43318a415d <== same address as in A
|
||||
b: kernel is in mapping: no error <== kernel is found in the mapping
|
||||
b: launched kernel
|
||||
b: synchronized stream
|
||||
b: copied from device to host
|
||||
b: out: 0
|
||||
b: kernel was NOT actually launched: out != 42 <== silent failure
|
||||
Reference in New Issue
Block a user