Ho iniziato ad imparare OpenCL e attualmente sto provando a testare quanto posso migliorare le prestazioni per un semplice algoritmo di animazione scheletrica. Per fare questo ho scritto un programma che esegue l'animazione scheletrica da vertici generati casualmente e matrici di trasformazione due volte, una volta con una libreria algebra lineare ottimizzata SSE in C++ semplice, e una volta usando il mio kernel OpenCL su GPU (sto testando su un Nvidia GTX 460).OpenCL Performance Optimization
Ho iniziato con un semplice kernel in cui ogni oggetto di lavoro trasforma esattamente un vertice, con tutti i valori letti dalla memoria globale. Poiché non ero soddisfatto delle prestazioni di questo kernel, ho cercato di ottimizzare un po '. Il mio kernel attuale si presenta così:
inline float4 MultiplyMatrixVector(float16 m, float4 v)
{
return (float4) (
dot(m.s048C, v),
dot(m.s159D, v),
dot(m.s26AE, v),
dot(m.s37BF, v)
);
}
kernel void skelanim(global const float16* boneMats, global const float4* vertices, global const float4* weights, global const uint4* indices, global float4* resVertices)
{
int gid = get_global_id(0);
int lid = get_local_id(0);
local float16 lBoneMats[NUM_BONES];
async_work_group_copy(lBoneMats, boneMats, NUM_BONES, 0);
barrier(CLK_LOCAL_MEM_FENCE);
for (int i = 0 ; i < NUM_VERTICES_PER_WORK_ITEM ; i++) {
int vidx = gid*NUM_VERTICES_PER_WORK_ITEM + i;
float4 vertex = vertices[vidx];
float4 w = weights[vidx];
uint4 idx = indices[vidx];
resVertices[vidx] = (MultiplyMatrixVector(lBoneMats[idx.x], vertex * w.x)
+ MultiplyMatrixVector(lBoneMats[idx.y], vertex * w.y)
+ MultiplyMatrixVector(lBoneMats[idx.z], vertex * w.z)
+ MultiplyMatrixVector(lBoneMats[idx.w], vertex * w.w));
}
}
Ora mi elaborare un numero costante di vertici per il lavoro-voce, e io prefetch tutte le matrici ossee nella memoria locale solo una volta per ogni opera-voce, che ho creduto porterebbe per migliorare le prestazioni in quanto le matrici per più vertici potrebbero essere letti dalla memoria locale più veloce in seguito. Sfortunatamente, questo kernel ha prestazioni peggiori del mio primo tentativo e persino peggiore rispetto all'implementazione della sola CPU.
Perché le prestazioni sono così negative con questa ottimizzazione dovrebbe essere?
Se aiuta, ecco come ho eseguire il kernel:
#define NUM_BONES 50
#define NUM_VERTICES 30000
#define NUM_VERTICES_PER_WORK_ITEM 100
#define NUM_ANIM_REPEAT 1000
uint64_t PerformOpenCLSkeletalAnimation(Matrix4* boneMats, Vector4* vertices, float* weights, uint32_t* indices, Vector4* resVertices)
{
File kernelFile("/home/alemariusnexus/test/skelanim.cl");
char opts[256];
sprintf(opts, "-D NUM_VERTICES=%u -D NUM_REPEAT=%u -D NUM_BONES=%u -D NUM_VERTICES_PER_WORK_ITEM=%u", NUM_VERTICES, NUM_ANIM_REPEAT, NUM_BONES, NUM_VERTICES_PER_WORK_ITEM);
cl_program prog = BuildOpenCLProgram(kernelFile, opts);
cl_kernel kernel = clCreateKernel(prog, "skelanim", NULL);
cl_mem boneMatBuf = clCreateBuffer(ctx, CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR, NUM_BONES*sizeof(Matrix4), boneMats, NULL);
cl_mem vertexBuf = clCreateBuffer(ctx, CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR, NUM_VERTICES*sizeof(Vector4), vertices, NULL);
cl_mem weightBuf = clCreateBuffer(ctx, CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR, NUM_VERTICES*4*sizeof(float), weights, NULL);
cl_mem indexBuf = clCreateBuffer(ctx, CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR, NUM_VERTICES*4*sizeof(uint32_t), indices, NULL);
cl_mem resVertexBuf = clCreateBuffer(ctx, CL_MEM_WRITE_ONLY | CL_MEM_ALLOC_HOST_PTR, NUM_VERTICES*sizeof(Vector4), NULL, NULL);
uint64_t s, e;
s = GetTickcount();
clSetKernelArg(kernel, 0, sizeof(cl_mem), &boneMatBuf);
clSetKernelArg(kernel, 1, sizeof(cl_mem), &vertexBuf);
clSetKernelArg(kernel, 2, sizeof(cl_mem), &weightBuf);
clSetKernelArg(kernel, 3, sizeof(cl_mem), &indexBuf);
clSetKernelArg(kernel, 4, sizeof(cl_mem), &resVertexBuf);
size_t globalWorkSize[] = { NUM_VERTICES/NUM_VERTICES_PER_WORK_ITEM };
size_t localWorkSize[] = { NUM_BONES };
for (size_t i = 0 ; i < NUM_ANIM_REPEAT ; i++) {
clEnqueueNDRangeKernel(cq, kernel, 1, NULL, globalWorkSize, localWorkSize, 0, NULL, NULL);
}
clEnqueueReadBuffer(cq, resVertexBuf, CL_TRUE, 0, NUM_VERTICES*sizeof(Vector4), resVertices, 0, NULL, NULL);
e = GetTickcount();
return e-s;
}
Credo che ci sono altre cose che potrebbero essere ottimizzati, forse il dosaggio alcune delle altre globale legge insieme, ma prima mi piacerebbe davvero per sapere perché questa prima ottimizzazione non ha funzionato.
non so circa le prestazioni, ma cosa si sta facendo sembra avere risultati non definiti . Si utilizza un'operazione async_copy seguita da una barriera. La barriera non aspetterà che finisca la copia asincrona - continuerà non appena tutti gli elementi di lavoro avranno raggiunto quel punto. Secondo le specifiche, devi usare la funzione wait_group_events nel tuo kernel dopo un async_copy, oppure i risultati non sono definiti. Questo ha senso, perché il processo async_copy sta accadendo mentre il resto del kernel è in esecuzione, quindi wait_group_events costringerà il kernel ad assicurarsi che la copia della memoria sia fatta. –