首页 > 解决方案 > 如何在 ArrayFire 中正确使用固定内存?

问题描述

在 ArrayFire 中使用固定内存时,性能会降低。

我尝试了各种创建固定内存并从中创建数组的方法,例如。cudaMallocHost。使用带有 cudaMemcpy 的 cudaMallocHost 方式非常快(几百微秒),但随后创建/初始化 arrayfire 数组非常慢(~ 2-3 秒)。最后我想出了以下方法,分配大约需要 2-3 秒,但可以移动到其他地方。使用主机数据初始化阵列是令人满意的(100 - 200 微秒),但现在操作(在这种情况下为 FFT)非常慢:~ 400 毫秒。我应该添加输入信号的大小是可变的,但是对于时间我使用了 64K 样本(复数)。另外,为了简洁,我没有提供我的计时功能,但这不是问题,我已经使用其他方法计时并且结果是一致的。

// Use the Frequency-Smoothing method to calculate the full 
// Spectral Correlation Density
// currently the whole function takes ~ 2555 msec. w/ signal 64K samples
// and window_length = 400 (currently not implemented)
void exhaustive_fsm(std::vector<std::complex<double>> signal, uint16_t window_length) {

  // Allocate pinned memory (eventually move outside function)
  // 2192 ms.
  af::af_cdouble* device_ptr = af::pinned<af::af_cdouble>(signal.size());

  // Init arrayfire array (eventually move outside function)
  // 188 us.
  af::array s(signal.size(), device_ptr, afDevice);

  // Copy to device
  // 289 us.
  s.write((af::af_cdouble*) signal.data(), signal.size() * sizeof(std::complex<double>), afHost);

  // FFT
  // 351 ms. equivalent to:
  // af::array fft = af::fft(s, signal.size());
  af::array fft = zrp::timeit(&af::fft, s, signal.size());
  fft.eval();

  // Convolution

  // Copy result to host

  // free memory (eventually move outside function)
  // 0 ms.
  af::freePinned((void*) s.device<af::af_cdouble>());

  // Return result
}

正如我上面所说,FFT 需要大约 400 毫秒。这个使用犰狳的功能需要大约 110 毫秒。包括卷积,使用 FFTW 的 FFT 大约需要 5 毫秒。同样在我的机器上使用 ArrayFire FFT 示例我得到以下结果(修改为使用 c64)

            A             = randu(1, N, c64);)

基准 1×N CX fft

   1 x  128:                    time:     29 us.
   1 x  256:                    time:     31 us.
   1 x  512:                    time:     33 us.
   1 x 1024:                    time:     41 us.
   1 x 2048:                    time:     53 us.
   1 x 4096:                    time:     75 us.
   1 x 8192:                    time:    109 us.
   1 x 16384:                   time:    179 us.
   1 x 32768:                   time:    328 us.
   1 x 65536:                   time:    626 us.
   1 x 131072:                  time:   1227 us.
   1 x 262144:                  time:   2423 us.
   1 x 524288:                  time:   4813 us.
   1 x 1048576:                 time:   9590 us.

所以我能看到的唯一区别是使用固定内存。知道我哪里出错了吗?谢谢。

编辑

我注意到在运行 AF FFT 示例时,在第一次打印出来之前有一个明显的延迟(即使时间不包括这个延迟)。所以我决定创建一个类并将所有的分配/解除分配到 ctor/dtor 中。出于好奇,我还在 ctor 中放了一个 FFT,因为我还注意到,如果我运行第二个 FFT,大约需要 600 微秒。与我的基准一致。果然,运行“初步” FFT 似乎“初始化”了某些东西,随后的 FFT 运行得更快。必须有更好的方法,我一定错过了一些东西。

标签: c++arrayfire

解决方案


我是 pradeep,也是 ArrayFire 的开发者之一。

首先,所有 ArrayFire 函数(CUDA 和 OpenCL)后端都有一些启动成本,其中包括设备预热和/或内核缓存(内核在第一次调用特定函数时被缓存)。这就是原因,您注意到第一次运行后运行时间更好。这也是原因,我们几乎总是强烈建议使用我们内置的 timeit函数来计时 arrayfire 代码,因为它在一组运行中平均,而不是使用第一次运行。

正如您已经从实验中推测的那样,以受控方式保持固定内存分配总是更好。如果您还没有意识到使用固定内存时所涉及的权衡取舍,您可以从 NVIDIA 的这篇博客文章开始(它同样适用于 OpenCL 后端的固定内存,当然有任何供应商特定的限制)。超链接帖子中建议的一般准则如下:

您不应该过度分配固定内存。这样做会降低整体系统性能,因为它会减少操作系统和其他程序可用的物理内存量。很难提前知道多少是太多,因此与所有优化一样,测试您的应用程序和它们运行的​​系统以获得最佳性能参数。

如果可能,以下是我为您的 FFT 使用固定内存的路线

  1. 将固定分配/释放封装成 RAII 格式,您现在已经从您编辑的描述中执行此操作。
  2. 如果可能,只进行一次固定内存分配 - 如果您的数据大小是静态的。

除此之外,我认为您的功能在几个方面是不正确的。我将按行顺序介绍该功能。

af::af_cdouble* device_ptr = af::pinned(signal.size());

此调用不会在设备/GPU 上分配内存。它是主机上的页面锁定内存,RAM。

af::array s(signal.size(), device_ptr, afDevice);

因为,af::pinned 不分配设备内存,所以它不是设备指针,枚举是 afHost。所以,电话将是af::array s(signal.size(), ptr);

s.write自己使用正确,但我相信您的用例不需要它。

以下是我会做的。

  • 对返回的指针使用 RAII 构造,af::pinned并且只分配一次。确保您没有太多这些页面锁定的分配。
  • 使用页面锁定分配作为常规主机分配,而不是std::vector<complex>因为这是主机内存,只是页面锁定。std::vector如果您以某种方式进行操作,这将涉及在主机端编写一些额外的代码。否则,您可以只使用 RAIIed-pinned-pointer 来存储您的数据。
  • 所有,您需要将您的 fft 数据传输到设备是af::array s(size, ptr)

此时,您必须花时间进行的操作是从固定内存转移到 GPU,这是上面列表中的最后一个调用;fft 执行;复制回主机。


推荐阅读