【问题标题】:CUDA kernel with single branch runs 1.5x faster than kernel without branch具有单分支的 CUDA 内核比没有分支的内核运行速度快 1.5 倍
【发布时间】:2018-03-02 03:19:04
【问题描述】:

我在过滤器内核上有一个奇怪的性能倒置,不管有没有分支。带分支的内核比不带分支的内核运行速度快约 1.5 倍。

基本上我需要对一堆辐射射线进行排序,然后应用交互内核。由于附带的数据很多,所以不能多次使用thrust::sort_by_key()之类的东西。

算法思路:

  1. 为所有可能的交互类型(共五种)运行循环
  2. 在每个循环中,warp 线程都会为其交互类型投票
  3. 循环完成后,每个 warp 线程都知道另一个具有相同交互类型的线程
  4. 线程选举他们的领导者(每个交互类型)
  5. Leader 使用 atomicAdd 更新交互偏移表
  6. 每个线程将其数据写入相应的偏移量

我使用了这篇 Nvidia 帖子https://devblogs.nvidia.com/parallelforall/cuda-pro-tip-optimized-filtering-warp-aggregated-atomics/中描述的技术

我的第一个内核在循环中包含一个分支,运行时间约为 5 毫秒:

int active;
int leader;
int warp_progress;
for (int i = 0; i != hit_interaction_count; ++i)
{
  if (i == decision)
  {
    active = __ballot(1);
    leader = __ffs(active) - 1;
    warp_progress = __popc(active);
  }
}

我的第二个内核使用两个元素的查找表,不使用分支并运行约 8 毫秒:

int active = 0;
for (int i = 0; i != hit_interaction_count; ++i)
{
  const int masks[2] = { 0, ~0 };
  int mask = masks[i == decision];
  active |= (mask & __ballot(mask));
}
int leader = __ffs(active) - 1;
int warp_progress = __popc(active);

公共部分:

int warp_offset;
if (lane_id() == leader)
  warp_offset = atomicAdd(&interactions_offsets[decision], warp_progress);
warp_offset = warp_broadcast(warp_offset, leader);
...copy data here...

怎么可能?有没有办法实现这样的过滤器内核,使其运行速度比分支更快?

UPD:完整的源代码可以在 filter_kernel cuda_equation/radiance_cuda.cu https://bitbucket.org/radiosity/engine/src

找到

【问题讨论】:

  • 在这两个版本中,您都有一个 for 循环,该循环运行 hit_interaction_count 次。但是在您的“分支版本”中,您实际上只是一次执行这三行,而在您的第二个内核中,您在所有运行中执行所有三行。为什么第二个版本应该更快?当然,一个简单的if 检查比你的第二个版本中的三行更快?另外,我希望您的 for-if 构造优化为一个简单的 if 块,并检查 decision < hit_interaction_count 是否成立。
  • 这是一个线程扭曲。所以不管是否在if 中执行其他线程代码,它们都会被停止。 if 块无法优化,因为 warp 中的每个线程都有自己的 decision 值。
  • 我相信这样的问题应该伴随着minimal reproducible example 这两种情况。就目前而言,您的实际比较内核是什么并不明显(因为“公共部分”不清楚并且没有显示一些代码)并且没有人可以运行它来查看差异,甚至编译这两​​种情况来比较机器代码。

标签: performance cuda gpu-warp


【解决方案1】:

我认为这是CPU程序员大脑变形。在 CPU 上,由于消除了分支和分支预测错误,我预计性能会有所提升。

但在 GPU 上没有分支预测,也没有惩罚,所以只有指令计数很重要。

首先我需要将代码重写为简单的代码。

有分支:

int active;
for (int i = 0; i != hit_interaction_count; ++i)
    if (i == decision)
        active = __ballot(1);

没有分支:

int active = 0;
for (int i = 0; i != hit_interaction_count; ++i)
{
  int mask = 0 - (i == decision);
  active |= (mask & __ballot(mask));
}

在第一个版本中,有 ~3 个操作:compareif__ballot()。 在第二个版本中,有大约 5 个操作:comparemake mask__ballot()&|=。 公共代码中大约有 15 个操作。

两个循环都运行 5 个周期。首先是 35 个操作,其次是 45 个操作。这种计算可以解释性能下降。

【讨论】:

  • 我还是不明白你为什么不把你的“分支版本”简化为一个简单的if 语句,比如if (decision < hit_interaction_count) active = ...。也许添加另一个条件来检查decision 是否为>= 0。或者直接使用__ballot(1) 初始化变量,因为active 变量缺少默认初始化似乎表明它总是会在稍后在for 循环中初始化。
  • 每条光线(经线中的线)都有自己的随机决定:吸收、反射、漫射或透射。并且线程不知道另一个线程决定。我正在使用__ballot() 来计算每种类型的决策计数,并在每个扭曲中执行一次atomicAdd()。对于具有特定决定的单线程,只有一个__ballot() 很重要。但是还有另一个线程(可能是)不同的决定。这就是为什么我需要多次__ballot()
猜你喜欢
  • 2020-11-23
  • 2014-04-06
  • 2015-04-22
  • 1970-01-01
  • 2012-04-12
  • 2021-11-29
  • 1970-01-01
  • 2012-04-06
  • 1970-01-01
相关资源
最近更新 更多