oneAPI GPU 优化指南 - 共享本地内存(SLM)

本章节翻译by chenshusmail@163.com 原文:Shared Local Memory (intel.com)

目录

SLM 的大小和 work-group 的大小 

存储体(bank)冲突

数据共享和 work-group 的 barrier

使用 SLM 作为缓存

排查 SLM 错误


通常,work-item 需要共享数据并相互通信。一方面,所有 work-group 中的所有 work-item 都可以访问全局内存, 因此数据共享和通信可以通过全局内存进行。但是,由于其较低的带宽和较高的延迟, 通过全局内存进行共享和通信效率较低。另一方面, 在矢量引擎(VE)线程中同时执行的 sub-group 中的 work-item 可以非常高效地共享数据并相互通信, 但 sub-group 中的 work-item 数量通常较小,数据共享和通信的范围非常有限。 具有更高带宽和更低延迟的内存,可供更大范围的 work-item 访问,对于 work-item 之间的数据共享通信非常合适。 Intel® GPU 中的共享本地内存(SLM)就是为此而设计。

Intel GPU 的每个 Xe-core 都有自己的 SLM。对 SLM 的访问仅限于 Xe-core 中的 VE 或计划在同一 Xe-core 的 VE 上执行的同一 work-group 中的 work-item。它局限于一个 Xe-core(或 work-group), 并由同一 Xe-core(或同一 work-group 中的 work-item)中的 VE 共享,因此称为 SLM。 由于它位于每个 Xe-core 芯片上,因此 SLM 比全局内存具有更高的带宽和更低的延迟。 由于它可供一个 work-group 中所有 work-item 访问,因此 SLM 可以容纳数百个 work-item 之间的数据共享和通信, 具体取决于 work-group 大小。

将 SLM 视为由 work-group 管理的缓存通常很有帮助。当一个 work-group 启动时, 该 work-group 中的 work-item 可以显式地将数据从全局内存加载到 SLM 中。 在整个 work-group 生命周期内,数据将保留在 SLM 中以便更快地访问。在 work-group 完成之前, SLM 中的数据可以由 work-item 显式写回全局内存。在 work-group 完成执行后,SLM 中的数据也消失并且无效。 SLM 与全局内存之间的数据一致性是程序员负责处理。正确地使用 SLM 可以产生显著的性能差异。

SLM 的大小和 work-group 的大小 

由于它在芯片器件上,SLM 的大小有限。一个 work-group 可用的内存量取决于设备,并且可以通过查询设备获得,例如:

  std::cout << "Local Memory Size: "
            << q.get_device().get_info<sycl::info::device::local_mem_size>()
            << std::endl;

输出可能是这样:

Local Memory Size: 65536

输出数值的单位是字节。因此,这个 GPU 设备每个 work-group 有65,536字节或 64KB 的SLM。

了解一个 work-group 可以拥有的最大 SLM 大小非常重要。在许多情况下, 可供一个 work-group 使用的 SLM 总大小和 work-group 中 work-item 数量是非常数的函数关系。 最大 SLM 大小可以限制 work-group 中 work-item 的总数,即 work-group 大小。例如, 如果最大 SLM 大小为 64KB,每个 work-item 需要512字节的 SLM,则最大 work-group 大小不能超过128。

存储体(bank)冲突

SLM 被划分为大小相等的内存 bank,可以同时被访问以获得高带宽。bank 总数取决于设备。在撰写本文时, 64个连续字节以4字节(32位)粒度存储在16个连续 bank 中。对不同 bank 的访问请求可以并行处理, 但对同一 bank 中不同地址的请求会导致 bank 冲突并被序列化。bank 冲突会对性能产生不利影响。 参考这个例子:

  constexpr int N = 32;
  int *data = sycl::malloc_shared<int>(N, q);

  auto e = q.submit([&](auto &h) {
    sycl::local_accessor<int, 1> slm(sycl::range(32 * 64), h);
    h.parallel_for(sycl::nd_range(sycl::range{N}, sycl::range{32}),
                   [=](sycl::nd_item<1> it) {
                     int i = it.get_global_linear_id();
                     int j = it.get_local_linear_id();

                     slm[j * 16] = 0;
                     it.barrier(sycl::access::fence_space::local_space);

                     for (int m = 0; m < 1024 * 1024; m++) {
                       slm[j * 16] += i * m;
                       it.barrier(sycl::access::fence_space::local_space);
                     }

                     data[i] = slm[j * 16];
                   });
  });

如果 bank 数量为 16,则上述示例中的所有 work-item 将从同一 bank 的不同地址读取并写入。内存带宽是全带宽的1/16。

下一个示例没有 SLM bank 冲突并实现了全内存带宽,因为每个 work-item 都从不同 bank 的不同地址读取并写入。

  constexpr int N = 32;
  int *data = sycl::malloc_shared<int>(N, q);

  auto e = q.submit([&](auto &h) {
    sycl::local_accessor<int, 1> slm(sycl::range(32 * 64), h);
    h.parallel_for(sycl::nd_range(sycl::range{N}, sycl::range{32}),
                   [=](sycl::nd_item<1> it) {
                     int i = it.get_global_linear_id();
                     int j = it.get_local_linear_id();

                     slm[j] = 0;
                     it.barrier(sycl::access::fence_space::local_space);

                     for (int m = 0; m < 1024 * 1024; m++) {
                       slm[j] += i * m;
                       it.barrier(sycl::access::fence_space::local_space);
                     }

                     data[i] = slm[j];
                   });
  });

数据共享和 work-group 的 barrier

让我们回顾一下 寄存器化和避免寄存器溢出 章节中256个 bin 的直方图示例。

  constexpr int BLOCK_SIZE = 256;
  constexpr int NUM_BINS = 256;

  std::vector<unsigned long> hist(NUM_BINS, 0);

  sycl::buffer<unsigned long, 1> mbuf(input.data(), N);
  sycl::buffer<unsigned long, 1> hbuf(hist.data(), NUM_BINS);

  auto e = q.submit([&](auto &h) {
    sycl::accessor macc(mbuf, h, sycl::read_only);
    auto hacc = hbuf.get_access<sycl::access::mode::atomic>(h);
    h.parallel_for(
        sycl::nd_range(sycl::range{N / BLOCK_SIZE}, sycl::range{64}),
        [=](sycl::nd_item<1> it) [[intel::reqd_sub_group_size(16)]] {
          int group = it.get_group()[0];
          int gSize = it.get_local_range()[0];
          auto sg = it.get_sub_group();
          int sgSize = sg.get_local_range()[0];
          int sgGroup = sg.get_group_id()[0];

          unsigned int
              histogram[NUM_BINS / 16]; // 直方图 bin 占用太多存储空间,无法提升到寄存器中

          for (int k = 0; k < NUM_BINS / 16; k++) {
            histogram[k] = 0;
          }
          for (int k = 0; k < BLOCK_SIZE; k++) {
            unsigned long x =
                sg.load(macc.get_pointer() + group * gSize * BLOCK_SIZE +
                        sgGroup * sgSize * BLOCK_SIZE + sgSize * k);
// sub-group 的大小是 16
#pragma unroll
            for (int j = 0; j < 16; j++) {
              unsigned long y = sycl::group_broadcast(sg, x, j);
#pragma unroll
              for (int i = 0; i < 8; i++) {
                unsigned int c = y & 0xFF;
                // (c & 0xF) is the workitem in which the bin resides
                // (c >> 4) is the bin index
                if (sg.get_local_id()[0] == (c & 0xF)) {
                  histogram[c >> 4] += 1;
                }
                y = y >> 8;
              }
            }
          }

          for (int k = 0; k < NUM_BINS / 16; k++) {
            hacc[16 * k + sg.get_local_id()[0]].fetch_add(histogram[k]);
          }
        });
  });

这个示例已经被优化为使用 int 数据类型而不是 long,并在 sub-group 中共享寄存器, 以便 private 直方图的 bin 可以放入寄存器以获得最佳性能。 如果您需要更大的 bin 大小(例如 1024),则 private 直方图的 bin 将不可避免地溢出到全局内存。

只要每个 bin 都以原子方式更新,直方图的 bin 就可以被同一个 work-group 中的 work-item 共享。

  constexpr int NUM_BINS = 1024;
  constexpr int BLOCK_SIZE = 256;

  std::vector<unsigned long> hist(NUM_BINS, 0);
  sycl::buffer<unsigned long, 1> mbuf(input.data(), N);
  sycl::buffer<unsigned long, 1> hbuf(hist.data(), NUM_BINS);

  auto e = q.submit([&](auto &h) {
    sycl::accessor macc(mbuf, h, sycl::read_only);
    sycl::accessor hacc(hbuf, h, sycl::read_write);
    sycl::local_accessor<unsigned int, 1> local_histogram(sycl::range(NUM_BINS),
                                                          h);
    h.parallel_for(
        sycl::nd_range(sycl::range{N / BLOCK_SIZE}, sycl::range{64}),
        [=](sycl::nd_item<1> it) {
          int group = it.get_group()[0];
          int gSize = it.get_local_range()[0];
          auto sg = it.get_sub_group();
          int sgSize = sg.get_local_range()[0];
          int sgGroup = sg.get_group_id()[0];

          int factor = NUM_BINS / gSize;
          int local_id = it.get_local_id()[0];
          if ((factor <= 1) && (local_id < NUM_BINS)) {
            sycl::atomic_ref<unsigned int, sycl::memory_order::relaxed,
                             sycl::memory_scope::device,
                             sycl::access::address_space::local_space>
                local_bin(local_histogram[local_id]);
            local_bin.store(0);
          } else {
            for (int k = 0; k < factor; k++) {
              sycl::atomic_ref<unsigned int, sycl::memory_order::relaxed,
                               sycl::memory_scope::device,
                               sycl::access::address_space::local_space>
                  local_bin(local_histogram[gSize * k + local_id]);
              local_bin.store(0);
            }
          }
          it.barrier(sycl::access::fence_space::local_space);

          for (int k = 0; k < BLOCK_SIZE; k++) {
            unsigned long x =
                sg.load(macc.get_pointer() + group * gSize * BLOCK_SIZE +
                        sgGroup * sgSize * BLOCK_SIZE + sgSize * k);
#pragma unroll
            for (std::uint8_t shift : {0, 16, 32, 48}) {
              constexpr unsigned long mask = 0x3FFU;
              sycl::atomic_ref<unsigned int, sycl::memory_order::relaxed,
                               sycl::memory_scope::device,
                               sycl::access::address_space::local_space>
                  local_bin(local_histogram[(x >> shift) & mask]);
              local_bin += 1;
            }
          }
          it.barrier(sycl::access::fence_space::local_space);

          if ((factor <= 1) && (local_id < NUM_BINS)) {
            sycl::atomic_ref<unsigned int, sycl::memory_order::relaxed,
                             sycl::memory_scope::device,
                             sycl::access::address_space::local_space>
                local_bin(local_histogram[local_id]);
            sycl::atomic_ref<unsigned long, sycl::memory_order::relaxed,
                             sycl::memory_scope::device,
                             sycl::access::address_space::global_space>
                global_bin(hacc[local_id]);
            global_bin += local_bin.load();
          } else {
            for (int k = 0; k < factor; k++) {
              sycl::atomic_ref<unsigned int, sycl::memory_order::relaxed,
                               sycl::memory_scope::device,
                               sycl::access::address_space::local_space>
                  local_bin(local_histogram[gSize * k + local_id]);
              sycl::atomic_ref<unsigned long, sycl::memory_order::relaxed,
                               sycl::memory_scope::device,
                               sycl::access::address_space::global_space>
                  global_bin(hacc[gSize * k + local_id]);
              global_bin += local_bin.load();
            }
          }
        });
  });

当启动 work-group 时,work-group 中的每个 work-item 都会将 SLM 中的直方图 bin 中的一部分初始化为 0 (上述示例中第 24-38 行的代码)。您可以指定一个 work-item 来初始化所有直方图的 bin, 但通常将这项任务分配给 work-group 中的所有 work-item 会更高效。

第 39 行初始化后的 work-group barrier 保证了所有直方图 bin 在任何 work-item 更新任何 bin 之前都被初始化为 0。

由于 SLM 中的直方图 bin 是被所有 work-item 共享的,因此任何 work-item 对任何 bin 的更新都必须是原子 (atomic) 操作。

一旦完成了 work-group 中的本地直方图,就会更新全局直方图。但是,在读取本地 SLM bin 以更新全局 bin 之前, 再次在第 43 行调用 work-group barrier,以确保所有 work-item 都完成了它们的工作。

当共享 SLM 数据时,通常需要使用 work-group barrier 进行 work-item 同步。barrier 有耗时,并且耗时可能会随着 work-group 大小的增加而增加。 一种好方法是尝试不同的 work-group 大小以找到最适合您应用程序的大小。

您可以在示例文件夹中找到一个带有 256 个 bin 的直方图的 SLM 版本示例。您可以将其性能与使用寄存器版本的性能进行比较。 您可能会得到一些令人惊讶的结果,并思考如何进行进一步优化。

使用 SLM 作为缓存

有时您可能会发现让应用程序自己来管理一些热数据的缓存,比让硬件自动执行的性能更为理想。 通过应用程序直接管理数据缓存,每当需要数据时,您都确切地知道数据在哪里以及访问它的成本。SLM 可以被用于此目的。

参看以下 1-D 卷积示例:

    sycl::buffer<int> ibuf(input.data(), N);
    sycl::buffer<int> obuf(output.data(), N);
    sycl::buffer<int> kbuf(kernel.data(), M);

    auto e = q.submit([&](auto &h) {
      sycl::accessor iacc(ibuf, h, sycl::read_only);
      sycl::accessor oacc(obuf, h);
      sycl::accessor kacc(kbuf, h, sycl::read_only);

      h.parallel_for(sycl::nd_range<1>(sycl::range{N}, sycl::range{256}),
                     [=](sycl::nd_item<1> it) {
                       int i = it.get_global_linear_id();
                       int group = it.get_group()[0];
                       int gSize = it.get_local_range()[0];

                       int t = 0;
                       int _M = static_cast<int>(M);
                       int _N = static_cast<int>(N);

                       if ((group == 0) || (group == _N / gSize - 1)) {
                         if (i < _M / 2) {
                           for (int j = _M / 2 - i, k = 0; j < _M; ++j, ++k) {
                             t += iacc[k] * kacc[j];
                           }
                         } else {
                           if (i + _M / 2 >= _N) {
                             for (int j = 0, k = i - _M / 2;
                                  j < _M / 2 + _N - i; ++j, ++k) {
                               t += iacc[k] * kacc[j];
                             }
                           } else {
                             for (int j = 0, k = i - _M / 2; j < _M; ++j, ++k) {
                               t += iacc[k] * kacc[j];
                             }
                           }
                         }
                       } else {
                         for (int j = 0, k = i - _M / 2; j < _M; ++j, ++k) {
                           t += iacc[k] * kacc[j];
                         }
                       }

                       oacc[i] = t;
                     });
    });

该示例使用 257 个元素的 kernel 数组对 8192 x 8192 个元素的整数数组进行卷积,并将结果写入输出数组。 每个 work-item 卷积一个元素。但是,要卷积一个元素,需要多达 256 个相邻元素的值。

注意到,每个输入元素都被多个 work-item 使用,您可以将整个 work-group 所需的所有输入元素预加载到 SLM 中。 之后,当需要某一个元素时,它可以被从 SLM 而不是全局内存中加载。

    sycl::buffer<int> ibuf(input.data(), N);
    sycl::buffer<int> obuf(output.data(), N);
    sycl::buffer<int> kbuf(kernel.data(), M);

    auto e = q.submit([&](auto &h) {
      sycl::accessor iacc(ibuf, h, sycl::read_only);
      sycl::accessor oacc(obuf, h);
      sycl::accessor kacc(kbuf, h, sycl::read_only);
      sycl::local_accessor<int, 1> ciacc(sycl::range(256 + (M / 2) * 2), h);

      h.parallel_for(
          sycl::nd_range(sycl::range{N}, sycl::range{256}),
          [=](sycl::nd_item<1> it) {
            int i = it.get_global_linear_id();
            int group = it.get_group()[0];
            int gSize = it.get_local_range()[0];
            int local_id = it.get_local_id()[0];
            int _M = static_cast<int>(M);

            ciacc[local_id + M / 2] = iacc[i];

            if (local_id == 0) {
              if (group == 0) {
                for (int j = 0; j < _M / 2; ++j) {
                  ciacc[j] = 0;
                }
              } else {
                for (int j = 0, k = i - _M / 2; j < _M / 2; ++j, ++k) {
                  ciacc[j] = iacc[k];
                }
              }
            }
            if (local_id == gSize - 1) {
              if (group == static_cast<int>(it.get_group_range()[0]) - 1) {
                for (int j = gSize + _M / 2; j < gSize + _M / 2 + _M / 2; ++j) {
                  ciacc[j] = 0;
                }
              } else {
                for (int j = gSize + _M / 2, k = i + 1;
                     j < gSize + _M / 2 + _M / 2; ++j, ++k) {
                  ciacc[j] = iacc[k];
                }
              }
            }

            it.barrier(sycl::access::fence_space::local_space);

            int t = 0;
            for (int j = 0, k = local_id; j < _M; ++j, ++k) {
              t += ciacc[k] * kacc[j];
            }

            oacc[i] = t;
          });
    });

当 work-group 启动时,每个 work-item 所需的所有输入元素都被加载到 SLM 中。除第一个和最后一个 work-item 外, 每个 work-item 都将一个元素加载到 SLM 中。第一个 work-item 将第一个元素左侧的邻居加载到 SLM 中, 最后一个 work-item 将最后一个元素右侧的邻居加载到 SLM 中。如果不存在邻居,则在 SLM 中将该元素填充为0。

在每个 work-item 开始卷积之前,调用 local barrier 以确保所有输入元素都已加载到 SLM 中。

每个 work-item 中的卷积都可以畅通无阻。所有相邻元素都从更快的 SLM 而不是全局内存中加载。

排查 SLM 错误

当 kernel 使用的 __private 或者 __local 内存超过模拟器默认支持的数量时,可能会发生 CL_OUT_OF_RESOURCES 错误。发生这种情况时,您会看到类似于以下内容的错误信息:

$  ./myapp
:
Problem size: c(150,600) = a(150,300) * b(300,600)
terminate called after throwing an instance of
  'cl::sycl::runtime_error'
  what():  Native API failed. Native API returns:
  -5 (CL_OUT_OF_RESOURCES) -5 (CL_OUT_OF_RESOURCES)
Aborted (core dumped)
$

.或者如果使用的是 onetrace:

$ onetrace -c ./myapp
:
>>>> [6254070891] zeKernelSuggestGroupSize: hKernel = 0x263b7a0 globalSizeX = 163850 globalSizeY = 1 globalSizeZ = 1 groupSizeX = 0x7fff94e239f0 groupSizeY = 0x7fff94e239f4 groupSizeZ = 0x7fff94e239f8
<<<< [6254082074] zeKernelSuggestGroupSize [922 ns] -> ZE_RESULT_ERROR_OUT_OF_DEVICE_MEMORY(0x1879048195)
terminate called after throwing an instance of 'cl::sycl::runtime_error'
  what():  Native API failed. Native API returns: -5 (CL_OUT_OF_RESOURCES) -5 (CL_OUT_OF_RESOURCES)
Aborted (core dumped)
$

要查看复制到 SLM 的内存量以及实际硬件限制,请设置 debug 环境变量:

export PrintDebugMessages=1
export NEOReadDebugKeys=1

输出将被更改为:

$ ./myapp
:
Size of SLM (656384) larger than available (131072)
terminate called after throwing an instance of 'cl::sycl::runtime_error'
  what():  Native API failed. Native API returns: -5 (CL_OUT_OF_RESOURCES) -5 (CL_OUT_OF_RESOURCES)
Aborted (core dumped)
$

或者如果使用的是 onetrace:

$ onetrace -c ./myapp
:
>>>> [317651739] zeKernelSuggestGroupSize: hKernel = 0x2175ae0 globalSizeX = 163850 globalSizeY = 1 globalSizeZ = 1 groupSizeX = 0x7ffd9caf0950 groupSizeY = 0x7ffd9caf0954 groupSizeZ = 0x7ffd9caf0958
Size of SLM (656384) larger than available (131072)
<<<< [317672417] zeKernelSuggestGroupSize [10325 ns] -> ZE_RESULT_ERROR_OUT_OF_DEVICE_MEMORY(0x1879048195)
terminate called after throwing an instance of 'cl::sycl::runtime_error'
  what():  Native API failed. Native API returns: -5 (CL_OUT_OF_RESOURCES) -5 (CL_OUT_OF_RESOURCES)
Aborted (core dumped)
$

确定所需 SLM 大小后,尝试为 CL_CONFIG_CPU_FORCE_PRIVATE_MEM_SIZE 或者 CL_CONFIG_CPU_FORCE_LOCAL_MEM_SIZE 环境变量设置更大的值, 如 oneAPI 编程指南中的 Emulator Environment Variables 章节所述。

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值