Резюме: Как избежать потери производительности, вызванной различной рабочей нагрузкой для разных потоков? (Ядро с циклом while на каждом потоке)
Проблема:
Я хочу решить траектории частиц (описанные дифференциальным уравнением 2-го порядка), используя Рунге-Кутту для многих различных начальных условий. Траектории обычно имеют различную длину (каждая траектория заканчивается, когда частица попадает в какую-то цель). Кроме того, чтобы обеспечить числовую стабильность, размер ступени Рунге-Кутты устанавливается адаптивно. Это приводит к двум вложенным циклам while с неизвестным числом итераций (см. Последовательный пример ниже).
Я хочу реализовать процедуру Runge-Kutta для запуска на графическом процессоре с CUDA / C ++. Траектории не зависят друг от друга, поэтому в качестве первого подхода я просто буду распараллеливать различные начальные условия, чтобы каждый поток соответствовал уникальной траектории. Когда поток завершен с траекторией частиц, я хочу, чтобы он начинался с новой.
Если я правильно понимаю, однако, неизвестная длина каждого цикла while (траектория частиц) означает, что разные потоки будут получать разный объем работы , что может привести к серьезной потере производительности на GPU.
Вопрос: Можно ли простым способом преодолеть потери производительности, вызванные различной рабочей нагрузкой для разных потоков? Например, установить для каждого размера деформации только 1, чтобы каждый поток (деформация) мог работать независимо? r приведет ли это к другим потерям производительности (например, нет чтения объединенной памяти)?
Серийный псевдокод :
// Solve a particle trajectory for each inital condition
// N_trajectories: much larger than 1e6
for( int t_i = 0; t_i < N_trajectories; ++t_i )
{
// Set start coordinates
double x = x_init[p_i];
double y = y_init[p_i];
double vx = vx_init[p_i];
double vy = vy_init[p_i];
double stepsize = ...;
double tolerance = ...;
...
// Solve Runge-Kutta trajectory until convergence
int converged = 0;
while ( !converged )
{
// Do a Runge-Kutta step, if step-size too large then decrease it
int goodStepSize = 0
while( !goodStepSize )
{
// Update x, y, vx, vy
double error = doRungeKutta(x, y, vx, vy, stepsize);
if( error < tolerance )
goodStepSize = 1;
else
stepsize *= 0.5;
}
if( (abs(x-x_final) < epsilon) && (abs(y-y_final) < epsilon) )
converged = 1;
}
}
Краткий тест моего кода показывает, что внутренний цикл while выполняется 2-4 раза в 99% всех случаев и> 10 раз в 1% всех случаев, прежде чем будет найден удовлетворительный размер шага Рунге-Кутты.
Параллельный псевдокод :
int tpb = 64;
int bpg = (N_trajectories + tpb-1) / tpb;
RungeKuttaKernel<<<bpg, tpb>>>( ... );
__global__ void RungeKuttaKernel( ... )
{
int idx = ...;
// Set start coordinates
double x = x_init[idx];
...
while ( !converged )
{
...
while( !goodStepSize )
{
double error = doRungeKutta( ... );
...
}
...
}
}