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:
muh-bot
2026-08-07 02:34:33 +00:00
parent 3f97dca7ad
commit 2a7ca101d7
908 changed files with 121615 additions and 0 deletions

View File

@@ -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

View File

@@ -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.

View File

@@ -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
)

View File

@@ -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;
}

View File

@@ -0,0 +1,8 @@
void a();
void b();
int main()
{
a();
b();
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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
)

View File

@@ -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();
}
};

View File

@@ -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();
}

View File

@@ -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_);
}

View File

@@ -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_);
}

View File

@@ -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
)

View File

@@ -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;
}

View File

@@ -0,0 +1,8 @@
void a();
void b();
int main()
{
a();
b();
}

View File

@@ -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");
}

View File

@@ -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");
}

View File

@@ -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