【发布时间】:2014-04-21 18:55:33
【问题描述】:
在使用 NVIDIA Visual Profiler 分析我的 CUDA 应用程序时,我注意到cudaStreamSynchronize 之后的任何操作都会阻塞,直到所有流完成为止。这是非常奇怪的行为,因为如果cudaStreamSynchronize 返回,则意味着流已完成,对吗?这是我的伪代码:
std::list<std::thread> waitingThreads;
void startKernelsAsync() {
for (int i = 0; i < 200; ++i) {
cudaHostAlloc(cpuPinnedMemory, size, cudaHostAllocDefault);
memcpy(cpuPinnedMemory, data, size);
cudaMalloc(gpuMemory);
cudaStreamCreate(&stream);
cudaMemcpyAsync(gpuMemory, cpuPinnedMemory, size, cudaMemcpyHostToDevice, stream);
runKernel<<<32, 32, 0, stream>>>(gpuMemory);
cudaMemcpyAsync(cpuPinnedMemory, gpuMemory, size, cudaMemcpyDeviceToHost, stream);
waitingThreads.push_back(std::move(std::thread(waitForFinish, cpuPinnedMemory, stream)));
}
while (waitingThreads.size() > 0) {
waitingThreads.front().join();
waitingThreads.pop_front();
}
}
void waitForFinish(void* cpuPinnedMemory, cudaStream_t stream, ...) {
cudaStreamSynchronize(stream);
cudaStreamDestroy(stream); // <== This blocks until all streams are finished.
memcpy(data, cpuPinnedMemory, size);
cudaFreeHost(cpuPinnedMemory);
cudaFree(gpuMemory);
}
如果我把cudaFreeHost放在cudaStreamDestroy之前,那么它就变成了阻塞操作。
这里有什么概念上的错误吗?
编辑:我发现了另一种奇怪的行为,有时它会在处理流的过程中解除阻塞,然后处理其余的流。
正常行为:
奇怪的行为(经常发生):
EDIT2:我在 CUDA 6.0 上使用计算能力 3.5 的 Tesla K40c 卡进行测试。
正如 cmets 中所建议的,减少流的数量可能是可行的,但是在我的应用程序中,内存传输非常快,我想主要使用流来动态调度 GPU 的工作。问题是,在流完成后,我需要从固定内存中下载数据并清除分配的内存以用于似乎阻塞操作的更多流。
我为每个数据集使用一个流,因为每个数据集都有不同的大小,并且处理时间非常长。
有什么办法解决这个问题吗?
【问题讨论】:
-
您不是隐含地假设可以在 GPU 繁忙时执行主机驱动程序级别的操作,例如流销毁和固定内存释放吗?你有什么理由认为它们应该是非阻塞的?
-
@talonmies 我愿意,你是对的。但是,为了使用流,您必须使用固定内存,如果对流和固定内存的操作会阻塞,那么它不是很有用。此外,即使 GPU 很忙,
cudaStreamCreate、cudaHostAlloc和cudaMalloc等操作也能正常工作。 -
我认为内存空闲操作(
cudaFree、cudaHostFree)可能会阻塞。如果cudaStreamDestroy也被阻止,我不会感到惊讶。我认为您的陈述“如果对流和固定内存的操作将被阻塞,那么它不是很有用”,可能会更好地附加“对我来说”。内存分配、空闲操作和流创建/销毁的典型使用场景是在应用程序的开始和结束时在对阻塞不敏感的区域执行这些操作。它们可能不适用于这种特定的代码设计。 -
对不起,我不同意你的分析。拥有 1 TB 的数据绝不会限制我预先进行分配。从性能的角度来看,内存分配很昂贵,即使在 CPU 代码中也是如此,因此进行大量小分配通常比进行少量大分配并自己组织和重用它们要慢。大多数处理任意数据大小并在单个 GPU 上运行的 CUDA 应用程序最多需要 3 或 4 个流才能获得完整的性能和并发性。
-
我会从this presentation开始。尤其是幻灯片 22。实际上,它只是以循环方式循环并重新使用 3 个流。如果时间允许,我会回来重写你的伪代码。我认为为此也不需要启动多个主机线程。