Cuda 累积图像的线条
Cuda accumulate lines of an image
我必须有效地累积存储在数组中的图像的行。
我想出了一个真正天真的解决方案,但我很确定有更好的方法可以用 cuda 来实现。
__global__
void Accumulate(double *x, double *y, int height, int width)
{
int i = blockIdx.x*blockDim.x + threadIdx.x;
if (i >= width)
return;
for (int j = 0; j < height; j++)
{
y[i] += x[j*width+ i];
}
}
这个问题的优雅解决方案是什么?
一般提示
__global__
void Accumulate(float *x, float *y, int height, int width)
{
int i = blockIdx.x * blockDim.x + threadIdx.x;
if (i >= width) return;
float sum = 0;
for (int j = 0; j < height; j++)
{
sum += x[i*width + j];
}
y[i] = sum;
}
这是对应用一些改进后代码的外观的建议。
- 已将
double
替换为 float
。这在大多数显卡上要快得多。如果您真的需要双精度,这是不可能的。但通常情况并非如此。
- 在将总和写入位于全局内存中的数组
y
之前,将总和保存在临时变量 sum
中。访问全局内存是非常昂贵的,尽可能避免它们。
这些提示可能会帮助您在 GPU 上加速代码。
寻找最佳块大小
同样对性能有影响的是块大小。这里没有最佳推荐。但是,一个块中的线程总数应始终能被 32 整除。这是一个 warp 的大小。
对于普通的 CPU 优化解决方案,我会使用这个:
void Accumulate(double *x, double *y, int height, int width)
{
constexpr int line_chunk_size = 16;
double sum[line_chunk_size]; // local stack memory for summing lines
double* line_0_begin = x, * line_0_end = x + width;
// for K chunks (until the less-than-chunk_size part of line is left)
while (line_0_begin+line_chunk_size <= line_0_end) {
double* line_b = line_0_begin;
// copy first line[chunk] to sum[chunk] (to init local sum)
for (int i = 0; i < line_chunk_size; ++i) sum[i] = line_b[i];
// sum remaining lines to sum[chunk]
for (int j = 1; j < height; ++j) {
line_b += width;
for (int i = 0; i < line_chunk_size; ++i) sum[i] += line_b[i];
}
// add sum[chunk] to global y[chunk]
// or assign it by "=", if y is not initialized
for (int i = 0; i < line_chunk_size; ++i) y[i] += sum[i];
y += line_chunk_size;
// process next chunk of lines
line_0_begin += line_chunk_size;
}
// process remaining less-than-chunk values of line
// (this part can be removed, if you have fixed widths divisible by chunk-size)
int remaining_chunk = line_0_end - line_0_begin;
if (remaining_chunk <= 0) return;
// copy first line[remaining] to sum[remaining] (to init local sum)
for (int i = 0; i < remaining_chunk; ++i) sum[i] = line_0_begin[i];
// sum remaining lines to sum[remaining]
for (int j = 1; j < height; ++j) {
line_0_begin += width;
for (int i = 0; i < remaining_chunk; ++i) sum[i] += line_0_begin[i];
}
// add sum[remaining] to global y[remaining]
// or assign it by "=", if y is not initialized
for (int i = 0; i < remaining_chunk; ++i) y[i] += sum[i];
}
gcc6.3 with -O3 -std=c++14 -march=skylake-avx512
命令行选项将生成 this code(如果 link 不再起作用,只需将上面的例程复制到 godbolt.org并自行设置编译器 + 选项)。这在我看来非常理想,我也尝试了其他块大小,例如 32 和 64,但它更像常规循环,而 16 耗尽了 xmm 寄存器以通过完全展开来矢量化块。
(当然,将 -march
调整为您的实际 CPU,这样可执行文件就可以工作了。我只是想试试高端矢量化是否按预期工作)。
这是我用来测试上面代码有效性的代码:
int main()
{
constexpr int width = 40, height = 20;
double testX[width*height], testY[width] = {0};
double* tXptr = testX;
for (int y = 0; y < height; ++y) {
for (int x = 0; x < width; ++x) {
*tXptr = (y <= x) ? 1+x : 0;
++tXptr;
}
}
// print testX for verification
for (int y = 0; y < height; ++y) {
for (int x = 0; x < width; ++x) std::cout << testX[y*width + x] << " ";
std::cout << "\n";
}
std::cout << "Y before:\n";
for (int x = 0; x < width; ++x) std::cout << testY[x] << " ";
std::cout << "\n";
Accumulate(testX, testY, height, width);
std::cout << "Y after first Accumulate:\n";
for (int x = 0; x < width; ++x) std::cout << testY[x] << " ";
std::cout << "\n";
Accumulate(testX, testY, height, width);
std::cout << "Y after second Accumulate:\n";
for (int x = 0; x < width; ++x) std::cout << testY[x] << " ";
std::cout << "\n";
}
拜托,我将不胜感激任何 profiling/measures 在现实生活中,它与您最初的 CUDA 实现的对比(这是单个 CPU 核心,但可以按块传播到多个线程很容易,虽然我不确定 CPU 会在多大程度上识别出内存访问模式没有冲突,也许我会在每个核心上使用几个块,所以 core0 会先处理 10 个列块,core1 块 11。 .20 等。因此每个线程将彼此分开)。我很想听听 CPU(根据我的说法 "good code" ;))在你的原始版本中对抗 CUDA 有多大,只是想知道这些 GPU 有多强大。谢谢(如果做一些分析不需要太多时间的话)。
编辑:我所做的只是同时处理来自同一行的多个值 ("chunk") 到局部总和变量中,最后我将其添加到 y
。我避免在寻址 x
/y
内存时使用任何 multiplication/division/modulus,总体上让编译器有机会了解正在发生的事情,因此它可以积极优化。
如果"chunk"大小是4,你的数据是10x4,这是x
访问内存的顺序:
1111555599
22226666AA
33337777BB
44448888CC
我必须有效地累积存储在数组中的图像的行。
我想出了一个真正天真的解决方案,但我很确定有更好的方法可以用 cuda 来实现。
__global__
void Accumulate(double *x, double *y, int height, int width)
{
int i = blockIdx.x*blockDim.x + threadIdx.x;
if (i >= width)
return;
for (int j = 0; j < height; j++)
{
y[i] += x[j*width+ i];
}
}
这个问题的优雅解决方案是什么?
一般提示
__global__
void Accumulate(float *x, float *y, int height, int width)
{
int i = blockIdx.x * blockDim.x + threadIdx.x;
if (i >= width) return;
float sum = 0;
for (int j = 0; j < height; j++)
{
sum += x[i*width + j];
}
y[i] = sum;
}
这是对应用一些改进后代码的外观的建议。
- 已将
double
替换为float
。这在大多数显卡上要快得多。如果您真的需要双精度,这是不可能的。但通常情况并非如此。 - 在将总和写入位于全局内存中的数组
y
之前,将总和保存在临时变量sum
中。访问全局内存是非常昂贵的,尽可能避免它们。
这些提示可能会帮助您在 GPU 上加速代码。
寻找最佳块大小
同样对性能有影响的是块大小。这里没有最佳推荐。但是,一个块中的线程总数应始终能被 32 整除。这是一个 warp 的大小。
对于普通的 CPU 优化解决方案,我会使用这个:
void Accumulate(double *x, double *y, int height, int width)
{
constexpr int line_chunk_size = 16;
double sum[line_chunk_size]; // local stack memory for summing lines
double* line_0_begin = x, * line_0_end = x + width;
// for K chunks (until the less-than-chunk_size part of line is left)
while (line_0_begin+line_chunk_size <= line_0_end) {
double* line_b = line_0_begin;
// copy first line[chunk] to sum[chunk] (to init local sum)
for (int i = 0; i < line_chunk_size; ++i) sum[i] = line_b[i];
// sum remaining lines to sum[chunk]
for (int j = 1; j < height; ++j) {
line_b += width;
for (int i = 0; i < line_chunk_size; ++i) sum[i] += line_b[i];
}
// add sum[chunk] to global y[chunk]
// or assign it by "=", if y is not initialized
for (int i = 0; i < line_chunk_size; ++i) y[i] += sum[i];
y += line_chunk_size;
// process next chunk of lines
line_0_begin += line_chunk_size;
}
// process remaining less-than-chunk values of line
// (this part can be removed, if you have fixed widths divisible by chunk-size)
int remaining_chunk = line_0_end - line_0_begin;
if (remaining_chunk <= 0) return;
// copy first line[remaining] to sum[remaining] (to init local sum)
for (int i = 0; i < remaining_chunk; ++i) sum[i] = line_0_begin[i];
// sum remaining lines to sum[remaining]
for (int j = 1; j < height; ++j) {
line_0_begin += width;
for (int i = 0; i < remaining_chunk; ++i) sum[i] += line_0_begin[i];
}
// add sum[remaining] to global y[remaining]
// or assign it by "=", if y is not initialized
for (int i = 0; i < remaining_chunk; ++i) y[i] += sum[i];
}
gcc6.3 with -O3 -std=c++14 -march=skylake-avx512
命令行选项将生成 this code(如果 link 不再起作用,只需将上面的例程复制到 godbolt.org并自行设置编译器 + 选项)。这在我看来非常理想,我也尝试了其他块大小,例如 32 和 64,但它更像常规循环,而 16 耗尽了 xmm 寄存器以通过完全展开来矢量化块。
(当然,将 -march
调整为您的实际 CPU,这样可执行文件就可以工作了。我只是想试试高端矢量化是否按预期工作)。
这是我用来测试上面代码有效性的代码:
int main()
{
constexpr int width = 40, height = 20;
double testX[width*height], testY[width] = {0};
double* tXptr = testX;
for (int y = 0; y < height; ++y) {
for (int x = 0; x < width; ++x) {
*tXptr = (y <= x) ? 1+x : 0;
++tXptr;
}
}
// print testX for verification
for (int y = 0; y < height; ++y) {
for (int x = 0; x < width; ++x) std::cout << testX[y*width + x] << " ";
std::cout << "\n";
}
std::cout << "Y before:\n";
for (int x = 0; x < width; ++x) std::cout << testY[x] << " ";
std::cout << "\n";
Accumulate(testX, testY, height, width);
std::cout << "Y after first Accumulate:\n";
for (int x = 0; x < width; ++x) std::cout << testY[x] << " ";
std::cout << "\n";
Accumulate(testX, testY, height, width);
std::cout << "Y after second Accumulate:\n";
for (int x = 0; x < width; ++x) std::cout << testY[x] << " ";
std::cout << "\n";
}
拜托,我将不胜感激任何 profiling/measures 在现实生活中,它与您最初的 CUDA 实现的对比(这是单个 CPU 核心,但可以按块传播到多个线程很容易,虽然我不确定 CPU 会在多大程度上识别出内存访问模式没有冲突,也许我会在每个核心上使用几个块,所以 core0 会先处理 10 个列块,core1 块 11。 .20 等。因此每个线程将彼此分开)。我很想听听 CPU(根据我的说法 "good code" ;))在你的原始版本中对抗 CUDA 有多大,只是想知道这些 GPU 有多强大。谢谢(如果做一些分析不需要太多时间的话)。
编辑:我所做的只是同时处理来自同一行的多个值 ("chunk") 到局部总和变量中,最后我将其添加到 y
。我避免在寻址 x
/y
内存时使用任何 multiplication/division/modulus,总体上让编译器有机会了解正在发生的事情,因此它可以积极优化。
如果"chunk"大小是4,你的数据是10x4,这是x
访问内存的顺序:
1111555599
22226666AA
33337777BB
44448888CC