以 GPU (OpenGL) 作为目标的 Halide - 基准测试和使用 HalideRuntimeOpenGL.h
Halide with GPU (OpenGL) as Target - benchmarking and using HalideRuntimeOpenGL.h
我是 Halide 的新手。我一直在研究这些教程来感受这门语言。现在,我正在 OSX.
上从命令行为 运行 编写一个小型演示应用程序
我的目标是对图像执行逐像素操作,在 GPU 上进行调度并测量性能。我已经尝试了一些我想在这里分享的东西,并且对接下来的步骤有几个问题。
第一种方法
我在 GPU 上安排了算法,目标是 OpenGL,但是因为我无法访问 GPU 内存来写入文件,所以在 Halide 例程中,我通过创建 CPU 将输出复制到 Func cpu_out
类似于 Halide repo
中的 glsl sample app
pixel_operation_cpu_out.cpp
#include "Halide.h"
#include <stdio.h>
using namespace Halide;
const int _number_of_channels = 4;
int main(int argc, char** argv)
{
ImageParam input8(UInt(8), 3);
input8
.set_stride(0, _number_of_channels) // stride in dimension 0 (x) is three
.set_stride(2, 1); // stride in dimension 2 (c) is one
Var x("x"), y("y"), c("c");
// algorithm
Func input;
input(x, y, c) = cast<float>(input8(clamp(x, input8.left(), input8.right()),
clamp(y, input8.top(), input8.bottom()),
clamp(c, 0, _number_of_channels))) / 255.0f;
Func pixel_operation;
// calculate the corresponding value for input(x, y, c) after doing a
// pixel-wise operation on each each pixel. This gives us pixel_operation(x, y, c).
// This operation is not location dependent, eg: brighten
Func out;
out(x, y, c) = cast<uint8_t>(pixel_operation(x, y, c) * 255.0f + 0.5f);
out.output_buffer()
.set_stride(0, _number_of_channels)
.set_stride(2, 1);
input8.set_bounds(2, 0, _number_of_channels); // Dimension 2 (c) starts at 0 and has extent _number_of_channels.
out.output_buffer().set_bounds(2, 0, _number_of_channels);
// schedule
out.compute_root();
out.reorder(c, x, y)
.bound(c, 0, _number_of_channels)
.unroll(c);
// Schedule for GLSL
out.glsl(x, y, c);
Target target = get_target_from_environment();
target.set_feature(Target::OpenGL);
// create a cpu_out Func to copy over the data in Func out from GPU to CPU
std::vector<Argument> args = {input8};
Func cpu_out;
cpu_out(x, y, c) = out(x, y, c);
cpu_out.output_buffer()
.set_stride(0, _number_of_channels)
.set_stride(2, 1);
cpu_out.output_buffer().set_bounds(2, 0, _number_of_channels);
cpu_out.compile_to_file("pixel_operation_cpu_out", args, target);
return 0;
}
因为我编译了这个 AOT,所以我在 main()
中为它调用了一个函数。 main()
驻留在另一个文件中。
main_file.cpp
注:这里用的Image
class和本Halide中的一样sample app
int main()
{
char *encodeded_jpeg_input_buffer = read_from_jpeg_file("input_image.jpg");
unsigned char *pixelsRGBA = decompress_jpeg(encoded_jpeg_input_buffer);
Image input(width, height, channels, sizeof(uint8_t), Image::Interleaved);
Image output(width, height, channels, sizeof(uint8_t), Image::Interleaved);
input.buf.host = &pixelsRGBA[0];
unsigned char *outputPixelsRGBA = (unsigned char *)malloc(sizeof(unsigned char) * width * height * channels);
output.buf.host = &outputPixelsRGBA[0];
double best = benchmark(100, 10, [&]() {
pixel_operation_cpu_out(&input.buf, &output.buf);
});
char* encoded_jpeg_output_buffer = compress_jpeg(output.buf.host);
write_to_jpeg_file("output_image.jpg", encoded_jpeg_output_buffer);
}
这工作得很好并且给了我预期的输出。据我了解,cpu_out
使 out
中的值在 CPU 内存中可用,这就是为什么我可以通过访问 main_file.cpp
中的 output.buf.host
来访问这些值=]
第二种方法:
我尝试的第二件事是不通过创建 Func cpu_out
在 Halide 计划中从设备复制到主机,而是使用 main_file.cpp
.[=35 中的 copy_to_host
函数=]
pixel_operation_gpu_out.cpp
#include "Halide.h"
#include <stdio.h>
using namespace Halide;
const int _number_of_channels = 4;
int main(int argc, char** argv)
{
ImageParam input8(UInt(8), 3);
input8
.set_stride(0, _number_of_channels) // stride in dimension 0 (x) is three
.set_stride(2, 1); // stride in dimension 2 (c) is one
Var x("x"), y("y"), c("c");
// algorithm
Func input;
input(x, y, c) = cast<float>(input8(clamp(x, input8.left(), input8.right()),
clamp(y, input8.top(), input8.bottom()),
clamp(c, 0, _number_of_channels))) / 255.0f;
Func pixel_operation;
// calculate the corresponding value for input(x, y, c) after doing a
// pixel-wise operation on each each pixel. This gives us pixel_operation(x, y, c).
// This operation is not location dependent, eg: brighten
Func out;
out(x, y, c) = cast<uint8_t>(pixel_operation(x, y, c) * 255.0f + 0.5f);
out.output_buffer()
.set_stride(0, _number_of_channels)
.set_stride(2, 1);
input8.set_bounds(2, 0, _number_of_channels); // Dimension 2 (c) starts at 0 and has extent _number_of_channels.
out.output_buffer().set_bounds(2, 0, _number_of_channels);
// schedule
out.compute_root();
out.reorder(c, x, y)
.bound(c, 0, _number_of_channels)
.unroll(c);
// Schedule for GLSL
out.glsl(x, y, c);
Target target = get_target_from_environment();
target.set_feature(Target::OpenGL);
std::vector<Argument> args = {input8};
out.compile_to_file("pixel_operation_gpu_out", args, target);
return 0;
}
main_file.cpp
#include "pixel_operation_gpu_out.h"
#include "runtime/HalideRuntime.h"
int main()
{
char *encodeded_jpeg_input_buffer = read_from_jpeg_file("input_image.jpg");
unsigned char *pixelsRGBA = decompress_jpeg(encoded_jpeg_input_buffer);
Image input(width, height, channels, sizeof(uint8_t), Image::Interleaved);
Image output(width, height, channels, sizeof(uint8_t), Image::Interleaved);
input.buf.host = &pixelsRGBA[0];
unsigned char *outputPixelsRGBA = (unsigned char *)malloc(sizeof(unsigned char) * width * height * channels);
output.buf.host = &outputPixelsRGBA[0];
double best = benchmark(100, 10, [&]() {
pixel_operation_gpu_out(&input.buf, &output.buf);
});
int status = halide_copy_to_host(NULL, &output.buf);
char* encoded_jpeg_output_buffer = compress_jpeg(output.buf.host);
write_to_jpeg_file("output_image.jpg", encoded_jpeg_output_buffer);
return 0;
}
所以,现在,我认为正在发生的事情是 pixel_operation_gpu_out
将 output.buf
保留在 GPU 上,当我这样做时 copy_to_host
,那就是我将内存复制到CPU。这个程序也给了我预期的输出。
问题:
第二种方法比第一种方法慢得多。缓慢的部分不在基准测试部分。例如,对于第一种方法,我得到 17 毫秒作为 4k 图像的基准时间。对于同一张图片,在第二种方法中,我得到的基准时间为 22us,而 copy_to_host
所用的时间为 10s。我不确定这种行为是否符合预期,因为方法 1 和方法 2 本质上都在做同样的事情。
接下来我尝试的是使用 [HalideRuntimeOpenGL.h][3]
和 link 纹理输入和输出缓冲区,以便能够直接从 main_file.cpp
绘制到 OpenGL 上下文而不是保存到一个 jpeg 文件。但是,我找不到任何示例来弄清楚如何使用 HalideRuntimeOpenGL.h
中的函数,而且我自己尝试的任何事情总是给我 运行 时间错误,我无法弄清楚如何解决.如果有人有任何资源可以指点我,那就太好了。
此外,也欢迎对我上面的代码提出任何反馈。我知道它有效并且正在做我想做的事,但这可能是完全错误的做事方式,我不会知道更好。
很可能 10s 复制内存的原因是因为 GPU API 已将所有内核调用排队,然后在调用 halide_copy_to_host 时等待它们完成。您可以在 运行 处理所有计算调用之后在基准计时内调用 halide_device_sync 来处理在没有复制返回时间的情况下获取循环内的计算时间。
我无法从代码中得知内核被此代码 运行 了多少次。 (我的猜测是 100,但可能是基准测试的那些参数设置了某种参数化,它尝试 运行 它需要多次以获得重要性。如果是这样,那是一个问题,因为排队调用确实很快,但计算当然是异步的。如果是这种情况,您可以执行诸如排队 10 个调用,然后调用 halide_device_sync 并使用数字“10”来获得多长时间的真实图片它需要。)
我是 Halide 的新手。我一直在研究这些教程来感受这门语言。现在,我正在 OSX.
上从命令行为 运行 编写一个小型演示应用程序我的目标是对图像执行逐像素操作,在 GPU 上进行调度并测量性能。我已经尝试了一些我想在这里分享的东西,并且对接下来的步骤有几个问题。
第一种方法
我在 GPU 上安排了算法,目标是 OpenGL,但是因为我无法访问 GPU 内存来写入文件,所以在 Halide 例程中,我通过创建 CPU 将输出复制到 Func cpu_out
类似于 Halide repo
pixel_operation_cpu_out.cpp
#include "Halide.h"
#include <stdio.h>
using namespace Halide;
const int _number_of_channels = 4;
int main(int argc, char** argv)
{
ImageParam input8(UInt(8), 3);
input8
.set_stride(0, _number_of_channels) // stride in dimension 0 (x) is three
.set_stride(2, 1); // stride in dimension 2 (c) is one
Var x("x"), y("y"), c("c");
// algorithm
Func input;
input(x, y, c) = cast<float>(input8(clamp(x, input8.left(), input8.right()),
clamp(y, input8.top(), input8.bottom()),
clamp(c, 0, _number_of_channels))) / 255.0f;
Func pixel_operation;
// calculate the corresponding value for input(x, y, c) after doing a
// pixel-wise operation on each each pixel. This gives us pixel_operation(x, y, c).
// This operation is not location dependent, eg: brighten
Func out;
out(x, y, c) = cast<uint8_t>(pixel_operation(x, y, c) * 255.0f + 0.5f);
out.output_buffer()
.set_stride(0, _number_of_channels)
.set_stride(2, 1);
input8.set_bounds(2, 0, _number_of_channels); // Dimension 2 (c) starts at 0 and has extent _number_of_channels.
out.output_buffer().set_bounds(2, 0, _number_of_channels);
// schedule
out.compute_root();
out.reorder(c, x, y)
.bound(c, 0, _number_of_channels)
.unroll(c);
// Schedule for GLSL
out.glsl(x, y, c);
Target target = get_target_from_environment();
target.set_feature(Target::OpenGL);
// create a cpu_out Func to copy over the data in Func out from GPU to CPU
std::vector<Argument> args = {input8};
Func cpu_out;
cpu_out(x, y, c) = out(x, y, c);
cpu_out.output_buffer()
.set_stride(0, _number_of_channels)
.set_stride(2, 1);
cpu_out.output_buffer().set_bounds(2, 0, _number_of_channels);
cpu_out.compile_to_file("pixel_operation_cpu_out", args, target);
return 0;
}
因为我编译了这个 AOT,所以我在 main()
中为它调用了一个函数。 main()
驻留在另一个文件中。
main_file.cpp
注:这里用的Image
class和本Halide中的一样sample app
int main()
{
char *encodeded_jpeg_input_buffer = read_from_jpeg_file("input_image.jpg");
unsigned char *pixelsRGBA = decompress_jpeg(encoded_jpeg_input_buffer);
Image input(width, height, channels, sizeof(uint8_t), Image::Interleaved);
Image output(width, height, channels, sizeof(uint8_t), Image::Interleaved);
input.buf.host = &pixelsRGBA[0];
unsigned char *outputPixelsRGBA = (unsigned char *)malloc(sizeof(unsigned char) * width * height * channels);
output.buf.host = &outputPixelsRGBA[0];
double best = benchmark(100, 10, [&]() {
pixel_operation_cpu_out(&input.buf, &output.buf);
});
char* encoded_jpeg_output_buffer = compress_jpeg(output.buf.host);
write_to_jpeg_file("output_image.jpg", encoded_jpeg_output_buffer);
}
这工作得很好并且给了我预期的输出。据我了解,cpu_out
使 out
中的值在 CPU 内存中可用,这就是为什么我可以通过访问 main_file.cpp
中的 output.buf.host
来访问这些值=]
第二种方法:
我尝试的第二件事是不通过创建 Func cpu_out
在 Halide 计划中从设备复制到主机,而是使用 main_file.cpp
.[=35 中的 copy_to_host
函数=]
pixel_operation_gpu_out.cpp
#include "Halide.h"
#include <stdio.h>
using namespace Halide;
const int _number_of_channels = 4;
int main(int argc, char** argv)
{
ImageParam input8(UInt(8), 3);
input8
.set_stride(0, _number_of_channels) // stride in dimension 0 (x) is three
.set_stride(2, 1); // stride in dimension 2 (c) is one
Var x("x"), y("y"), c("c");
// algorithm
Func input;
input(x, y, c) = cast<float>(input8(clamp(x, input8.left(), input8.right()),
clamp(y, input8.top(), input8.bottom()),
clamp(c, 0, _number_of_channels))) / 255.0f;
Func pixel_operation;
// calculate the corresponding value for input(x, y, c) after doing a
// pixel-wise operation on each each pixel. This gives us pixel_operation(x, y, c).
// This operation is not location dependent, eg: brighten
Func out;
out(x, y, c) = cast<uint8_t>(pixel_operation(x, y, c) * 255.0f + 0.5f);
out.output_buffer()
.set_stride(0, _number_of_channels)
.set_stride(2, 1);
input8.set_bounds(2, 0, _number_of_channels); // Dimension 2 (c) starts at 0 and has extent _number_of_channels.
out.output_buffer().set_bounds(2, 0, _number_of_channels);
// schedule
out.compute_root();
out.reorder(c, x, y)
.bound(c, 0, _number_of_channels)
.unroll(c);
// Schedule for GLSL
out.glsl(x, y, c);
Target target = get_target_from_environment();
target.set_feature(Target::OpenGL);
std::vector<Argument> args = {input8};
out.compile_to_file("pixel_operation_gpu_out", args, target);
return 0;
}
main_file.cpp
#include "pixel_operation_gpu_out.h"
#include "runtime/HalideRuntime.h"
int main()
{
char *encodeded_jpeg_input_buffer = read_from_jpeg_file("input_image.jpg");
unsigned char *pixelsRGBA = decompress_jpeg(encoded_jpeg_input_buffer);
Image input(width, height, channels, sizeof(uint8_t), Image::Interleaved);
Image output(width, height, channels, sizeof(uint8_t), Image::Interleaved);
input.buf.host = &pixelsRGBA[0];
unsigned char *outputPixelsRGBA = (unsigned char *)malloc(sizeof(unsigned char) * width * height * channels);
output.buf.host = &outputPixelsRGBA[0];
double best = benchmark(100, 10, [&]() {
pixel_operation_gpu_out(&input.buf, &output.buf);
});
int status = halide_copy_to_host(NULL, &output.buf);
char* encoded_jpeg_output_buffer = compress_jpeg(output.buf.host);
write_to_jpeg_file("output_image.jpg", encoded_jpeg_output_buffer);
return 0;
}
所以,现在,我认为正在发生的事情是 pixel_operation_gpu_out
将 output.buf
保留在 GPU 上,当我这样做时 copy_to_host
,那就是我将内存复制到CPU。这个程序也给了我预期的输出。
问题:
第二种方法比第一种方法慢得多。缓慢的部分不在基准测试部分。例如,对于第一种方法,我得到 17 毫秒作为 4k 图像的基准时间。对于同一张图片,在第二种方法中,我得到的基准时间为 22us,而 copy_to_host
所用的时间为 10s。我不确定这种行为是否符合预期,因为方法 1 和方法 2 本质上都在做同样的事情。
接下来我尝试的是使用 [HalideRuntimeOpenGL.h][3]
和 link 纹理输入和输出缓冲区,以便能够直接从 main_file.cpp
绘制到 OpenGL 上下文而不是保存到一个 jpeg 文件。但是,我找不到任何示例来弄清楚如何使用 HalideRuntimeOpenGL.h
中的函数,而且我自己尝试的任何事情总是给我 运行 时间错误,我无法弄清楚如何解决.如果有人有任何资源可以指点我,那就太好了。
此外,也欢迎对我上面的代码提出任何反馈。我知道它有效并且正在做我想做的事,但这可能是完全错误的做事方式,我不会知道更好。
很可能 10s 复制内存的原因是因为 GPU API 已将所有内核调用排队,然后在调用 halide_copy_to_host 时等待它们完成。您可以在 运行 处理所有计算调用之后在基准计时内调用 halide_device_sync 来处理在没有复制返回时间的情况下获取循环内的计算时间。
我无法从代码中得知内核被此代码 运行 了多少次。 (我的猜测是 100,但可能是基准测试的那些参数设置了某种参数化,它尝试 运行 它需要多次以获得重要性。如果是这样,那是一个问题,因为排队调用确实很快,但计算当然是异步的。如果是这种情况,您可以执行诸如排队 10 个调用,然后调用 halide_device_sync 并使用数字“10”来获得多长时间的真实图片它需要。)