Utilisation correcte des tampons dans les shaders de calcul OpenGL

Aug 19 2020

Je réécris un algorithme que j'ai d'abord écrit en utilisant une opération matrice / vecteur dans les noyaux OpenGL afin d'essayer de maximiser les performances.

J'ai une connaissance de base d'OpenGL, donc j'ai pu faire fonctionner les choses, mais j'ai beaucoup de mal à faire les différents choix proposés par OpenGL, en particulier les paramètres du tampon qui, je suppose, ont un impact énorme dans mon cas. où je lis et écris beaucoup de données.

J'appelle les trois noyaux séquentiellement:

Premier :

/* 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);
    }
}

Seconde:

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)]);
    }
}

Troisième:

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);
}

Ce que j'ai trouvé en lisant la doc OpenGL:

  • Certaines valeurs identiques pendant toute la durée de l'algorithme sont générées en tant que const avant la compilation du shader. Particulièrement utile pour la limite de la boucle for
  • bufferMat, qui est très petit par rapport aux autres tampons, est placé dans un UBO, qui devrait avoir de meilleures performances que SSBO. Est-ce que je pourrais obtenir de meilleures performances d'événement en en faisant une constante de compilation? C'est petit, mais encore quelques centaines mat4
  • Les autres tampons, étant à la fois lus et écrits plusieurs fois, devraient être meilleurs que SSBO
  • J'ai du mal à comprendre quelle pourrait être la meilleure valeur pour les paramètres «d'utilisation» du tampon. Tous les tampons étant écrits et lus plusieurs fois, je ne sais pas quoi mettre ici.
  • Si je comprends bien, local_size n'est utile que lors du partage de données entre les appels, je devrais donc le garder à un?

Je serais ravi de prendre toutes les recommandations ou astuces sur où chercher pour optimiser ces noyaux!

Réponses

1 NicolBolas Aug 19 2020 at 21:51

Est-ce que je pourrais obtenir de meilleures performances d'événement en en faisant une constante de compilation?

Vous devrez le profiler. Cela étant dit, "quelques centaines de mat4" n'est pas "petit" .

J'ai du mal à comprendre quelle pourrait être la meilleure valeur pour les paramètres «d'utilisation» du tampon. Tous les tampons étant écrits et lus plusieurs fois, je ne sais pas quoi mettre ici.

Premièrement, les paramètres d'utilisation concernent votre utilisation de l'objet tampon, et non l'utilisation par OpenGL de la mémoire derrière eux. Autrement dit, ils parlent des fonctions comme glBufferSubData, glMapBufferRange, et ainsi de suite. READsignifie que le CPU lira à partir du tampon, mais n'y écrira pas. DRAWsignifie que le CPU écrira dans le tampon, mais ne lira pas à partir de celui-ci.

Deuxièmement ... vous ne devriez vraiment pas vous en soucier. Les conseils d'utilisation sont terribles, mal spécifiés et ont été si mal utilisés que de nombreuses implémentations les ignorent carrément. L'implémentation GL de NVIDIA est probablement celle qui les prend le plus au sérieux.

Utilisez plutôt des tampons de stockage immuables . Ces "conseils d'utilisation" ne sont pas des indices ; ce sont les exigences de l'API. Si vous ne l'utilisez pas GL_DYNAMIC_STORAGE_BIT, vous ne pouvez pas écrire dans le tampon via glBufferSubData. Et ainsi de suite.

Si je comprends bien, local_size n'est utile que lors du partage de données entre les appels, je devrais donc le garder à un?

Non. En fait, n'utilisez jamais 1. Si toutes les invocations font leur propre travail, sans barrières d'exécution ou autre, vous devez choisir une taille locale équivalente à la taille du front d'onde du matériel sur lequel vous travaillez . De toute évidence, cela dépend de l'implémentation, mais 32 est certainement une meilleure valeur par défaut que 1.