Wow, this ended up being a pretty long answer but I've explained it as thoroughly as I could, so it might be worth the read (or at least I hope so :D) I explain as much as I can about the theory, the practice and little optimizations you can make. Enjoy!
The theory
A kernel in CUDA is actually what gives you extreme parallel processing. CUDA follows a SPMD (Single Program-Multiple Data) approach: a single program (your kernel) is executed along several threads, manipulating multiple data in parallel. Bear in mind it's SPMD and not SIMD since your program code can branch (e.g. in if
clauses, which you should avoid, since branching is heavy for GPUs) so it's not just a single instruction executed in multiple data (which would be a vector-processor, which GPUs are not! I hope I made this clear.)
Inside your kernel you should update your particles. Each thread executing your kernel would correspond to a particle (or a set of particles if you wish, which may help coalescing memory accesses.) That way you should IMMEDIATELY get rid of the for loop, wherever it's right now. Your kernel is practically your new loop (although it's not a loop per se.)
Since your particles don't affect each other you can safely update themselves inside the kernel, not needing a double buffer (which you would need to avoid race conditions.) This means you can safely overwrite each particle data because it doesn't matter to other particles' simulation.
I can see you barely got it, but it's weird how, although you do have a struct ParticleType
, you're passing pointers to position and velocity to your kernel. Why are you not passing the struct array in the first place?
I'm going to continue this answer without using the struct, but if your data is laid as an array of ParticleType
in GPU memory you should pass this array to the kernel, because if you didn't, you'd have to separate the data in different arrays (which would defeat all purposes of paralellization, since it would take a LONG time of memory accesses for each frame.)
Some more theory on kernels
Kernels are executed in batches of blocks (actually in batches of warps, which are 32 threads, but you don't need to know this right now since it's hardware-stuff.) These blocks contain up to 768 threads (in G80... the limit is higher in newer architectures) which can communicate with each other, either by shared memory (with the __shared__
qualifier) or by syncing (calling __synchthreads();
.) Threads inside blocks are guaranteed to be able to do these tasks, but outside blocks, threads are oblivious to other threads (although not to the non-shared data they change.) Even blocks are oblivious to each other.
Once your kernel is executing you need to address your different data indices. But you're not passing any index as an argument! To address your particles, you have some convenient built-in variables:
blockIdx
- Your current global block id.
blockDim
- The global block dimension (threads per block.) Thus blockIdx.x * blockDim.x
is the index of the first thread in your block.
threadIdx
- The current thread id (inside the block.) blockIdx.x * blockDim.x + threadIdx.x
would be your current thread index (easy if you think about it, firstThreadInBlockIndex + currentThreadInsideBlockIndex.)
This way you can address different indices using the following statement:
int idx = blockIdx.x * blockDim.x + threadIdx.x;
See the .x
after each variable? These built-ins are actually dim3
(can have up to three dimensions) but you don't need this, since you're working on a 1d array so you just need the x dimension (just so you knew it.)
Allocating data in GPU
You first need to allocate and feed your buffers in GPU. If you're not modifying your data in your host code (which you shouldn't) you should just allocate the data once and then re-use the buffer in subsequent calls.
To allocate (and copy from host) a single buffer (if you don't use the ParticleType
, you'll need one allocation for the position
, one for the velocity
buffer, etc.) you have to use these CUDA calls:
float* h_position; // Your host pointer. This holds the data (I assume it's already filled with the data.)
float* d_position; // Your device pointer, we will allocate and fill this
cudaMalloc( (void**)&d_position, numParticles * sizeof(float) );
cudaMemcpy( d_position, h_position, numParticles * sizeof(float), cudaMemcpyHostToDevice);
Pretty easy to understand.
First approach: naive implementation
Summing up, you should call your UpdateParticle kernel like this:
int threads_per_block = 128; // You should play with this value
int blocks = numParticles/threads_per_block + ( (numParticles%threads_per_block)1:0 );
I'm making a stop here to clarify the modulo (%) thing. You need it because if numParticles%threads_per_block
is not zero you need one extra block for the remaining threads. E.g. 129 particles/128 part_per_blk = 1 blk
, while you actually need two blocks (one for the first 128 threads, and one block for the remaining one.)
There is a problem with this approach. Think of the aforementioned example: since you're allocating 2 blocks with 128 threads per block, your actually spawning 256 threads... but your array has 129 elements! Not only some threads are going to repeat work but they're also going to do the work twice (which would lead to some particles moving twice as fast) or even worse (this is the case here) unintendedly writing to illegal indices.
You can safely overcome that problem checking for the correct indices. This is your complete kernel call:
UpdateParticle <<< blocks, threads_per_block >>> ( d_position, d_velocity, frameTime, numParticles );
Your kernel should look like this:
__global__ void UpdateParticle(float* position, float* velocity, float frameTime, int numParticles)
{
int idx = blockIdx.x * blockDim.x + threadIdx.x; // Compute the index
if (idx < numParticles) { // Is this index valid?
position[idx] = position[idx] - velocity[idx] * frameTime * 0.001f;
... // some more updates
}
}
Second approach: re-tessellating
As I said earlier, it might be wise to tessellate further inside your kernel. Your GPU bus is 128 bits, so reading 4 floats (4*32 bits = 128 bits
) per thread is just as cheap as reading just one float. You might have to adjust this further if you end up using the ParticleType
array. Just to try keep your data 128-bit aligned.
You have to pass a new argument to your kernel:
UpdateParticle <<< blocks, threads_per_block >>> ( d_position, d_velocity, frameTime, numParticles, particlesPerThread );
Bear in mind this would change your number of blocks, so change it accordingly.
Your kernel would almost be the same.
__global__ void UpdateParticle(float* position, float* velocity, float frameTime, int numParticles, int particlesPerThread)
{
int idx = blockIdx.x * blockDim.x + threadIdx.x;
if (idx < numParticles/particlesPerThread) {
for ( int i = 0; i < particlesPerThread; i++ ) {
position[idx+i] = position[idx+i] - velocity[idx+i] * frameTime * 0.001f;
... // some more updates
}
}
}
Also bear in mind your particlesPerThread
should be a divisor of your total number of particles (or the for loop is going to access indices outside your array size in the very last thread.)
It might also be wise to unroll the loop (as I said earliear, braching is no good to GPUs) thus you wouldn't need the particlesPerThread
parameter (although you still need to adjust blocks
accordingly.) I know this might be hell to maintain but hey, if it's worth the performance... why not do it? This would be your new unrolled kernel:
__global__ void UpdateParticle(float* position, float* velocity, float frameTime)
{
int idx = blockIdx.x * blockDim.x + threadIdx.x;
if (idx < numParticles/4) { // the unrolled loop is 4 iterations
position[idx] = position[idx] - velocity[idx] * frameTime * 0.001f;
position[idx+1] = position[idx+1] - velocity[idx+1] * frameTime * 0.001f;
position[idx+2] = position[idx+2] - velocity[idx+2] * frameTime * 0.001f;
position[idx+3] = position[idx+3] - velocity[idx+3] * frameTime * 0.001f;
... // some more updates
}
}
Conclusion
Unfortunately performace in CUDA involves a lot of tweaking, but I hope with all this background you'll be able to tweak it to fit your needs.
There could be further optimizations such as using __shared__
qualified memory (which is A LOT faster than reading from global-memory) but since you're executing so little operations on your data it would probably not be worth the hassle.
You might also want to precompute the frameTime * 0.001f
bit in a register before anything else (just do float realTime = frameTime * 0.001f
and use it instead) or even better: pass it already transformed from host code. It won't be a problem for such a small number of operations, but registers are also shared between blocks, so registers (any non-qualified variable inside your kernel, like idx
in my examples) can be a bottleneck too. Bear it in mind!