如何在CUDA中使用原子负载

如何在CUDA中使用原子负载,cuda,gpu-atomics,Cuda,Gpu Atomics,我的问题是如何在CUDA中实现原子负载。原子交换可以模拟原子存储。原子负载能否以类似的方式进行无成本模拟? 我可以使用带有0的原子add以原子方式加载内容,但我认为这很昂贵,因为它执行原子读-修改-写操作,而不仅仅是读操作 据我所知,目前没有办法在CUDA中请求原子负载,这将是一个很好的功能 有两种准备选方案,各有优缺点: 按照您的建议使用无操作原子读修改写。我过去曾提供过一份报告。保证原子性和内存一致性,但您需要支付不必要的写入成本 实际上,第二个最接近原子负载的东西可能是标记一个变量vola

我的问题是如何在CUDA中实现原子负载。原子交换可以模拟原子存储。原子负载能否以类似的方式进行无成本模拟?
我可以使用带有0的原子add以原子方式加载内容,但我认为这很昂贵,因为它执行原子读-修改-写操作,而不仅仅是读操作

据我所知,目前没有办法在CUDA中请求原子负载,这将是一个很好的功能

有两种准备选方案,各有优缺点:

  • 按照您的建议使用无操作原子读修改写。我过去曾提供过一份报告。保证原子性和内存一致性,但您需要支付不必要的写入成本

  • 实际上,第二个最接近原子负载的东西可能是标记一个变量
    volatile
    ,尽管严格来说语义是完全不同的。该语言并不保证加载的原子性(例如,理论上,您可能会得到一个不完整的读取),但可以保证获得最新的值。但实际上,正如@Robert Crovella的评论所指出的,不可能对最多32个字节的正确对齐的事务进行撕碎读取,这确实使它们具有原子性


  • 解决方案2有点老套,我不推荐它,但它是目前唯一一个比1少写的替代方案。理想的解决方案是添加一种直接用语言表示原子负载的方法。

    除了按照另一个答案中的建议使用
    volatile
    ,还需要适当地使用
    \u threadfence
    以获得具有安全内存顺序的原子负载

    虽然一些评论说只使用普通的读取,因为它不能撕裂,但这与原子负载不同。原子学不仅仅是撕裂:

    正常读取可能会重用寄存器中已有的先前加载,因此可能不会反映其他SMs对所需内存顺序所做的更改。例如,
    int*flag=。。。;而(*flag){…}
    只能读取
    flag
    一次,并在循环的每次迭代中重用此值。如果您正在等待另一个线程更改标志的值,您将永远无法观察到该更改。
    volatile
    修饰符确保每次访问时都从内存中读取该值。有关更多信息,请参阅

    此外,您还需要使用内存围栏在调用线程中强制执行正确的内存顺序。如果没有围栏,用C++11的说法,您会得到“放松”的语义,而在使用原子进行通信时,这可能是不安全的

    例如,假设您的代码(非原子地)将一些大数据写入内存,然后使用正常写入来设置原子标志,以指示数据已被写入。在设置标志之前,指令可能会被重新排序,硬件缓存线可能不会被刷新,等等。结果是这些操作不能保证以任何顺序执行,其他线程可能不会按照您期望的顺序观察这些事件:在写入受保护的数据之前,允许写入标志

    同时,如果读取线程在有条件地加载数据之前也在使用普通读取来检查标志,那么在硬件级别将出现竞争。无序和/或推测性执行可能会在标记读取完成之前加载数据。然后使用推测加载的数据,该数据可能无效,因为它是在读取标志之前加载的

    放置良好的内存围栏通过强制执行指令重新排序不会影响所需的内存顺序,并且使以前的写入对其他线程可见,从而防止了此类问题<代码>\uu threadfence()和朋友也包括在内

    综上所述,在CUDA中编写自己的原子加载方法看起来像:

    // addr must be aligned properly.
    __device__ unsigned int atomicLoad(const unsigned int *addr)
    {
      const volatile unsigned int *vaddr = addr; // volatile to bypass cache
      __threadfence(); // for seq_cst loads. Remove for acquire semantics.
      const unsigned int value = *vaddr;
      // fence to ensure that dependent reads are correctly ordered
      __threadfence(); 
      return value; 
    }
    
    // addr must be aligned properly.
    __device__ void atomicStore(unsigned int *addr, unsigned int value)
    {
      volatile unsigned int *vaddr = addr; // volatile to bypass cache
      // fence to ensure that previous non-atomic stores are visible to other threads
      __threadfence(); 
      *vaddr = value;
    }
    
    对于其他非撕裂加载/存储尺寸,也可以采用类似的方式编写


    通过与一些致力于CUDA atomics的NVIDIA开发人员的交谈,我们似乎应该开始看到CUDA中对atomics的更好支持,并且PTX已经包含了语义——但如果不借助内联PTX,目前无法访问它们。他们希望在今年的某个时候加入他们。一旦这些都准备好了,一个完整的
    std::atomic
    实现应该不会落后太多了。

    那么您想要一个阻塞负载吗?这听起来像你需要滚动自己的互斥。更具体地说,我想要一些像原子负载和存储在C++中,我真的不理解这个问题。每个线程最多128位的适当加载是“原子的”,即加载的任何部分都不会被“干预”(加载或)存储修改。商店本身也同样保证是原子的。原子函数的目的是提供不间断的RMW功能。我不确定
    volatile
    限定符是否有助于负载的原子性。我认为并考虑缓存中存在的值。它还会使加载操作不被线程破坏吗?@Farzad
    volatile
    确实对原子性没有任何帮助,因此,如果OP希望得到保证,为什么他们应该使用no-OP RMW。对于小于或等于本机字长的任何内容,都不能进行写或读操作,因此对于32位类型,这些操作不会发生。对于64位,是的,这是可能的。我不建议使用volatile,但OP表示他们不想为额外的原子写入付费。我将在中编辑此内容。64位类型的正确对齐加载不能被“插入”写入“撕裂”或部分修改。我认为整个问题都很愚蠢。所有内存事务都是针对二级缓存执行的。二级缓存仅提供32字节缓存线。没有其他可能的交易。正确对齐的64位类型将始终属于一个二级缓存线,并且该缓存线的服务