省略const限定符时,为什么这个CUDA程序会崩溃?

时间:2016-01-30 19:53:07

标签: templates cuda

我有以下最小的非工作示例:

#include <cstdio>
#include <cuda_runtime_api.h>

/* this declaration would normally be in a header, but it doesn't matter */
template<class T_PREC> __global__ void testKernel( T_PREC );

template<class T_PREC> __global__ void testKernel( T_PREC const x )
{
    printf( "%f", x );
}

int main()
{
    printf("calling kernel...");
    testKernel<<<1,1>>>( 3.0f );
    cudaDeviceSynchronize();
    printf("OK\n");
    return 0;
}

我用

编译和运行
nvcc simple.cu && ./a.out

输出结果为:

calling kernel...

意味着程序崩溃之前它既不能打印“OK”也不能打印浮点数。这不是分段错误,所以我不能回溯任何东西。我正在使用CUDA 7.0.27。在gdb中运行时,消息为:

[Inferior 1 (process 27899) exited with code 01]

上面的例子有四件事可以解决:

  • 不要使用CUDA:

    template<class T_PREC> void testKernel( T_PREC );
    
    template<class T_PREC> void testKernel( T_PREC const x )
    {
        printf( "%f", x );
    }
    
    int main()
    {
        printf("calling kernel...");
        testKernel( 3.0f );
        cudaDeviceSynchronize();
        printf("OK\n");
        return 0;
    }
    
  • 不要使用模板:

    __global__ void testKernel( float );
    
    __global__ void testKernel( float const x )
    {
        printf( "%f", x );
    }
    
  • 省略声明(如果我需要从库中获取声明,则不是选项)

    //template<class T_PREC> void testKernel( T_PREC );
    
  • 不要在声明中省略const限定符:

    template<class T_PREC> __global__ void testKernel( T_PREC const );
    

    这是最合理的选择,但我不明白我为什么要这样做。在普通的C ++中,按值调用的const限定符不应更改函数签名。即使它确实如此,它也不应该链接,只会在执行时崩溃。那么为什么CUDA表现不同,为什么只有模板?

额外考试:

由于汇编代码本身对我来说太难了,我查看了创建的可执行文件:

nvcc sameTypeQualifier/main.cu -o same.o
    [no warning output whatsoever]

nvcc diffTypeQualifier/main.cu -o diff.o
    diffTypeQualifier/main.cu: In instantiation of ‘void __wrapper__device_stub_testKernel(T_PREC* const&) [with T_PREC = float]’:
    diffTypeQualifier/main.cu:8:45:   required from ‘void testKernel(T_PREC*) [with T_PREC = float]’
    diffTypeQualifier/main.cu:15:67:   required from here
    diffTypeQualifier/main.cu:7:86: warning: unused parameter ‘x’ [-Wunused-parameter]
             template<class T_PREC> __global__ void testKernel( T_PREC * const x )
                                                                                          ^
 diff <(nm -C same.o | sed 's/^[0-9a-f]*//') <(nm -C diff.o | sed 's/^[0-9a-f]*//')
    389a390
    >  t void __wrapper__device_stub_testKernel<float>(float*&)
    419c420
    <  t __sti____cudaRegisterAll_39_tmpxft_000050c8_00000000_9_main_cpp1_ii_main()
    ---
    >  t __sti____cudaRegisterAll_39_tmpxft_0000511c_00000000_9_main_cpp1_ii_main()

cudaRegisterAll中的十六进制数字甚至在相同源代码的两个编译之间有所不同,因此可以忽略它。由于某种原因,非工作示例具有额外的包装函数。

2 个答案:

答案 0 :(得分:1)

恕我直言,你只是误导了编译器,导致未定义的行为。实际上(我希望有更深入的C ++知识的人会发表评论)你告诉编译器会有一个函数接受带有声明的int ,然后让编译器生成一个接受一个函数的函数常数int。从我的观点来看,编译器应该告诉你 - 不,不,这里存在歧义,我有一个未解决的符号并且失败。

为什么呢?好吧,一种可能的情况是由于一些奇怪的优化,因为定义采用一个不会被修改的常量int,因此不需要存储它可以减少到编译时常量。另一方面,为调用而生成的代码假定它必须通过它。我不确定它是否如此(理解的最好方法是反汇编这个例子)但我认为这样的例子足以怀疑示例本身的正确性。

为什么不像其他任何情况那样保持声明和定义相同?

答案 1 :(得分:0)

在比较中间文件时,可以发现一些有趣的东西:

nvcc --keep [...]
colordiff -r c/ nc/

[...]
diff c/main.cu.cpp.ii nc/main.cu.cpp.ii
32767c32767
< template< class T_PREC> static void __wrapper__device_stub_testKernel(const T_PREC &); template< class T_PREC> void testKernel(const T_PREC);
---
> template< class T_PREC> static void __wrapper__device_stub_testKernel(T_PREC &); template< class T_PREC> void testKernel(T_PREC);
[...]
diff c/main.cudafe1.cpp nc/main.cudafe1.cpp
70764c70764
< template< class T_PREC> static void __wrapper__device_stub_testKernel(const T_PREC &); template< class T_PREC> void testKernel(const T_PREC);
---
> template< class T_PREC> static void __wrapper__device_stub_testKernel(T_PREC &); template< class T_PREC> void testKernel(T_PREC);
[...]

我从差异中剥离了线条,这些线条只有(const float)而不是(float)

之类的差异

在我看来,在创建模板化声明的中间包装函数时,nvcc中存在一个错误。因为类型是复制粘贴并更改为逐个引用调用,所以内核本身可能是相同的,但不是包装器调用,因为如果它是一个const调用引用或非const调用,它会有所不同 - 引用。此外,在我看来,这是一个错误,仅仅是声明首先创建了一个包装调用。

这是一个C ++示例,演示了发生的问题:

#include<cstdio>

void f( float const & x ) { printf( "float const &\n", x ); }
void f( float       & x ) { printf( "float &\n", x ); }

int main( void )
{
    f( 3.0 );
    float x = 3.0;
    f( x );
}

该计划的输出是:

float const &
float &

当greper for wrapper函数时,我们发现如何定义和调用重载函数:

grep -C20 '__wrapper__device_stub_testKernel' nc/main.cu.cpp.ii

和输出:

# 4 "main.cu"
template< class T_PREC> static void __wrapper__device_stub_testKernel(T_PREC &);
template< class T_PREC> void testKernel(T_PREC);
# 5 "main.cu"
template< class T_PREC> static void __wrapper__device_stub_testKernel(const T_PREC &x)
{
    exit(1);
}
# 5 "main.cu"
template< class T_PREC> void testKernel(const T_PREC x)
{
    # 6 "main.cu"
    __wrapper__device_stub_testKernel<T_PREC>(x);
    # 8 "main.cu"
    return;
}
# 10 "main.cu"
int main()
# 11 "main.cu"
{
    # 12 "main.cu"
    printf("calling kernel...");
    # 13 "main.cu"
    (cudaConfigureCall(1, 1)) ? (void)0 : (testKernel)((3.0F));
    # 14 "main.cu"
    cudaDeviceSynchronize();
    # 15 "main.cu"
    printf("OK\n");
    # 16 "main.cu"
    return 0;
    # 17 "main.cu"
}
[...]
static void __device_stub__Z10testKernelIfEvT_(float __par0)
{
    if (cudaSetupArgument((void *)(char *)&__par0, sizeof(__par0), (size_t)0UL) != cudaSuccess) 
        return;
    {
        volatile static char *__f __attribute__((unused));
        __f = ((char *)( (void ( *)(float))testKernel<float> ) );
        (void)cudaLaunch( ((char *)((void ( *)(float))testKernel<float> )) );
    };
}
[...]
template<> void __wrapper__device_stub_testKernel<float>( float &__cuda_0)
{
    __device_stub__Z10testKernelIfEvT_( __cuda_0);
}

(注意:我添加了一些缩进和换行符以提高可读性)

因此,当非const const-by-reference函数调用可能的内核时,const call-by-reference重载函数调用exit(1)

由于某些原因,除了将const copy-by-value转换为const call-by-reference的错误之外,似乎nvcc混淆了原始文件中的“two”内核。非const声明被转换为一个调用__device_stub__Z10testKernelIfEvT_的包装函数,而带有const copy-by-value参数的函数定义被转换为一个调用exit(1)的包装器。

不幸的是,我不是CUDA开发人员所以我不能提交错误报告,但也许我说服任何人为我做这件事。或者也许一些反馈意见证明我的答案是错误的。