如何在ArrayFire中正确使用固定内存?

时间:2019-03-31 01:17:51

标签: c++ arrayfire

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

我尝试了各种方法来创建固定内存并从中创建数组,例如。 cudaMallocHost。使用带有cudaMemcpy的cudaMallocHost的方法非常快(几百个usec。),但是创建/初始化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毫秒。使用Armadillo的此功能大约需要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中。出于好奇,我也将FFT放在了ctor中,因为我还注意到,如果我运行第二次FFT,大约需要600微秒。与我的基准保持一致。足够肯定的是,运行“初步” FFT似乎可以“初始化”某些内容,并且后续FFT的运行速度要快得多。必须有一种更好的方法,我必须丢失一些东西。

1 个答案:

答案 0 :(得分:1)

我很虔诚,是ArrayFire的开发人员之一。

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

正如您已经从实验中推测的那样,最好以受控的方式保持固定的内存分配。如果您在使用固定内存时还没有权衡取舍,则可以从NVIDIA的this blog post开始(这同样适用于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执行;复制回主机。