oneAPI GPU 优化指南 - 原子操作

本章节翻译by chenshusmail@163.com 原文:Atomic Operations (intel.com)

目录

原子操作中的数据类型

全局和本地地址空间中的原子操作

基于 USM 数据的原子操作


原子操作允许多个 work-item 通过内存进行任何跨 work-item 通信。 SYCL 原子类似于 C++ 原子,保证被原子保护的资源的访问作为单个单元执行。 以下因素影响原子操作的性能和合法性:

  • 数据类型

  • 本地与全局地址空间

  • 主机端、共享和设备端分配的 USM

原子操作中的数据类型

下面的 kernel 展示了在 SYCL 中实现归约(reduction)操作的方法,其中每个 work-item 原子地更新全局累加器。 这种加法的输入数据类型和应用此 reduction 操作的向量是整型数。与用于 reduction 的其他技术(如阻塞 blocking)相比, 此 kernel 的性能更为合理。

    q.submit([&](auto &h) {
      sycl::accessor buf_acc(buf, h, sycl::read_only);
      sycl::accessor sum_acc(sum_buf, h, sycl::write_only, sycl::no_init);

      h.parallel_for(data_size, [=](auto index) {
        size_t glob_id = index[0];
        auto v = sycl::atomic_ref<int, sycl::memory_order::relaxed,
                                  sycl::memory_scope::device,
                                  sycl::access::address_space::global_space>(
            sum_acc[0]);
        v.fetch_add(buf_acc[glob_id]);
      });
    });

如果向量的数据类型是浮点数或双精度浮点数,如下面的 kernel 所示,由于某些加速器缺乏对浮点数或双精度原子的硬件支持, 对应的性能会受到影响。基于硬件是否支持原子操作,下面两个 kernel 演示了原子加操作(add)的执行时间有巨大差异。

//
int VectorInt(sycl::queue &q, int iter) {
  VectorAllocator<int> alloc;
  AlignedVector<int> a(array_size, alloc);
  AlignedVector<int> b(array_size, alloc);

  InitializeArray<int>(a);
  InitializeArray<int>(b);
  sycl::range num_items{a.size()};
  sycl::buffer a_buf(a);
  sycl::buffer b_buf(b);
  auto start = std::chrono::steady_clock::now();
  for (int i = 0; i < iter; i++) {
    q.submit([&](sycl::handler &h) {
      // InpuGt accessors
      sycl::accessor a_acc(a_buf, h, sycl::read_write);
      sycl::accessor b_acc(a_buf, h, sycl::read_only);

      h.parallel_for(num_items, [=](auto i) {
        auto v = sycl::atomic_ref<int, sycl::memory_order::relaxed,
                                  sycl::memory_scope::device,
                                  sycl::access::address_space::global_space>(
            a_acc[0]);
        v += b_acc[i];
      });
    });
  }
  q.wait();
  auto end = std::chrono::steady_clock::now();
  std::cout << "Vector int completed on device - took " << (end - start).count()
            << " u-secs\n";
  return ((end - start).count());
}

在使用原子操作时,必须注意确保硬件支持并且能够有效执行。 在 Gen9 和 Intel® Iris® Xe 集成显卡上不支持浮点数或双精度数据类型上的原子操作, VectorDouble 的性能将非常差。

//
int VectorDouble(sycl::queue &q, int iter) {
  VectorAllocator<double> alloc;
  AlignedVector<double> a(array_size, alloc);
  AlignedVector<double> b(array_size, alloc);

  InitializeArray<double>(a);
  InitializeArray<double>(b);
  sycl::range num_items{a.size()};
  sycl::buffer a_buf(a);
  sycl::buffer b_buf(b);

  auto start = std::chrono::steady_clock::now();
  for (int i = 0; i < iter; i++) {
    q.submit([&](sycl::handler &h) {
      // InpuGt accessors
      sycl::accessor a_acc(a_buf, h, sycl::read_write);
      sycl::accessor b_acc(a_buf, h, sycl::read_only);

      h.parallel_for(num_items, [=](auto i) {
        auto v = sycl::atomic_ref<double, sycl::memory_order::relaxed,
                                  sycl::memory_scope::device,
                                  sycl::access::address_space::global_space>(
            a_acc[0]);
        v += b_acc[i];
      });
    });
  }
  q.wait();
  auto end = std::chrono::steady_clock::now();
  std::cout << "Vector Double completed on device - took "
            << (end - start).count() << " u-secs\n";
  return ((end - start).count());
}

通过使用 VTune Profiler 分析这些 kernel, 我们可以测出硬件是否支持原子操作的影响。 您可以看到 VectorInt kernel 比 VectorDouble 和 VectorFloat 快得多。

VTune 动态指令

VTune Profiler 动态指令的分析让我们看到当没有原子操作的硬件支持时,指令计数会发生巨大变化。

这是我们 VectorInt kernel 的汇编代码。

VTune 原子整型数

与 VectorDouble 的汇编代码相比,当我们执行 VectorDouble kernel 时,需要多执行 3300 万个 GPU 指令。

VTune 原子双精度数

Intel® Advisor 工具有推荐窗格栏,可提供有关如何提高 GPU kernel 性能的建议。

Advisor 推荐窗格

Intel® Advisor 提供的一项建议是“存在低效原子操作”。当硬件中不支持原子操作时,它们会由软件来模拟。 这可以被检测到, Intel® Advisor 会给出可能解决方案的建议。

Advisor 低效原子操作

全局和本地地址空间中的原子操作

标准 C++ 内存模型假定应用程序在具有单个地址空间的单个设备上执行。但是,这两个假设都不适用于 SYCL 应用程序: 应用程序的不同部分在不同设备上执行(即主机端和一个或多个加速器); 每个设备都有多个地址空间(即私有、本地和全局);并且每个设备的全局地址空间可能是不相交的(取决于 USM 的支持)。

在全局地址空间中使用原子操作时,同样要小心,因为全局更新比本地更新慢得多。

#include <CL/sycl.hpp>
#include <iostream>
int main() {
  constexpr int N = 256 * 256;
  constexpr int M = 512;
  int total = 0;
  int *a = static_cast<int *>(malloc(sizeof(int) * N));
  for (int i = 0; i < N; i++)
    a[i] = 1;
  sycl::queue q({sycl::property::queue::enable_profiling()});
  sycl::buffer<int> buf(&total, 1);
  sycl::buffer<int> bufa(a, N);
  auto e = q.submit([&](sycl::handler &h) {
    sycl::accessor acc(buf, h);
    sycl::accessor acc_a(bufa, h, sycl::read_only);
    h.parallel_for(sycl::nd_range<1>(N, M), [=](auto it) {
      auto i = it.get_global_id();
      sycl::atomic_ref<int, sycl::memory_order_relaxed,
                       sycl::memory_scope_device,
                       sycl::access::address_space::global_space>
          atomic_op(acc[0]);
      atomic_op += acc_a[i];
    });
  });
  sycl::host_accessor h_a(buf);
  std::cout << "Reduction Sum : " << h_a[0] << "\n";
  std::cout
      << "Kernel Execution Time of Global Atomics Ref: "
      << e.get_profiling_info<sycl::info::event_profiling::command_end>() -
             e.get_profiling_info<sycl::info::event_profiling::command_start>()
      << "\n";
  return 0;
}

您可以重构代码以使用本地内存空间,如下面的示例所示。

#include <CL/sycl.hpp>
#include <iostream>
int main() {
  constexpr int N = 256 * 256;
  constexpr int M = 512;
  constexpr int NUM_WG = N / M;
  int total = 0;
  int *a = static_cast<int *>(malloc(sizeof(int) * N));
  for (int i = 0; i < N; i++)
    a[i] = 1;
  sycl::queue q({sycl::property::queue::enable_profiling()});
  sycl::buffer<int> global(&total, 1);
  sycl::buffer<int> bufa(a, N);
  auto e1 = q.submit([&](sycl::handler &h) {
    sycl::accessor b(global, h);
    sycl::accessor acc_a(bufa, h, sycl::read_only);
    auto acc = sycl::local_accessor<int, 1>(NUM_WG, h);
    h.parallel_for(sycl::nd_range<1>(N, M), [=](auto it) {
      auto i = it.get_global_id(0);
      auto group_id = it.get_group(0);
      sycl::atomic_ref<int, sycl::memory_order_relaxed,
                       sycl::memory_scope_device,
                       sycl::access::address_space::local_space>
          atomic_op(acc[group_id]);
      sycl::atomic_ref<int, sycl::memory_order_relaxed,
                       sycl::memory_scope_device,
                       sycl::access::address_space::global_space>
          atomic_op_global(b[0]);
      atomic_op += acc_a[i];
      it.barrier(sycl::access::fence_space::local_space);
      if (it.get_local_id() == 0)
        atomic_op_global += acc[group_id];
    });
  });
  sycl::host_accessor h_global(global);
  std::cout << "Reduction Sum : " << h_global[0] << "\n";
  int total_time =
      (e1.get_profiling_info<sycl::info::event_profiling::command_end>() -
       e1.get_profiling_info<sycl::info::event_profiling::command_start>());
  std::cout << "Kernel Execution Time of Local Atomics : " << total_time
            << "\n";
  return 0;
}

基于 USM 数据的原子操作

在独立 GPU 上,

  • 不支持在主机端分配的 USM (sycl::malloc_host)上进行原子操作。

  • 不支持从主机端和设备端对共享 USM 位置(sycl::malloc_shared) 进行并行访问。

我们建议使用设备端分配的 USM(sycl::malloc_device)内存进行原子操作和使用具有原子操作的设备端算法。

上一章                                         主目录    上级目录                                                               下一章

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值