• 阅读cuda docs best practice


    cuda toolkit v11.8 docs, link:https://docs.nvidia.com/cuda/cuda-c-best-practices-guide/index.html


    preface

    assess评估 application

    异构计算

    application profile

    parallel it

    get started

    获得正确答案

    优化cuda applications

    perf metrics 性能指标

    timing

    bandwidth

    Bandwidth - the rate at which data can be transferred - is one of the most important gating factors for performance,表明了内存性能,是cuda优化最基础的门槛。
    bandwidth包括理论值和有效值,一般有效值比理论值要低,要使用有效值来作为优化目标

    effective

    单位是GB/s,把读的字节数和写的字节数求和再除个运算时间。
    例如

    theoretical

    // TODO

    mem opt 内存优化

    between host and device

    从带宽的理论峰值来看,device上的数据传输高达890gb/s,而host2device的理论峰值只有16gb/s(受限于pcie总线)。所以,即便尽可能缩减了host2device的数据传输,对kernel本身的计算性能没啥影响,但是这个要求也是门槛级别的。具体做法上,应该把数据的生命周期都尽可能放在device上,不要让他沾到host的边。host2device的开销太大了,尽可能把数据做batching,一次性传输。(原文中写到,即便这些数据在mem不连续,但是把他们放到连续的buffer,以batching的格式传输,到device再拆开也值得)。

    pinned mem

    获得高带宽要用page lock mem,在锁页mem上进行alloc有专门的API,但同时也不是可以随意尽情使用锁页mem,因为空间不大。我们又没法提前预知锁页mem大小,所以应该根据不同的执行参数去看跑出来的结果?

    使用计算来遮挡数据传输

    简单的说就是异步进行内存拷贝。
    异步拷贝需要使用pinned mem,而且还需要指定stream。stream指的是在device上执行指令的一个队列,不同的stream之间可以交错执行或者完全并行,并行地执行多个stream就可以实现用计算来掩盖数据拷贝。实现计算遮盖内存拷贝有两种方式:1)可以通过异步数据拷贝来遮盖host计算;2)用device计算遮盖host计算。
    具体例子:

    cudaMemcpyAsync(a_d, a_h, size, cudaMemcpyHostToDevice, 0);
    kernel<<<grid, block>>>(a_d);
    cpuFunction();
    

    异步内存拷贝使用了默认stream,即stream0,kernel的执行也同样使用默认stream。由于kernel需要使用拷贝好的数据,因此这里都使用默认stream,就不用进行kernel和内存拷贝的同步操作。
    因为异步内存拷贝和kernel执行都会立即将控制权返回给host,所以下面的cpufunction可以立即执行,并且这个计算被内存拷贝和kernel执行遮盖掉了。
    部分设备支持数据拷贝和device计算并行操作,也就是说用数据拷贝来遮盖device计算。(cudadeivceprop的asyncenginecount表示是否支持),进行这个操作还是需要pinned mem,此外要将数据拷贝和kernel放在不同的stream。

    并行执行kernel和数据拷贝

    cudaStreamCreate(&stream1);
    cudaStreamCreate(&stream2);
    cudaMemcpyAsync(a_d, a_h, size, cudaMemcpyHostToDevice, stream1);
    kernel<<<grid, block, 0, stream2>>>(otherData_d);
    

    当存在数据依赖时,可以通过顺序阶段并行来解决,顺序就是最愚蠢的方式了,主要是看下阶段性并行

    顺序地拷贝和执行

    cudaMemcpy(a_d, a_h, N*sizeof(float), dir);
    kernel<<<N/nThreads, nThreads>>>(a_d);
    

    阶段地并行拷贝和执行

    // TODO

    0拷贝

    统一内存寻址

    device 内存空间

    合并全局内存访问

    全局内存的读写由warp为单位进行,并且被尽可能少的transaction(事务)来完成。
    重点: 尽可能地使用合并访问
    不同具体设备的合并访问要求是不一样的,即和架构相关,要参考具体卡的架构说明

    不过对于算力6.0以上的设备来说,总结起来就是:warp所访问的地址宽度以32字节每个内存事务来划分。
    3.5/3.7/5.2算力设备的L1缓存可以手动开启,如果开启后,那么内存事务的宽度将提升为128字节。
    对于算力6.0及以上的设备来说,L1缓存是默认开启的,不过与上一条不同,在这些设备上,global的读写无论是否cache到L1缓存中,内存事务仍然是32字节。
    在ECC(error correcting code,错误检查,提高数据正确性,参考:https://stackoverflow.com/questions/23432834/cuda-ecc-performance-cost)开启的卡上,执行合并的内存访问更为重要,发散内存访问会带来更为严重的内存访问开销,尤其是往global写数据时。

    简单访问模式

    在32字节对齐的地址上,第k个线程访问第k个word(4B),比如是warp访问了连续的float数组。在下图中,4个连续的32B内存事务来提供这些内存访问操作。

    同时,如果warp中一旦有多个线程访问了相同的地址或者某些线程没有访问,虽然请求的地址不能填满4个内存事务,但是这4个内存事务仍然会进行读取。又或者是这个warp的线程不是像图中这样规矩访问的,比如是乱的,但仍然刚好填满对齐的4个内存事务,那么也仍然是这4个内存事务来完成访问操作(6.0算力及以上)。

    顺序但不对齐的访问

    如果访问的地址没有对齐32B这样的内存事务宽度,例如下图,那么就会请求5个内存事务。

    使用cudamalloc这样的api申请内存时,至少保证是256B对齐的,因此要针对这个特点,布局自己的线程块。

    非对齐内存访问的开销

    // TODO

    __global__ void offsetCopy(float *odata, float* idata, int offset)
    {
        int xid = blockIdx.x * blockDim.x + threadIdx.x + offset;
        odata[xid] = idata[xid];
    }
    


    当offset=0,8,...时,带宽能达到相同的水平。例如offset=8,一个warp访问的地址空间是[0+8],[1+8],...,[32+8], 即32B, 36B, ..., 160B,共128B, 仍然是4个内存事务(每个32B)。4个内存事务的关键是8offset,在读float这种4B时,可以把地址对齐到32B,所以可以正好对齐内存事务。
    其他offset时,是5个内存事务,所以耗时提高,带宽下降。

    固定步长的访问
  • 相关阅读:
    Hadoop Ambari 安装
    hadoop 集群配置--增加减少新的机器不重启
    使用 XMPP 构建一个基于 web 的通知工具——转
    Hadoop 1.1.2 Eclipse 插件使用——异常解决
    UltraEdit中文乱码的解决方法
    Hadoop开发环境简介(转)
    Hadoop构成
    hadoop 1.2.1 eclipse 插件编译
    Python print 输出到控制台 丢数据
    社招面试总结
  • 原文地址:https://www.cnblogs.com/ijpq/p/16825905.html
Copyright © 2020-2023  润新知