GPGPU基础(五)(使用compute shader进行通用计算及示例)

1.工作组及其执行
compute shader是在OpenGL4.3(Opengl es 3.1)以后引入的一种专门用于并行计算的着色器。在计算着色器中,任务以组为单位进行执行,我们称之为工作组(work group)。拥有邻居的工作组被称为本地工作组(local workgroup), 这些组可以组成更大的组,称为全局工作组(global workgroup),而其通常作为执行命令的一个单位。
计算着色器会被每个本地工作组中的每个单元调用一次。工作组的每一个单元称为工作项(work item),每次调用称为一次执行。执行的单元之间可以通过变量和显存进行通信,且可以通过执行同步操作保持一致性。图12-1显示了一个全局工作组。这个全局工作组包括16个本地工作组,每个本地工作组又包括16个执行单元,排成4X4的网格,每个执行单元拥有一个二维向量表示的索引值。尽管图示中,全局和本地工作组都是2维的,而事实上它们都是3维的,为了适应1维、2维的任务,只需把额外的2维或1维设为0即可。计算着色器的每个执行单元本质是相互独立的,可以并行地在支持OpenGL地GPU上执行。
GPGPU基础(五)(使用compute shader进行通用计算及示例)
文章图片

【GPGPU基础(五)(使用compute shader进行通用计算及示例)】

大部分OpenGL硬件会将这些执行单元打包成较小地集合(lockstep),然后将这些小集合拼成本地工作组。本地工作组的大小在计算着色器的代码中输入布局限定符莱设置。全局工作组的大小则是本地工作组大小的整数倍。当计算着色器执行时,它可以通过内置变量来知道当前在本地工作组中的相对坐标、本地工作组的大小,及本地工作组在全局工作组中的相对坐标。基于这些还能进一步获得执行单元在全局工作组红的坐标。着色器根据这些变量来决定应该负责计算任务中的哪些部分,同时也能知道一个工作组中的其他执行单元,以便共享数据。
通过布局限定符在计算着色器中声明本地工作组的大小,分别使用local_size_x,local_size_y,local_size_z,它们的默认值为1.如忽略local_size_z,就会创建一个NXM的2维组。如声明一个本地工作组大小为16x16的着色器。

#version 430 core layout (local_size_x = 16, local_size_y = 16) in; void main(void){ ... }

当创建并链接一个计算着色器后,就可以通过glUseProgram将它设置为当前要执行的程序,然后用glDispatchCompute将工作组发送到计算管线上。
void glDispatchCompute(GLuint num_group_x, GLuint num_group_y, GLuint num_group_z); 在3个维度上分发执行计算工作组,num_group_x、num_group_y和num_group_z分别设置工作组在X、Y、Z维度上的数量。 每个参数均需大于0,小于或等于一个与设备相关的常量数组GL_MAX_COMPUTE_WORK_GROUP_SIZE的对应元素。


2.知道工作组的位置
当执行计算着色器时,它可能需要对输入数组的多个单元赋值,或者需要读取输入数组的特定位置的数据。因此计算着色器需要知道当前处于本地和全局工作组的具体位置。这是坐标是通过OpenGL的一组内置变量获得的。

  • gl_WorkGroupSize是一个用于存储本地工作组大小的常数
  • gl_NumWorkGroups是一个向量,它包含传给glDispatchCompute的参数(num_group_x、num_group_y、num_group_z)
  • gl_LocalInvocationID表示当前执行单元在本地工作组中的位置。它的范围从uvec3(0)到gl_WorkGroupSize-uvec3(1)
  • gl_WorkGroupID表示当前本地工作组在全局工作组中的位置。该变量的范围在uvec3(0)和gl_NumWorkGroups-uvec3(1)之间
  • gl_GlobalInvocationID由gl_LocalInvocationID、gl_WorkGroupSize和gl_WorkGroupID派生而来。它的准确值是gl_WorkGroupID *gl_WorkGroupSize + gl_LocalInvocationID,所以它是当前执行单元在全局工作组中的位置的一种有效的3维索引。
  • gl_LocalInvocationIndex是gl_LocalInvocationID的一种扁平形式。其值等于gl_LocalInvocationID.z * gl_WorkGroupSize.x * gl_WorkGroupSize.y + gl_LocalInvocationID.y * gl_WorkGroupSize.x + gl_LocalInvocationID.x。它可以用1维的索引来代表2维或3维的数据。
3.通信
在计算着色器中可以使用shared关键字来声明着色器变量,其格式与其他关键字uniform等类似。
//一个共享的无符号整型变量 shared uint foo; //一个共享的向量数组 shared vec4 bar[128]; //一个共享的数据块 shared struct baz_struct{ vec4 a_vector; int an_integer; ivec2 an_array_of_integers[27]; }baz[42];

一个变量被声明维shared,那么它将被保存到特定的位置,从而对同一个本地工作组内所有计算着色器可见。如果某个计算着色器请求对共享变量进行写入,那么这个数据的修改信息将最终通知给同一个本地工作组的所有着色器。通常访问共享shared变量的性能会远好于访问图像或者着色器存储缓存(如主内存)的性能。因为着色器会将共享内存作为局部量处理,并且可以在设备中进行拷贝,所以访问共享变量可能比使用缓冲区的方法更迅速。因此,如果着色器需要对同一处内存进行大量的访问,优先考虑将内存拷贝到共享变量中,然后操作。

4.同步
如果本地工作组请求的执行顺序,以及全局工作组中所有本地工作组的执行顺序都没有定义,那么请求执行操作的时机与其他请求完全无关。如果请求之间不需要相互通信,只需要完全独立执行,那么没有问题。但如果请求之间需要进行通信,无论是通过图像,缓存还是共享内存,那么我们就有必要对它们的操作进行同步处理。

同步命令有两种。首先是运行屏障(execution barrier),可以通过barrier()函数触发。如果计算着色器的一个请求遇到barrier,那么它会停止运行,等待同一个本地工作组的所有请求也到达barrier,然后才会执行后面的代码。
第二种同步叫做内存屏障(memory barrier)。它的最直接版本就是memoryBarrier()。如果调用memoryBarrier,那么久可以保证着色器请求内存的写入操作一定是提交到内存端,而不是通过缓冲区(cache)或者调度队列之类的方式。所有发生在memoryBarrier之后的操作在读取同一处内存时,都可以使用这些内存写入的结果,即使同一个计算着色器的其他请求也是如此。
5.例子
本例运行环境为ubuntu(非虚拟机,虚拟机一运行compute shader相关的代码就报 exit code 139),需要先安装好OpenGL环境,GLUT和GLEW。在这个例子中,会先产生一个 1024 个数据,并将这些数据赋予一个 16 x 16 x 4 的image2D,然后通过计算着色器对这个image2D的每个坐标点的4个通道分别加上0、1、2、3,最后将计算结果读取出来。
首先初始化GL环境
void initGLUT(int argc, char **argv) { glutInit(&argc, argv); glutWindowHandle = glutCreateWindow("GPGPU Tutorial"); }

创建FBO
void initFBO() { // create FBO (off-screen framebuffer) glGenFramebuffers(1, &fb); // bind offscreen framebuffer (that is, skip the window-specific render target) glBindFramebuffer(GL_FRAMEBUFFER, fb); }

创建texture
/** * create textures and set proper viewport etc. */ void createTextures() { glGenTextures(1, &outputTexID); glGenTextures(1, &intermediateTexID); glGenTextures(1, &inputTexID); // set up textures setupTexture(outputTexID); setupTexture(intermediateTexID); setupTexture(inputTexID); glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, textureParameters.texTarget, inputTexID, 0); glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT1, textureParameters.texTarget, intermediateTexID, 0); glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT2, textureParameters.texTarget, outputTexID, 0); transferToTexture(pfInput, inputTexID); // set texenv mode glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE); }/** * Sets up a floating point texture with the NEAREST filtering. */ void setupTexture(const GLuint texID) { // make active and bind glBindTexture(textureParameters.texTarget, texID); glTexStorage2D(GL_TEXTURE_2D, 8, GL_RGBA32F, 16, 16); // turn off filtering and wrap modes glTexParameteri(textureParameters.texTarget, GL_TEXTURE_MIN_FILTER, GL_NEAREST); glTexParameteri(textureParameters.texTarget, GL_TEXTURE_MAG_FILTER, GL_NEAREST); glTexParameteri(textureParameters.texTarget, GL_TEXTURE_WRAP_S, GL_CLAMP); glTexParameteri(textureParameters.texTarget, GL_TEXTURE_WRAP_T, GL_CLAMP); }

注意:不要用 glTexImage2D生成纹理。
A very important restriction for using shader images is that the underlying texture must have been allocated using "immutable" storage, i.e. via glTexStorage*()-like functions, and not glTexImage2D().

将数据赋予inputTexID
void transferToTexture(float *data, GLuint texID) { glBindTexture(textureParameters.texTarget, texID); glTexSubImage2D(textureParameters.texTarget, 0, 0, 0, unWidth, unHeight, textureParameters.texFormat, GL_FLOAT, data); }

执行计算
void performCompute(const GLuint inputTexID, const GLuint outputTexID) { // enable GLSL program glUseProgram(glslProgram); glUniform1fv(glGetUniformLocation(glslProgram, "v"), 4, v); // Synchronize for the timing reason. glFinish(); CTimer timer; long lTime; timer.reset(); glBindImageTexture(0, inputTexID, 0, GL_FALSE, 0, GL_READ_ONLY, GL_RGBA32F); glBindImageTexture(1, outputTexID, 0, GL_FALSE, 0, GL_WRITE_ONLY, GL_RGBA32F); glDispatchCompute(1, 1, 1); glMemoryBarrier(GL_SHADER_IMAGE_ACCESS_BARRIER_BIT); glFinish(); lTime = timer.getTime(); cout << "Time elapsed: " << lTime << " ms." << endl; }

compute shader代码
#version 430 corelayout (local_size_x = 16, local_size_y = 16) in; // 传递卷积核 uniform float v[4]; layout (rgba32f, binding = 0) uniform image2D input_image; layout (rgba32f, binding = 1) uniform image2D output_image; shared vec4 scanline[16][16]; void main(void) { ivec2 pos = ivec2(gl_GlobalInvocationID.xy); scanline[pos.x][pos.y] = imageLoad(input_image, pos); barrier(); vec4 data = https://www.it610.com/article/scanline[pos.x][pos.y]; data.r = v[0] + data.r; data.g = v[1] + data.g; data.b = v[2] + data.b; data.a = v[3] + data.a; imageStore(output_image, pos.xy, data); }

读回数据
void transferFromTexture(float *data) { glReadBuffer(GL_COLOR_ATTACHMENT2); glReadPixels(0, 0, unWidth, unHeight, textureParameters.texFormat, GL_FLOAT, data); }

主流程代码
#include #include #include #include #include #include "utils/CReader.h" #include "utils/CTimer.h"#define WIDTH16//data block width #define HEIGHT16//data block heightusing namespace std; void initGLSL(GLenum type); void initFBO(); void initGLUT(int argc, char **argv); void createTextures(void); void setupTexture(const GLuint texID); void performCompute(const GLuint inputTexID, const GLuint outputTexID); void transferFromTexture(float *data); void transferToTexture(float *data, GLuint texID); // 纹理标识 GLuint outputTexID; GLuint intermediateTexID; GLuint inputTexID; // GLSL 变量 GLuint glslProgram; GLuint fragmentShader; // FBO 标识 GLuint fb; // 提供GL环境 GLuint glutWindowHandle; struct structTextureParameters { GLenum texTarget; GLenum texInternalFormat; GLenum texFormat; char *shader_source; } textureParameters; float *pfInput; unsigned unWidth = (unsigned) WIDTH; unsigned unHeight = (unsigned) HEIGHT; unsigned unSize = unWidth * unHeight; GLfloat v[4]; // 传如compute shader中int main(int argc, char **argv) { int i; // 创建测试数据 unsigned unNoData = https://www.it610.com/article/4 * unSize; //total number of Data pfInput = new float[unNoData]; float *pfOutput = new float[unNoData]; for (i = 0; i < unNoData; i++) pfInput[i] = i * 0.001f; for (i = 0; i < 4; i++) { v[i] = i; }// create variables for GL textureParameters.texTarget = GL_TEXTURE_2D; textureParameters.texInternalFormat = GL_RGBA32F; textureParameters.texFormat = GL_RGBA; CReader reader; initGLUT(argc, argv); glewInit(); initFBO(); createTextures(); char c_convolution[] ="../convolution.cs"; textureParameters.shader_source = reader.textFileRead(c_convolution); initGLSL(GL_COMPUTE_SHADER); performCompute(inputTexID, intermediateTexID); performCompute(intermediateTexID, outputTexID); // get GPU results transferFromTexture(pfOutput); for (int i = 0; i < unNoData; i++) { cout << "input:" << pfInput[i] << " output:" << pfOutput[i] << endl; }// clean up glDetachShader(glslProgram, fragmentShader); glDeleteShader(fragmentShader); glDeleteProgram(glslProgram); glDeleteFramebuffersEXT(1, &fb); glDeleteTextures(1, &inputTexID); glDeleteTextures(1, &outputTexID); glutDestroyWindow(glutWindowHandle); // exit delete pfInput; delete pfOutput; return EXIT_SUCCESS; }/** * Set up the GLSL runtime and creates shader. */ void initGLSL(GLenum type) { // create program object glslProgram = glCreateProgram(); // create shader object (fragment shader) fragmentShader = glCreateShader(type); // set source for shader const GLchar *source = textureParameters.shader_source; glShaderSource(fragmentShader, 1, &source, nullptr); // compile shader glCompileShader(fragmentShader); // attach shader to program glAttachShader(glslProgram, fragmentShader); // link into full program, use fixed function vertex shader. // you can also link a pass-through vertex shader. glLinkProgram(glslProgram); }

全部代码


    推荐阅读