我使用库Thrust来获得两组更大的整数的交集。在使用2个小输入的测试中我得到了正确的结果,但是当我使用10个8 ^和65535 * 1024个元素的两个集合时,我得到了一个空集。谁能解释这个问题?将两个第一变量更改为较小的值,推力返回预期的交集。我的代码正在关注。
#include <thrust/set_operations.h>
#include <thrust/device_vector.h>
#include <thrust/device_ptr.h>
#include <iostream>
#include <stdio.h>
int main() {
int sizeArrayLonger = 100*1000*1000;
int sizeArraySmaller = 65535*1024;
int length_result = sizeArraySmaller;
int* list = (int*) malloc(4*sizeArrayLonger);
int* list_smaller = (int*) malloc(4*sizeArraySmaller);
int* result = (int*) malloc(4*length_result);
int* list_gpu;
int* list_smaller_gpu;
int* result_gpu;
// THE NEXT TWO FORS TRANSFORMS THE SMALLER ARRAY IN A SUBSET OF THE LARGER ARRAY
for (int i=0; i < sizeArraySmaller; i++) {
list_smaller[i] = i+1;
list[i] = i+1;
}
for (int i=sizeArraySmaller; i < sizeArrayLonger; i++) {
list[i] = i+1;
}
cudaMalloc(&list_gpu, sizeof(int) * sizeArrayLonger);
cudaMalloc(&list_smaller_gpu, sizeof(int) * sizeArraySmaller);
cudaMalloc(&result_gpu, sizeof(int) * length_result);
cudaMemcpy(list_gpu, list, sizeof(int) * sizeArrayLonger, cudaMemcpyHostToDevice);
cudaMemcpy(list_smaller_gpu, list_smaller, sizeof(int) * sizeArraySmaller, cudaMemcpyHostToDevice);
cudaMemset(result_gpu, 0, sizeof(int) * length_result);
typedef thrust::device_ptr<int> device_ptr;
thrust::set_intersection(device_ptr(list_gpu), device_ptr(list_gpu + sizeArrayLonger), device_ptr(list_smaller_gpu),
device_ptr(list_smaller_gpu + sizeArraySmaller), device_ptr(result_gpu), thrust::less<int>() );
// MOVING TO CPU THE MARKER ARRAY OF ELEMENTS OF INTERSECTION SET
cudaMemcpy(result, result_gpu, sizeof(int)*length_result, cudaMemcpyDeviceToHost);
cudaDeviceSynchronize();
// THIS LOOP ITERATES ALL ARRAY NAMED "result" WHERE THE POSITION ARE MARKED WITH 1
int counter = 0;
for (int i=0; i < length_result; i++)
if (result[i]) {
printf("\n-> %d", result[i]);
counter++;
}
printf("\nTHRUST -> Total of elements: %d\n", counter);
cudaDeviceReset();
return 0;
}
答案 0 :(得分:3)
OP似乎最近没有访问过,所以我会扩展我对其他读者的评论。 (我希望得到一些确认,即在编译期间指定正在使用的设备的计算目标也会确定OP的观察结果。)
根据我的测试,OP的代码将:
这最后的结果有点不直观。通常情况下,由于the runtime JIT mechanism,我们认为使用PTX编译的CUDA代码(例如nvcc -arch=sm_20 ...
或类似代码)与未来的体系结构是向前兼容的。
但是,有一个陷阱(以及推动中的一些相关问题。)CUDA代码查询它们实际运行的设备并不罕见(例如,通过cudaGetDeviceProperties
)和根据使用的设备做出决策(例如内核配置决策)。具体来说,在这种情况下,推力是在引擎盖下启动内核,并根据实际使用的设备决定网格x尺寸的大小以选择该内核。对于此参数,CC 2.x设备限制为65535,但CC 3.x和更高设备have a much higher limit。因此,在这种情况下,对于足够大的数据集,如果推力检测到它在cc3.0设备上运行,它将使用大于65535的网格x维度配置此特定内核。(对于足够小的数据集,它不会这样做,所以这个可能的错误不会浮出水面。因此,这个问题与问题规模有着松散的联系。)
如果我们在二进制文件中嵌入了cc 2.x和cc 3.x PTX(或适当的SASS),那么仍然不会出现问题。但是,如果我们在二进制文件中只嵌入了cc2.x PTX,那么JIT进程将使用它来创建适合在cc 3.x设备上运行的机器代码,如果这是正在使用的设备。 但是这个正向JIT编译的SASS仍然受CC 2.x限制,包括网格X维度限制65535.但是cudaGetDeviceProperties
返回设备是cc3的事实。 x设备,因此如果将此信息用于此特定决策(可接受的网格X维度),则此信息将具有误导性。
由于此序列,内核配置不正确,并且内核启动失败,并出现特定类型的非粘性CUDA运行时API错误。这种类型的非粘性错误不会破坏CUDA上下文,因此仍然允许进一步的CUDA操作,并且未来的CUDA API调用将不会返回此错误。为了在CUDA内核启动后捕获此类错误,在内核启动后发出cudaGetLastError()
或cudaPeekAtLastError()
调用是必要的,正如proper cuda error checking所建议的那样。如果不这样做就意味着错误会丢失&#34;并且不会从未来的CUDA API调用中发现(cudaGetLastError()
或cudaPeekAtLastError()
除外),因为它们不会在状态返回值中指示存在此错误或内核启动失败。
通过仔细使用cuda性能分析工具,例如,大多数情况都是可以发现的。 nvprof
,在过往和失败的情况下,以及cuda-memcheck
。在传递的情况下,cuda-memcheck
报告没有错误,并且探查器显示8次调用cudaLaunch
以及实际在GPU上执行的8个内核。在失败的情况下,cuda-memcheck
报告上述类型的2个内核启动失败,并且探查器显示8个cudaLaunch
的调用,但实际上只在GPU上执行了6个内核。失败的内核在cc2.x GPU上运行时配置了网格X维度65535,并且在cc3.x GPU上运行时配置了更大的数字。
因此,通过适当的cuda错误检查,上述序列虽然不一定是理想的,但至少会因显式错误而失败。但OP的代码无声地失败 - 它在失败的情况下返回了错误的结果,但推力并没有引发任何类型的错误。
事实证明,在引擎盖下,对从集合操作(特别是至少这一个)启动的内核的推力错误检查具有这种特定的错误检查间隙。
通过仔细研究分析器输出,我们可以发现在这种情况下哪些文件包含推力用于启动闭包的代码(即内核启动实际来自哪里)。 (您也可以通过仔细跟踪模板序列来解决这个问题。)在特定的失败案例中,我认为内核启动来自here。如果我们查看其中一个内核启动,我们会看到类似this的内容:
#ifndef __CUDA_ARCH__
kernel<<<(unsigned int) num_blocks, (unsigned int) block_size, (unsigned int) smem_size, stream(thrust::detail::derived_cast(exec))>>>(f);
#else
...
#endif // __CUDA_ARCH__
synchronize_if_enabled("launch_closure_by_value");
在内核启动后立即调用 synchronize_if_enabled
(在此特定代码路径中)。可以找到该功能here:
inline __host__ __device__
void synchronize_if_enabled(const char *message)
{
// XXX this could potentially be a runtime decision
// note we always have to synchronize in __device__ code
#if __THRUST_SYNCHRONOUS || defined(__CUDA_ARCH__)
synchronize(message);
#else
// WAR "unused parameter" warning
(void) message;
#endif
调用synchronize()
:
inline __host__ __device__
void synchronize(const char *message)
{
throw_on_error(cudaDeviceSynchronize(), message);
} // end synchronize()
我们在synchronize()
中看到throw_on_error
调用cudaDeviceSynchronize()
,它消除了之前的非粘性错误11,表示错误配置的内核启动尝试,实际上返回cudaSuccess
(因为cudaDeviceSynchronize()
操作本身实际上是成功的。)
所以总结是存在两个问题:
Thrust(在这种情况下)做出关于内核启动配置的运行时决定,如果执行设备是cc3.0或更高版本并且代码是针对cc2.x(仅限)编译的,那么这将是不正确的。
对此特定set_intersection调用的推力错误检查不足,因为它没有正确的机制来捕获与错误配置的内核启动相关联的非粘性CUDA运行时API错误(错误11)。
如果您打算在cc3.0或更高版本的设备上运行,那么建议始终编译指定cc3.0或更高目标(至少)的推力代码。 (当然,您可以指定 cc2.x和cc3.x目标,并选择nvcc
命令行开关。)Thrust使用引擎盖下的各种发射机制并且并非所有(可能是大多数)都不受这种特殊缺陷(#2)的影响,但是(对我来说)这个特定的set_intersection
呼叫 会受到这种不足的影响,这次(推力v1.8)。
我不清楚(对我而言)有一种方法可以系统地解决上面的第一个问题(#1)。我已将第二个问题(#2)提请推力开发人员注意(通过RFE或错误报告。)
作为一种解决方法,推力开发人员可以在他们的推力应用程序中插入cudaGetLastError()
的调用(可能在最后),以防止这种类型的错误被静音&#34;。