【问题标题】:Why does this CUDA program crash when omitting the const qualifier?为什么这个 CUDA 程序在省略 const 限定符时会崩溃?
【发布时间】:2016-01-30 19:53:06
【问题描述】:

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

#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 中的十六进制数字在相同源代码的两次编译之间甚至不同,因此可以忽略。出于某种原因,非工作示例具有额外的包装功能。

【问题讨论】:

  • 看来我的观察(CUDA 7.5,MSVS 2010)与你的略有不同:只要模板声明和模板定义在使用 const 时匹配(即两者都有它或两者都没有),内核被调用并按预期打印传递的值。我没有足够的 C++ 语言律师来说明这里应该发生什么;可能这也可能是 CUDA 编译器中的一个错误,但我不愿意这么说,因为我不确定当 const 在声明和定义之间不匹配时代码是否会调用未定义的行为。
  • @njuffa:我认为这些答案是相关的:stackoverflow.com/questions/18215686/… 顺便说一句:float * 上的 __restrict__ 关键字也会出现同样的问题。只有当声明也有它时它才有效。
  • 请注意,问题指的是 C,而 CUDA 是 C++ 的变体。虽然 C 和 C++ 密切相关,但它们之间存在许多差异,

标签: templates cuda


【解决方案1】:

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

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

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

【讨论】:

  • 我的意图是去除不必要的类型限定符的接口以清理它。 const 只是一个示例,但__restrict__volatile 也会出现问题。但是这些类型限定符应该只与实现相关,用于编译器错误检查和优化。它对接口(即声明)没有任何意义,至少在 C 语言中是:stackoverflow.com/questions/18215686/…
  • @mxmlnkn 为什么您将const 限定词视为肮脏的标志? :) 再次,正如我所说 - 可以通过检查程序集找到答案。
【解决方案2】:

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

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);
[...]

我从 diff 中删除了只有 (const float) 而不是 (float) 之类的差异的行

在我看来,在为模板化声明创建中间包装函数时,nvcc 中有一个错误。因为类型是复制粘贴并更改为引用调用,所以内核本身可能相同,但包装调用不同,因为它是 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 &

当 grepping 包装函数时,我们会发现重载函数是如何定义和调用的:

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 引用调用重载函数调用 exit(1)

由于某些原因,除了将 const 按值复制转换为 const 按引用调用的错误之外,nvcc 似乎混淆了原始文件中的“两个”内核。非 const 声明被转换为调用 __device_stub__Z10testKernelIfEvT_ 的包装函数,而具有 const 按值复制参数的函数定义被转换为调用 exit(1) 的包装函数。

不幸的是,我不是 CUDA 开发人员,所以我无法提交错误报告,但也许我说服了任何人为我这样做。或者也许一些反馈 cmet 证明我的回答是错误的。

【讨论】:

    猜你喜欢
    • 2017-04-01
    • 2021-07-23
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-06-15
    相关资源
    最近更新 更多