【问题标题】:Memory copy by two CUDA kernels - why speed differs?两个 CUDA 内核的内存复制 - 为什么速度不同?
【发布时间】:2018-10-04 15:42:28
【问题描述】:

谁能帮助我了解 memCopy2dA 和 memCopy2dB 内核之间的性能差异?

他们应该将大小为 xLen,yLen 的 2D 数据从一个地方复制到另一个地方,但他们使用不同的策略:

  • 当使用 memCopy2dA 时,块/线程覆盖整个 2D 空间,因为该内核假设只复制一个数据点

  • 当使用 memCopy2dB 时,仅为一整 X 行创建块/线程,然后每个内核在 Y 方向上循环以复制所有数据。

根据分析器 (nvvp) 在这两种情况下,GPU 访问内存模式为 100%,并且 X 维度大到足以使“B”内核(Titan X,24SM)的设备饱和。不幸的是,“B”内核速度较慢,在我的机器上结果是:

GB/s: 270.715
GB/s: 224.405

附加问题:是否有可能接近 336.48 GB/s (3505MHz * 384 bits * 2 / 8) 的理论内存带宽限制?至少我的测试显示最大值始终在 271-272 GB/s 左右。

测试代码:

#include <cuda_runtime.h>
#include <device_launch_parameters.h>
#include <iostream>
#include <chrono>

template<typename T>
__global__ void memCopy2dA(T *in, T *out, size_t xLen, size_t yLen) {
    int xi = blockIdx.x * blockDim.x + threadIdx.x;
    int yi = blockIdx.y * blockDim.y + threadIdx.y;
    if (xi < xLen && yi < yLen) {
        out[yi * xLen + xi] = in[yi * xLen + xi];
    }
}

template<typename T>
__global__ void memCopy2dB(T *in, T *out, size_t xLen, size_t yLen) {
    int xi = blockIdx.x * blockDim.x + threadIdx.x;
    if (xi < xLen) {
        size_t idx = xi;
        for (int y = 0; y < yLen; ++y) {
            out[idx] = in[idx];
            idx += xLen;
        }
    }
}

static void waitForCuda() {
    cudaDeviceSynchronize();
    cudaError_t err = cudaGetLastError();
    if (err != cudaSuccess) printf("Error: %s\n", cudaGetErrorString(err));
}

int main() {
    typedef float T;

    size_t xLen = 24 * 32 * 64; //49152
    size_t yLen = 1024;
    size_t dataSize = xLen * yLen * sizeof(T);

    T *dInput;
    cudaMalloc(&dInput, dataSize);
    T *dOutput;
    cudaMalloc(&dOutput, dataSize);

    const int numOfRepetitions = 100;
    double gigabyte = 1000 * 1000 * 1000;
    {
        dim3 threadsPerBlock(64, 1);
        dim3 numBlocks((xLen + threadsPerBlock.x - 1) / threadsPerBlock.x,
                       (yLen + threadsPerBlock.y - 1) / threadsPerBlock.y);

        auto startTime = std::chrono::high_resolution_clock::now();
        for (int i = 0; i < numOfRepetitions; ++i) {
            memCopy2dA <<< numBlocks, threadsPerBlock >>> (dInput, dOutput, xLen, yLen);
            waitForCuda();
        }
        auto stopTime = std::chrono::high_resolution_clock::now();
        std::chrono::duration<double> elapsed = stopTime - startTime;
        std::cout << "GB/s: " << (2 * dataSize * numOfRepetitions) / elapsed.count() / gigabyte << std::endl;
    }
    {
        dim3 threadsPerBlock(64);
        dim3 numBlocks((xLen + threadsPerBlock.x - 1) / threadsPerBlock.x);

        auto startTime = std::chrono::high_resolution_clock::now();
        for (int i = 0; i < numOfRepetitions; ++i) {
            memCopy2dB <<< numBlocks, threadsPerBlock >>> (dInput, dOutput, xLen, yLen);
            waitForCuda();
        }
        auto stopTime = std::chrono::high_resolution_clock::now();
        std::chrono::duration<double> elapsed = stopTime - startTime;
        std::cout << "GB/s: " << ((2 * dataSize * numOfRepetitions) / elapsed.count()) / gigabyte << std::endl;
    }

    cudaFree(dInput);
    cudaFree(dOutput);

    return 0;
}

编译:

nvcc -std=c++11 memTest.cu -o memTest

【问题讨论】:

  • 第一种方法很可能具有更好的缓存局部性。 gpu 很可能会看到您正在访问顺序内存地址,并且可以使用所需的值预加载内存缓存。由于第二个内核从各处获取内存,因此缓存预加载没有那么大的效果。
  • 根据我的理解,两个内核都在做很好的对齐合并内存读/写。在内核“B”中,每个 warp 有 32 个线程处理 4 字节(浮点)元素,一次执行 128 字节的 RD/WR。测试数据维度也“不错” - 一切都非常适合,没有空闲线程左右。这就是我感到困惑的原因。
  • 在合并的写入之上可能存在一定程度的内存预取。 GPU 可以假设,由于您刚刚请求了这些字节,您可能很快就会想要它们旁边的字节,因此它可以为您预加载缓存。
  • 这是什么 CUDA 版本?关于“附加问题”:bandwidthTest 示例代码通常是这样的复制内核可实现带宽的良好代理。实现峰值理论带宽至少有 2 个限制器。 1. 数据传输有开销——一些位/DRAM 周期通过总线传输,实际上不是用户数据。 2. 像这样的复制内核从读取到写入频繁地“调转总线”。这个周转时间从可实现的带宽中减去。
  • 您想验证并尝试使用 gld128 指令来更好地使总线饱和。 32 位负载,即使合并也可能不会使其饱和。此外,使用 restrict 关键字强制(或显式使用)ldg 指令可能有助于使带宽饱和,尤其是在并发写入时。

标签: c++ performance cuda


【解决方案1】:

我找到了一个如何加速 memCopy2dB 内核的解决方案。这是在 1080Ti 上进行的测试(我不再可以使用 TITAN X)。 问题部分的代码产生以下结果:

GB/s: 365.423
GB/s: 296.678

或多或少与之前在 Titan X 上观察到的百分比差异相同。 现在修改后的 memCopy2dB 内核看起来像:

template<typename T>
__global__ void memCopy2dB(T *in, T *out, size_t xLen, size_t yLen) {
    int xi = blockIdx.x * blockDim.x + threadIdx.x;
    if (xi < xLen) {
        size_t idx = xi;
        for (int y = 0; y < yLen; ++y) {
            __syncthreads();  // <------ this line added
            out[idx] = in[idx];
            idx += xLen;
        }
    }
}

当 warp 中的所有线程都应该访问相同对齐的内存段时,有很多关于 warp 级别的合并内存操作的重要性的信息。 但似乎在一个块中同步扭曲使得在扭曲间级别上的合并成为可能,可能在不同的 GPU 上利用更好的内存总线宽度

无论如何添加这个不需要的行(因为从代码逻辑我不需要同步扭曲)给我以下两个内核的结果:

GB/s: 365.255
GB/s: 352.026

因此,即使代码执行因同步而变慢,我们也会得到更好的结果。我已经在我的一些代码上尝试了这种技术,这些代码以 memCopy2dB 访问模式的方式处理数据,它给了我很好的加速。

【讨论】:

    猜你喜欢
    • 2016-10-13
    • 2021-11-02
    • 1970-01-01
    • 2022-11-02
    • 1970-01-01
    • 2010-10-25
    • 2021-05-30
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多