Utilizzo corretto dei buffer negli shader di calcolo OpenGL

Aug 19 2020

Sto riscrivendo un algoritmo che ho scritto per la prima volta utilizzando il funzionamento matrice / vettore nei kernel OpenGL per cercare di massimizzare le prestazioni.

Ho una conoscenza di base di OpenGL, quindi sono riuscito a far funzionare le cose, ma ho molti problemi quando si tratta di fare le varie scelte offerte da OpenGL, in particolare i parametri del buffer che immagino abbiano un enorme impatto nel mio caso dove leggo e scrivo molti dati.

Chiamo i tre kernel in sequenza:

Primo :

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

Secondo:

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

Terzo:

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

Quello che ho scoperto leggendo il documento OpenGL:

  • Alcuni valori che sono gli stessi per tutta la durata dell'algoritmo vengono generati come const prima di compilare lo shader. Particolarmente utile per il limite del ciclo for
  • bufferMat, che è molto piccolo rispetto agli altri buffer, viene inserito in un UBO, che dovrebbe avere prestazioni migliori rispetto a SSBO. È possibile ottenere una migliore performance dell'evento rendendolo una costante del tempo di compilazione? È piccolo, ma ancora poche centinaia mat4
  • Gli altri buffer, essendo sia letti che scritti più volte, dovrebbero essere migliori come SSBO
  • Ho difficoltà a capire quale potrebbe essere il miglior valore per i parametri di "utilizzo" del buffer. Tutti i buffer vengono scritti e letti più volte, non sono sicuro di cosa mettere qui.
  • Se ho capito correttamente, local_size è utile solo quando si condividono dati tra invocazioni, quindi dovrei mantenerlo a uno?

Accetterei volentieri qualsiasi consiglio o suggerimento su dove cercare per ottimizzare questi kernel!

Risposte

1 NicolBolas Aug 19 2020 at 21:51

È possibile ottenere una migliore performance dell'evento rendendolo una costante del tempo di compilazione?

Dovrai profilarlo. Detto questo, "poche centinaia di mat4" non è "piccolo" .

Ho difficoltà a capire quale potrebbe essere il miglior valore per i parametri di "utilizzo" del buffer. Tutti i buffer vengono scritti e letti più volte, non sono sicuro di cosa mettere qui.

In primo luogo, i parametri di utilizzo riguardano l' utilizzo dell'oggetto buffer, non l'utilizzo da parte di OpenGL della memoria sottostante. Cioè, stanno parlando di funzioni come glBufferSubData, glMapBufferRangee così via. READsignifica che la CPU leggerà dal buffer, ma non vi scriverà. DRAWsignifica che la CPU scriverà nel buffer, ma non leggerà da esso.

Secondo ... davvero non dovrebbe importarti. I suggerimenti sull'utilizzo sono terribili, scarsamente specificati e sono stati utilizzati in modo così improprio che molte implementazioni li ignorano completamente . L'implementazione GL di NVIDIA è probabilmente quella che li prende più sul serio.

Utilizzare invece buffer di archiviazione immutabili . Questi "suggerimenti di utilizzo" non sono suggerimenti ; sono requisiti API. Se non usi GL_DYNAMIC_STORAGE_BIT, non puoi scrivere nel buffer tramite glBufferSubData. E così via.

Se ho capito correttamente, local_size è utile solo quando si condividono dati tra invocazioni, quindi dovrei mantenerlo a uno?

No. In effetti, non usare mai 1. Se tutte le invocazioni stanno facendo le loro cose, senza barriere di esecuzione o simili, allora dovresti scegliere una dimensione locale che sia equivalente alla dimensione del fronte d'onda dell'hardware su cui stai lavorando . Ovviamente dipende dall'implementazione, ma 32 è sicuramente un valore predefinito migliore di 1.