Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
курсак.doc
Скачиваний:
3
Добавлен:
07.12.2018
Размер:
787.97 Кб
Скачать

3. Технологічний розділ

3.1 Опис алгоритму

Як було вище зазначено, що розробка OpenCL проекту, полягая з написанню ядeр(kernal), почнемо з написанню ядер.

Написання Kernal.

Добуток матриць. Взагалі ядра OpenCL дуже нагадують звичайну фунцію в мові С, а бо її розширенню С++. Фрагмент коду 3.1. Kernal вказує що це саме ядро OpenCL, далі слідує тип, і назва функції, після того відкриваються дужки і перечислюємо вхідні параметри. Далі потрібно підсумувати суму по рядкам і по стовбцям( i-рядок * на j-стовбець, потім сумуємо). Змінна і, j індетифікатор global вказує, що читати і записувати данні будимо в глобальну пам'ять. Також є перевірка, щоб не війти за значення масивів. Цикл саме для сумування. Для реалізації алгоритму перемноження матриць, потрібно 3 цикли, чому тут 1?тому що int i = get_global_id (1); це вже цикл мовою OpenCl. Тому потрібно лише 1 для підрахунку сум. А далі записуємо результат з нашої суми, в масив.

Фрагмент коду 3.1. Добуток матриць.

1. __kernel void matrix_multiply (

unsigned AH , unsigned AW , __global float * AE ,

unsigned BH , unsigned BW , __global float * BE ,

unsigned RH , unsigned RW , __global float * RE, int iNumElements)

{

2. float sum = 0;

3. int i = get_global_id (1);

4. int j = get_global_id (0);

5. int k;

6. if ((i>= iNumElements)&&( j>= iNumElements))

{

7. return;

}

8. for (k = 0; k < AW; k += 1)

{

9. sum += AE[i * AW + k] * BE[k * BW + j];

}

10. RE[i * RW + j] = sum ;

}

Віднімання матриць. Фрагмент коду 3.2. Все майже так само як і в добутку матриць, але зменшилась кількість параметрів, тай циклів зменшилось, потрібно всього два, і суму не потрібно рахувати. Як читати данні так і записувати будемо з глобально пам’яті.

Фрагмент коду 3.2. Віднімання матриць.

1. __kernel void matrix_Sub (__global float * AE , __global float * BE ,

__global float * RE, int iNumElements)

{

2. int i = get_global_id (1);

3. int j = get_global_id (0);

4. if ((i>= iNumElements)&&( j>= iNumElements))

{

5. return;

}

6. RE[i * j] = AE[i *j] - BE[i *j];

}

Сума матриць. Фрагмент коду 3.3. Змінилась назва, і тепер не віднімаємо, а додаємо.

Фрагмент коду 3.3. Додавання матриць.

1. __kernel void matrix_ADD (__global float * AE , __global float * BE ,

__global float * RE, int iNumElements)

{

2. int i = get_global_id (1);

3. int j = get_global_id (0);

4. if ((i>= iNumElements)&&( j>= iNumElements))

{

5. return;

}

6. RE[i * j] = AE[i *j] + BE[i *j];

}

Додавання векторів. Фрагмент коду 3.4. Передача параметрів як покажчики. Перевірка щоб не вийти за межі масиву. Формування одного циклу, і запис суми в інший вектор.

Фрагмент коду 3.4. Додавання векторів.

1.__kernel void VectorAdd(__global const float* a, __global const float* b, __global float* c, int iNumElements)

{

2. int iGID = get_global_id(0);

3. if (iGID >= iNumElements)

{

4. return;

}

5. c[iGID] = a[iGID] + b[iGID];

}

Віднімання векторів. Фрагмент коду 3.5. Передача параметрів як покажчики. Перевірка щоб не вийти за межі масиву. Формування одного циклу, і запис різниці в інший вектор.

Фрагмент коду 3.5. Віднімання двох векторів.

1.__kernel void VectorSub(__global const float* a, __global const float* b, __global float* c, int iNumElements)

{

2. int iGID = get_global_id(0);

3. if (iGID >= iNumElements)

{

4. return;

}

5. c[iGID] = a[iGID] - b[iGID];

}

Множення вектора на скаляр. Фрагмент коду 3.6. При множенні вектора на скаляр, отримується такий самий вектор. Передача параметрів. Перевірка щоб не вийти за межі масиву. Ну і саме перемноження.

Фрагмент коду 3.6. Вектор на скаляр.

1. __kernel void VectorMulSc(__global const float* a, __global const float* b, __global float* c, int iNumElements)

{

2. int iGID = get_global_id(0);

3. if (iGID >= iNumElements)

{

4. return;

}

5. c[iGID] = a[iGID] * b[0];

}

Добуток векторів. Фрагмент коду 3.7. При добутку двох в результаті отримуємо число. Тобто потрібно просумувати по циклу. Що й показано на прикладі.

Фрагмент коду 3.7. Добуток векторів.

1. __kernel void VectorSub(__global const float* a, __global const float* b, int iNumElements)

{

2. int iGID = get_global_id(0);

3. int Sum=0;

4. if (iGID >= iNumElements)

{

5. return;

}

6. Sum += a[iGID] * b[iGID];

}

По-перше, в тому, як добувається індекс нитки? При запуску ниток , OpenCL створює індексний простір, який повинен бути звично 1-но, 2-ух або 3-ох мірним. У цьому просторі кожна нитка має свій унікальний глобальний індекс, i-ту компоненту якого і повертає виклик get_global_id (i). Цей глобальний індексний простір може бути розбитий на локальні блоки, і нитки можуть визначати свої індекси і всередині цих блоків, але глобальна нумерація залишається при цьому попередньою. І тут, мабуть, варто підкреслити: в OpenCL глобальний індексний простір розбивається на блоки, на відміну від CUDA, та Ati Stream, де глобальний простір складається як сітка, з локальних блоків, тобто, в OpenCL первинно глобальний простір, а в CUDA та Ati Stream локальні блоки.

По-друге, при описі ядер на OpenCL C програміст повинен вказувати область дії показників. OpenCL подібно CUDA та Ati Stream передбачає наявність різної пам'яті на прискорювачі. Вважається, що є глобальна, більш повільна пам'ять, доступна всім ниткам, і доступна для звернень (читання і запису блоків даних) з боку основної системи. І, що є локальна, швидка пам'ять, доступна ниткам, зібраними в одну локальну групу. Відповідно, модифікатори __global і __local. Використовують перший, щоб вказати те, що читати матриці і записувати результати обчислення потрібно в глобальну пам'ять прискорювача.

По-третє, власне сам процес запуску обчислень. Логічні кроки, які потрібно виконати для цього запуску, такі ж, які Brook + і CUDA. Але технічно, через те, що OpenCL є тільки бібліотекою і ніяк не розширює основну мову програмування якимись новими базовими типами і синтаксичними конструкціями, для запуску ниток потрібно описати більше операцій і деталей, які в Ati Stream або C for CUDA формуються і уточнюються відповідними компіляторами автоматично.

Кожне звернення до функції OpenCL потенційно може повернути код помилки, і помилки ці в реальних програмах слід обробляти.

Написання HOST.

1. Host написаний на мові С++. Робота з прискорювачем починається зі створення контексту для роботи з пристроями(фрагмент коду 3.8. - це перший пристрій) черги команд (рядки 1-6 в Фрагменті коду 3.8.).

Фрагмент коду 3.8. Ініціалізація пристрою.

1. cntx = clCreateContextFromType (NULL , CL_DEVICE_TYPE_GPU , NULL , NULL , NULL );

2. clGetContextInfo (cntx , CL_CONTEXT_DEVICES , 0, NULL , &cb );

3. ds = malloc (cb );

4. clGetContextInfo (cntx , CL_CONTEXT_DEVICES , cb , ds , NULL );

5. cq = clCreateCommandQueue (cntx , ds [0] , 0, NULL );

6. free (ds );

фрагмент коду 3.8. контекст створюється для GPU, що в термінах OpenCL означає пристрої, які окрім проведення обчислень здатні забезпечувати роботу з графікою через інтерфейси OpenGL або DirectX. Контекст можна створювати і для інших пристроїв, наприклад, для центральних процесорів (CL_DEVIC_TYPE_CPU) або прискорювачів на основі процесорів Cell (CL_DEVICE_TYPE_ACCELERATOR), або взагалі для всіх OpenCL пристроїв в системі (CL_DEVICE_TYPE_ALL). Як вже було зазначено, OpenCL намагаються зробити універсальним інструментом.

Контекст визначає те, в які бінарні коди потрібно транслювати вихідний текст ядра, і на яких пристроях запускати нитки, ці коди виконують і інші особливості.

Самий запуск груп ниток, що виконують ядра, або операції запису / читання даних в пам'ять / з пам`яті прискорювачів задається чергою спеціальних команд, виконуваних не обов'язково в послідовному порядку. Частково порядок виконання цих команд, можна змінити за допомогою подій (events), операції з якими так само визначає OpenCL. Власне ось: черга команд (command queue) тут створюється для того, щоб можна було запустити через неї обчислення. Вона прив'язується до одного (першому серед доступних) пристрою.

2. Далі (рядки1-3 в фрагменті коду 3.9.) здійснюється компіляція програми і створення з неї ядра.

Фрагмент коду 3.9. Компіляція програми.

1. p = clCreateProgramWithSource (cntx , 1, & program_source , NULL , NULL );

2. clBuildProgram (p, 0, NULL , NULL , NULL , NULL );

3. k = clCreateKernel (p, " matrix_multiply ", NULL );

Вважається, що вихідний текст програми «ядра», послідовність байтів, завантажених в пам'ять і на них вказуює program_source. Трансляція здійснюється в створеному раніше контексті, тобто, для пристроїв класу GPU. Програму можна формувати не з одного джерела, а з декількох (в розглянутому фрагменті коду 3.9. явно вказано як використовувати один текст у другому аргументі виклику clCreateProgramWithSource). Після компіляції програми «бінарні» модулі можна отримати за допомогою спеціальної функції OpenCL.

3. Наступними діями (рядки 1-9 в фрагмент коду 3.10.) виділяється пам'ять на прискорювачі.

Фрагмент коду 3.10. Виділяється пам'ять на прискорювачі.

1. A.H = L; A.W = M;

2. A.E = clCreateBuffer (cntx , CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR ,

3. A.H * A.W * sizeof ( cl_float ), mem_A , NULL );

4. B.H = M; B.W = N;

5. B.E = clCreateBuffer (cntx , CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR ,

6. B.H * B.W * sizeof ( cl_float ), mem_B , NULL );

7. R.H = L; R.W = N;

8. R.E = clCreateBuffer (cntx , CL_MEM_READ_WRITE ,

9. R.H * R.W * sizeof ( cl_float ), NULL , NULL );

Одночасно з цим вона заповнюється вихідними даними для матриць A і B. У OpenCL це можна зробити за один виклик, що є зручно.

4. Потім йде блок коду (рядки 1-9 в фрагменті коду 3.10.), який встановлює аргументи ядра.

Фрагмент коду 3.10. Встановлення аргументів для ядра.

1. clSetKernelArg (k, i += 1, sizeof ( unsigned ), &A.H);

2. clSetKernelArg (k, i += 1, sizeof ( unsigned ), &A.W);

3. clSetKernelArg (k, i += 1, sizeof ( cl_mem ), &A.E);

4. clSetKernelArg (k, i += 1, sizeof ( unsigned ), &B.H);

5. clSetKernelArg (k, i += 1, sizeof ( unsigned ), &B.W);

6. clSetKernelArg (k, i += 1, sizeof ( cl_mem ), &B.E);

7. clSetKernelArg (k, i += 1, sizeof ( unsigned ), &R.H);

8. clSetKernelArg (k, i += 1, sizeof ( unsigned ), &R.W);

9. clSetKernelArg (k, i += 1, sizeof ( cl_mem ), &R.E);

Тут ще раз можна повторити зауваження про те, що область пам'яті в прискорювачі описуються значеннями типу cl_mem. Коли система виконання OpenCL виявляє, що деякий аргумент ядра є покажчиком, то вона очікує передачі значення типу cl_mem, щоб правильно сформувати цей покажчик.

Відповідно, теоретично, іншим способом передати інформацію про область пам'яті в ядро OpenCL не можна.

При цьому, аргументи ядра передаються дуже просто - через копіювання зазначеного обсягу (третій аргумент clSetKernelArg) даних, розміщених за вказаною адресою (аргумент 4). після копіювання даних у clSetKernelArg, пам'ять можна знову використовувати.

5. Нарешті (рядки 1-4 в фрагменті коду 3.11.) запуск обчислення і отримання його результату.

Фрагмент коду 3.11. Отримання результату.

1. clEnqueueNDRangeKernel (cq , k, 2, NULL ,

2. global_work_size , local_work_size , 0, NULL , NULL );

3. clEnqueueReadBuffer (cq , R.E, CL_TRUE , 0,

4. R.W * R.H * sizeof ( cl_float ), mem_R , 0, NULL , NULL );

Здійснюється це через запис команд у чергу. Перша команда, NDRangeKernel (ND - N-Dimension), є командою запуску ядра. У ній вказується розмірність і розміри глобального простору індексів (2, global_work_size - Двовимірна область розмірами L × N), для кожного з яких буде запущена нитка, розміри локальних груп ниток (local_work_size - 16 × 16).

Після постановки обчислення в чергу управління повертається в основну програму, і саме обчислення відбувається відносно цього основного потоку керування асинхронно.

Наступна команда, яка поміщається в чергу - це ReadBuffer. Сенс її зрозумілий, а уваги заслуговує третій аргумент виклику: CL_TRUE. Він говорить про те, що OpenCL повинен дочекатися виконання цієї операції читання перед тим, як повернути управління в основну програму.

При цьому, обидві команди, що записані в чергу, записуються туди без будь-якої інформації про події (останні три параметри у виклику по нулях: 0, NULL, NULL). Для OpenCL це означає те, що виконувати ці команди потрібно в тому порядку, в якому вони були поміщені в чергу. Тобто, ReadBuffer виконається після NDRangeKernel, після чого управління буде повернуто в основну програму, в якій можна буде використовувати дані з mem_R. Що й потрібно нам.

Опишемо тепер клас «Операції з векторами» .

Фрагмент коду 3.12. Змінні класу.

1. class VectorGPU {

2. private:

3. cl_context cxGPUContext;

4. cl_command_queue cqCommandQueue;

5. cl_platform_id cpPlatform;

6. cl_device_id cdDevice;

6. cl_program cpProgram;

7. cl_kernel ckKernel;

8. cl_mem cmDevSrcA;

9. cl_mem cmDevSrcB;

10. cl_mem cmDevDst;

В першій строчці об`являємо клас VectorGPU. Рядки 3-10, йде ініціалізація пристроя, платформи, ядра, тобто поля класу.

Фрагмент коду 3.13. Змінні класу.

1. size_t szGlobalWorkSize;

2. size_t szLocalWorkSize;

3. size_t szParmDataBytes;

4. size_t szKernelLength;

5. cl_int ciErr1, ciErr2;

6. char* cPathAndName;

7. char* cSourceCL;

Ініціалізується пам'ять яку будемо використовувати(1-7). Теж поля класу.

Фрагмент коду 3.14. Об`явлення методів.

1. public:

2. int n;

3. void *srcA, *srcB, *dst;

4. VectorGPU();

5. VectorGPU(int size);

6. ~VectorGPU();

7. int ClInit();

8. int CLAllocMem (int iNumElements);

9. int ClCreateProg(char *funcname, int iNumElements);

10. int ClWriteDataGPU();

11. int ClCompute();

12. int ClReadDataGPU();

13. unsigned int VAdd();

14. unsigned int VSub();

15. unsigned int MulSc(float scalar);

};

Методи, які будемо використовуються в даній програмі: додавання векторів, множення , віднімання, методи запису в пам'ять, методи читання с пам’яті, і таке інше.

Метод додавання векторів.

Фрагмент коду 3.15. Реалізація методу додавання.

1. unsigned int VectorGPU::VAdd()

{

2. unsigned int t1,t2;

3. if (ClCreateProg("VectorAdd",n)==1)

{

4. t1=GetTickCount();

5. if (ClWriteDataGPU()==1)

{

6. if (ClCompute()==1)

{

7. t2=GetTickCount();

}

8. if (ClReadDataGPU()==1)/

{

}

}

}

9. return ((int)(t2-t1));

}

В фрагменті коду 3.15. Описується метод додавання. Об`ява змінних які будуть повідомлювати, що операція додавання векторів завершилась.

Метод множення на скаляр.

Фрагмент коду 3.16. Метод множення на скаляр.

1. unsigned int VectorGPU::MulSc(float scalar)

{

2. unsigned int t1,t2;

3. float *b=(float*) srcB;

4. b[0]=scalar;

5. if (ClCreateProg("VectorMulSc",n)==1)

{

6. t1=GetTickCount();

7. if (ClWriteDataGPU()==1)

{

8. if (ClCompute()==1)

{

9. t2=GetTickCount();

}

10. if (ClReadDataGPU()==1)

{

}

}

}

11. return ((int)(t2-t1));

}

Фрагмент коду 3.16. реалізація множення вектора на скаляр. Отримуємо вхідний параметр наший скаляр, і передаємо його в наше ядро. В кінці нам буде показано час виконання операції.

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]