Menggunakan buffer dengan benar di shader komputasi OpenGL
Saya menulis ulang algoritme yang pertama kali saya tulis menggunakan operasi matriks / vektor ke dalam kernel OpenGL untuk mencoba memaksimalkan kinerja.
Saya memiliki pengetahuan dasar tentang OpenGL, jadi saya bisa membuat semuanya berfungsi, tetapi saya memiliki banyak masalah ketika harus membuat berbagai pilihan yang ditawarkan oleh OpenGL, terutama parameter buffer yang menurut saya berdampak besar dalam kasus saya tempat saya membaca dan menulis banyak data.
Saya menyebut tiga kernel secara berurutan:
Pertama:
/* Generated constants (for all three shaders):
* #version 430
* const vec3 orig
* const float vx
* const ivec2 size
* const uint projections
* const uint subIterations
*/
layout(local_size_x = 1, local_size_y = 1) in;
layout(std430, binding = 0) buffer bufferA { //GL_SHADER_STORAGE_BUFFER, GL_DYNAMIC_READ
uint bufferProjection[]; //Written and read (AtomicAdd) by this shader, read by the second kernel
};
layout(std430, binding = 1) readonly buffer bufferB { //GL_SHADER_STORAGE_BUFFER, GL_DYNAMIC_READ
uint layer[]; //Written and read by the third kernel, read by this shader and by glGetNamedBufferSubData
};
layout(std140) uniform bufferMat { //GL_UNIFORM_BUFFER, GL_STATIC_DRAW
mat4 proj_mat[projections*subIterations]; //Read only by this shader and the third
};
layout(location = 0) uniform int z;
layout(location = 1) uniform int subit;
void main() {
vec4 layer_coords = vec4(orig,1.0) + vec4(gl_GlobalInvocationID.x, z, gl_GlobalInvocationID.y, 0.0)*vx;
uint val = layer[gl_GlobalInvocationID.y*size.x + gl_GlobalInvocationID.x];
for(int i = 0; i < projections; ++i) {
vec4 proj_coords = proj_mat[subit+i*subIterations]*layer_coords;
ivec2 tex_coords = ivec2(floor((proj_coords.xy*size)/(2.0*proj_coords.w)) + size/2);
bool valid = all(greaterThanEqual(tex_coords, ivec2(0,0))) && all(lessThan(tex_coords, size));
atomicAdd(bufferProjection[tex_coords.y*size.x+tex_coords.x+i*(size.x*size.y)], valid?val:0);
}
}
Kedua:
layout(local_size_x = 1, local_size_y = 1) in;
layout(std430, binding = 0) buffer bufferA { //GL_SHADER_STORAGE_BUFFER, GL_DYNAMIC_READ
float updateProjection[]; //Written by this shader, read by the third kernel
};
layout(std430, binding = 1) readonly buffer bufferB { //GL_SHADER_STORAGE_BUFFER, GL_DYNAMIC_READ
uint bufferProjection[]; //Written by the first, read by this shader
};
layout(std430, binding = 2) readonly buffer bufferC { //GL_SHADER_STORAGE_BUFFER, GL_DYNAMIC_READ
uint originalProjection[]; //Only modified by glBufferSubData, read by this shader
};
void main() {
for(int i = 0; i < projections; ++i) {
updateProjection[gl_GlobalInvocationID.x+i*(size.x*size.y)] = float(originalProjection[gl_GlobalInvocationID.x+i*(size.x*size.y)])/float(bufferProjection[gl_GlobalInvocationID.x+i*(size.x*size.y)]);
}
}
Ketiga:
layout(local_size_x = 1, local_size_y = 1) in;
layout(std430, binding = 0) readonly buffer bufferA { //GL_SHADER_STORAGE_BUFFER, GL_DYNAMIC_READ
float updateProjection[]; //Written by the second kernel, read by this shader
};
layout(std430, binding = 1) buffer bufferB { //GL_SHADER_STORAGE_BUFFER, GL_DYNAMIC_READ
uint layer[]; //Written and read by this shader, read by the first kernel and by glGetNamedBufferSubData
};
layout(std140) uniform bufferMat { //GL_UNIFORM_BUFFER, GL_STATIC_DRAW
mat4 proj_mat[projections*subIterations]; //Read only by this shader and and the first
};
layout(location = 0) uniform int z;
layout(location = 1) uniform int subit;
layout(location = 2) uniform float weight;
void main() {
vec4 layer_coords = vec4(orig,1.0) + vec4(gl_GlobalInvocationID.x, z, gl_GlobalInvocationID.y, 0.0)*vx;
float acc = 0;
for(int i = 0; i < projections; ++i) {
vec4 proj_coords = proj_mat[subit+i*subIterations]*layer_coords;
ivec2 tex_coords = ivec2(floor((proj_coords.xy*size)/(2.0*proj_coords.w)) + size/2);
bool valid = all(greaterThanEqual(tex_coords, ivec2(0,0))) && all(lessThan(tex_coords, size));
acc += valid?updateProjection[tex_coords.y*size.x+tex_coords.x+i*(size.x*size.y)]:0;
}
float val = pow(float(layer[gl_GlobalInvocationID.y*size.x + gl_GlobalInvocationID.x])*(acc/projections), weight);
layer[gl_GlobalInvocationID.y*size.x + gl_GlobalInvocationID.x] = uint(val);
}
Apa yang saya dapatkan dengan membaca dokumen OpenGL:
- Beberapa nilai yang sama untuk semua durasi algoritme dihasilkan sebagai const sebelum mengompilasi shader. Berguna terutama untuk batas for-loop
- bufferMat, yang sangat kecil dibandingkan buffer lainnya, dimasukkan ke dalam UBO, yang seharusnya memiliki performa yang lebih baik daripada SSBO. Apakah saya bisa mendapatkan kinerja acara yang lebih baik dengan menjadikannya konstanta waktu kompilasi? Memang kecil, tapi masih beberapa ratus mat4
- Buffer lain, yang dibaca dan ditulis beberapa kali, seharusnya lebih baik sebagai SSBO
- Saya kesulitan memahami apa yang bisa menjadi nilai terbaik untuk parameter 'penggunaan' buffer. Semua buffer sedang ditulis dan dibaca beberapa kali, saya tidak yakin apa yang harus dimasukkan di sini.
- Jika saya mengerti dengan benar, local_size hanya berguna saat berbagi data di antara pemanggilan, jadi saya harus menyimpannya di satu?
Saya akan dengan senang hati mengambil rekomendasi atau petunjuk tentang ke mana harus mencari untuk mengoptimalkan kernel tersebut!
Jawaban
Apakah saya bisa mendapatkan kinerja acara yang lebih baik dengan menjadikannya konstanta waktu kompilasi?
Anda harus memprofilkannya. Yang sedang berkata, "beberapa ratus mat4" bukanlah "kecil" .
Saya kesulitan memahami apa yang bisa menjadi nilai terbaik untuk parameter 'penggunaan' buffer. Semua buffer sedang ditulis dan dibaca beberapa kali, saya tidak yakin apa yang harus dimasukkan di sini.
Pertama, parameter penggunaan sekitar Anda penggunaan objek penyangga, bukan penggunaan OpenGL tentang memori di belakang mereka. Artinya, mereka berbicara tentang fungsi seperti glBufferSubData
, glMapBufferRange
, dan sebagainya. READ
artinya CPU akan membaca dari buffer, tetapi tidak menulis ke buffer tersebut. DRAW
artinya CPU akan menulis ke buffer, tetapi tidak membacanya.
Kedua ... Anda seharusnya tidak peduli. Petunjuk penggunaan sangat buruk, tidak ditentukan dengan benar, dan telah disalahgunakan sehingga banyak implementasi mengabaikannya . Implementasi GL NVIDIA mungkin adalah salah satu yang menganggapnya paling serius.
Sebagai gantinya, gunakan buffer penyimpanan yang tidak dapat diubah . "Petunjuk penggunaan" itu bukanlah petunjuk ; itu adalah persyaratan API. Jika Anda tidak menggunakan GL_DYNAMIC_STORAGE_BIT
, maka Anda tidak dapat menulis ke buffer melalui glBufferSubData
. Dan seterusnya.
Jika saya mengerti dengan benar, local_size hanya berguna saat berbagi data di antara pemanggilan, jadi saya harus menyimpannya di satu?
Tidak. Faktanya, jangan pernah menggunakan 1. Jika semua pemanggilan melakukan hal mereka sendiri, tanpa hambatan eksekusi atau sejenisnya, maka Anda harus memilih ukuran lokal yang setara dengan ukuran wavefront dari perangkat keras yang Anda kerjakan . Jelas itu bergantung pada implementasi, tetapi 32 tentu saja merupakan default yang lebih baik daripada 1.