1 1 Тестовая параллельная задача

Для начала приведем текст параллельного варианта программы, которая будет исполнятся (частично) на графическом процессоре.

  1. #include <stdio.h>
  2. #include <stdlib.h>
  3.  
  4. /* vector elements */
  5. #define xmax 49152
  6. #define ymax 1000
  7. #define THREADS 500
  8. #define BLOCKS 2
  9.  
  10. /* definition of an function executed on GPU */
  11. __global__ void iteration(float *a,float *b,float *rdf)
  12. {
  13. int y = blockIdx.x*blockDim.x+threadIdx.x;
  14. int x;
  15.  
  16. float dt = 0.01f;
  17. float dx = 0.5f;
  18. float dy = 0.5f;
  19. float dff,df = 0.0f;
  20.  
  21. if((y==0) || (y==(ymax-1))) {rdf[y]=df; return;}
  22.  
  23. for(x=1;x<xmax-1;x++) {
  24. int i = (ymax*x)+y; // (x,y)
  25. int ixp = (ymax*(x+1))+y; // (x+1,y)
  26. int ixm = (ymax*(x-1))+y; // (x-1,y)
  27. int iyp = i+1;
  28. int iym = i-1;
  29. dff = dt * (
  30. ((a[ixp]-(2*a[i])+a[ixm])/(dx*dx))
  31. +
  32. ((a[iyp]-(2*a[i])+a[iym])/(dy*dy))
  33. );
  34. b[i]=a[i]+dff;
  35. if( df < fabs(dff) ) { df=fabs(dff); }
  36. }
  37. rdf[y]=df;
  38. }
  39.  
  40. //Подпрограмма задания начальных значений
  41. void initial(float *f[ymax]) {
  42. int x,y;
  43. for(x=0;x<xmax;x++) {
  44. for(y=0;y<ymax;y++) {
  45. f[x][y]=0.0;
  46. }}
  47. }
  48.  
  49. //Подпрограмма задания граничных условий
  50. void boundary(float *f[ymax]) {
  51. int x,y;
  52. for(y=0;y<ymax;y++) {
  53. f[0][y]=0.0;
  54. f[xmax-1][y]=0.0;
  55. }
  56. for(x=0;x<xmax;x++) {
  57. f[x][0]=sin((x*1.0)/(xmax/2));
  58. f[x][ymax-1]=0.0;
  59. }
  60. f[xmax/2][ymax-1]=-5.0;
  61. }
  62.  
  63. /* an entry point */
  64. int main(int argc, char *argv[])
  65. {
  66. float *hostF0, *hostF1;
  67. float *devA, *devB, *devDF;
  68. int i;
  69. float elapsed,df; clock_t start, end;
  70. printf("Start... [%dx%d]\n",xmax,ymax);
  71.  
  72. float **f0 = (float **) malloc(xmax * sizeof(float *));
  73. float **f1 = (float **) malloc(xmax * sizeof(float *));
  74. float *hostDF = (float *) malloc(BLOCKS*THREADS*sizeof(float));
  75. for(i=0;i<xmax;i++) f0[i] = (float *) malloc(ymax * sizeof(float));
  76. for(i=0;i<xmax;i++) f1[i] = (float *) malloc(ymax * sizeof(float));
  77.  
  78. /* execution on GPU */
  79. initial(f0); initial(f1);
  80. boundary(f0); boundary(f1);
  81.  
  82. hostF0=&f0[0][0]; hostF1=&f1[0][0];
  83. cudaMalloc((void**)&devA, xmax*ymax*sizeof(float));
  84. cudaMalloc((void**)&devB, xmax*ymax*sizeof(float));
  85. cudaMalloc((void**)&devDF, BLOCKS*THREADS*sizeof(float));
  86. cudaMemcpy(devA, hostF0, xmax*ymax*sizeof(float), cudaMemcpyHostToDevice);
  87. cudaMemcpy(devB, hostF1, xmax*ymax*sizeof(float), cudaMemcpyHostToDevice);
  88. cudaMemcpy(devDF, hostDF, BLOCKS*THREADS*sizeof(float), cudaMemcpyHostToDevice);
  89. start=clock(); int n = 0;
  90. do {
  91. if( ((n/2)*2) == n ) {
  92. iteration<<<BLOCKS, THREADS>>>(devA,devB,devDF);
  93. }else{
  94. iteration<<<BLOCKS, THREADS>>>(devB,devA,devDF);
  95. }
  96. cudaThreadSynchronize();
  97. cudaMemcpy(hostDF, devDF, BLOCKS*THREADS*sizeof(float), cudaMemcpyDeviceToHost);
  98. n++;
  99. df = 0.0f;
  100. for(i=1;i<BLOCKS*THREADS;i++)
  101. {if(df<hostDF[i]) {df=hostDF[i];}}
  102. //printf(" [%d] Diff: %f\n",n,df);
  103. } while ( df > 0.01f );
  104. end=clock();
  105. cudaFree((void**)&devA);
  106. cudaFree((void**)&devB);
  107. cudaFree((void**)&devDF);
  108. elapsed=((float) (end - start)) / CLOCKS_PER_SEC;
  109. printf("GPU Calculation time: %.2lf sec.\n",elapsed);
  110. printf("Iteration count: %d\n",n);
  111. printf("Average Iteration time: %.2lf sec.\n",elapsed/n);
  112.  
  113. for(i=0;i<xmax;i++) free((void *) f0[i]);
  114. for(i=0;i<xmax;i++) free((void *) f1[i]);
  115. free((void *) f0); free((void *) f1); free ((void *) hostDF);
  116.  
  117. return 0;
  118. }
  119.  

Подпрограмма iteration, выполняющая итерационный цикл расчетов будет модифицирована нами таким образом, чтобы вычисления велись только на одной строке разностной сетки. Таким образом разные строки будут обрабатываться различными параллельными процессами. Далее подробно опишем процесс распараллеливания программы.

В строках 7 и 8 мы определяем сколько будет задействовано ядер (BLOCKS) графического процессора и сколько потоков (THREADS) будет запущено в каждом ядре. Эти константы мы определяем таким образом, чтобы общее количество запущенных процессов (BLOCKS·THREADS) равнялось размерности нашей разностной сетки по оси Y.

Первое отличие заключается в том, что подпрограмма, выполняемая на графическом процессоре, не может возвращать значения, поэтому ее придется немного переделать. Для начала с помощью модификатора __global__ объявим подпрограмму, как исполняемую на GPU. Далее, поскольку в последовательной версии программы подпрограмма iteration возвращала вычисленную максимальную для всей разностной сетки дельту, а в данном случае мы этого сделать не можем, поскольку процесс обрабатывает только свою часть общего массива данных, то добавляем в третий параметр rdf, являющийся массивом. Каждый параллельный процесс будет заносить в соответствующий ему элемент этого массива локальную дельту, а уже потом центральный процесс на основании этого массива будет вычислять глобальную дельту и принимать решение о продолжении итераций.

В строке 13 мы определяем какой именно процесс из общего количества запущенных выполняется и на основе этого определяется какая именно строка разностной сетки будет обработана.

В 21-ой строке программы мы запрещаем обрабатывать первую и последнюю строки, то есть непосредственно границу сетки.

Так как два слоя двумерной разностной сетки передаются в подпрограмму в виде ссылки на одномерный массив данных, то в строках 24-28 мы вычисляем положение интересующих нас элементов. Иными словами мы отображаем одномерный массив на двумерный.

В строке 37 мы заносим в соответствующий текущему процессу элемент массива rdf вычисленную локальную дельту.

Теперь перейдем к основной программе. Процессы, исполняемые на GPU, не имеют возможности обращаться к основной оперативной памяти компьютера. Поэтому для двух слоев нашей разностной сетки и массива локальных дельт мы определяем и размещаем области памяти GPU. Делается это в строках 67 и 83-85.

Непосредстаенно перед началом цикла итераций и после первоначального заполнения массивов мы должны скопировать данные из оперативной памяти в память графического устройства. Этому действию соответствуют строки 86-88 программы.

Запуск процессов на исполнение осуществляется в строках 92 или 94. В отличие от обычного вызова подпрограммы, запуск с использованием GPU описывается с помощью модификатора <<<BLOCKS, THREADS>>>, в котором указываются сколько ядер мультипроцессора будет задействовано и сколько на каждом из них будет выполняться потоков.

exclamationВ действительности это не совсем верно. Параметр BLOCKS указывает не количество задействованных ядер, а количество блоков, каждый из которых будет исполняться на отдельном ядре. Количество таких блоков может быть больше числа ядер GPU. В этом случае эти блоки будут исполнены по мере окончания работы ранее запущенных блоков и освобождения ядер.
 
CUDA использует большое число отдельных нитей для вычислений, часто каждому вычисляемому элементами соответствует одна нить. Все нити группируются в иерархию - grid/block/thread. На вершине иерархии находится grid (решетка), которая по сути являюется объединением всех ядер GPU. Блоки (block) - это совокупность отдельных вычислительных процессов, которые выполняются на одном и том же ядре. И наконец thread (нить) - это непосредственно процесс, осуществляющий вычисления.
 
Иерархия нитей в CUDA
 
Если программа запрашивает количество блоков меньшее или равное количеству ядер GPU, то все эти блоки и соответствующие им нити процессов будут выполнены одновременно. Если же запрошенное количество блоков превышает размер решетки, то какие-то блоки будут поставлены в очередь до того времени, когда в каком-либо выполняемом блоке все нити будут завершены и этот блок освободит ядро GPU.

В строке 96 мы дожидаемся окончания работы всех запущенных блоков, после чего копируем локальные дельты в оперативную память компьютера (строка 97), вычисляем максимальную для всей разностной сетки дельту (строки 99-101) и принимаем решение о продолжении итераций (строка 103).

И, наконец, после того, как итерационный процесс завершился, мы должны освободить занятую память графического процессора: строки 105-107.

Далее посмотрим, какую выгоду это нам принесло...


Copyright © 1998-2011 Юрий Сбитнев