Реализация сложения и умножения матриц в SYCL

Я пытаюсь реализовать сложение матриц и умножение в sycl в рамках одной программы, но получаю сообщение об ошибке в части сложения [нет жизнеспособного перегруженного оператора [] для типа 'const]. Я не знаю причину ошибки. Было бы здорово помочь, если бы кто-нибудь и сказал мне причину. Спасибо

Я реализовал как сложение, так и умножение отдельно, и это работает. Я думаю, причина в том, что я использую шаблон для умножения, и это создает проблему для добавления части.

#include <CL/sycl.hpp>
#include <chrono>
#include <cmath>
#include <ctime>
#include <iostream>

using namespace cl::sycl;

class mxm_kernel;

void display_matrix(float* m, int matSize) {
if (matSize > 16) {
    return;
}

std::cout << "=======" << std::endl;
for (int i = 0; i < matSize; i++) {
    for (int j = 0; j < matSize; j++) {
        std::cout << m[i * matSize + j] << " ";
    }
    std::cout << std::endl;
}
std::cout << "=======" << std::endl;
;
}

inline int prevPowerOfTwo(int x) {
if (x < 0) {
    return 0;
}
--x;
x |= x >> 1;
x |= x >> 2;
x |= x >> 4;
x |= x >> 8;
x |= x >> 16;
return x - (x >> 1);
}

inline bool isPowerOfTwo(int x) {
return (x & (x - 1)) == 0;
}

template <typename T>
bool local_mxm(cl::sycl::queue& q, T* MA, T* MB, T* MC, T* MD, int matSize) {
// Make sure it is power of two before running
if (!isPowerOfTwo(matSize)) {
    std::cout << " This example only works with power of two sizes "
        << std::endl;
    return true;
}

auto device = q.get_device();
auto maxBlockSize =
    device.get_info<cl::sycl::info::device::max_work_group_size>();
auto blockSize = prevPowerOfTwo(std::sqrt(maxBlockSize));
std::cout << " The Device Max Work Group Size is : " << maxBlockSize
    << std::endl;
std::cout << " The order is : " << matSize << std::endl;
std::cout << " The blockSize is : " << blockSize << std::endl;
// Make sure the block size is not larger than the mat size
blockSize = std::min(matSize, blockSize);

{
    range<1> dimensions(matSize * matSize);
    const property_list props = { property::buffer::use_host_ptr() };
    buffer<T> bA(MA, dimensions, props);
    buffer<T> bB(MB, dimensions, props);
    buffer<T> bC(MC, dimensions, props);
    buffer<T> bD(MD, dimensions, props);

    q.submit([&](handler& cgh) {
        auto pA = bA.template get_access<access::mode::read>(cgh);
        auto pB = bB.template get_access<access::mode::read>(cgh);
        auto pC = bC.template get_access<access::mode::write>(cgh);
        auto pD = bD.template get_access<access::mode::write>(cgh);
        auto localRange = range<1>(blockSize * blockSize);

        accessor<T, 1, access::mode::read_write, access::target::local> pBA(
            localRange, cgh);
        accessor<T, 1, access::mode::read_write, access::target::local> pBB(
            localRange, cgh);

        cgh.parallel_for<class matrix_add>(range<2> {matSize, matSize}, [=](id<2> it) {
            pD[it] = pA[it] + pB[it];
        });

        cgh.parallel_for<mxm_kernel>(
            nd_range<2>{range<2>(matSize, matSize),
            range<2>(blockSize, blockSize)},
            [=](nd_item<2> it) {
            // Current block
            int blockX = it.get_group(0);
            int blockY = it.get_group(1);

            // Current local item
            int localX = it.get_local_id(0);
            int localY = it.get_local_id(1);

            // Start in the A matrix
            int a_start = matSize * blockSize * blockY;
            // End in the b matrix
            int a_end = a_start + matSize - 1;
            // Start in the b matrix
            int b_start = blockSize * blockX;

            // Result for the current C(i,j) element
            T tmp = 0.0f;
            // We go through all a, b blocks
            for (int a = a_start, b = b_start; a <= a_end;
                a += blockSize, b += (blockSize * matSize)) {
                // Copy the values in shared memory collectively
                pBA[localY * blockSize + localX] =
                    pA[a + matSize * localY + localX];
                // Note the swap of X/Y to maintain contiguous access
                pBB[localX * blockSize + localY] =
                    pB[b + matSize * localY + localX];
                it.barrier(access::fence_space::local_space);
                // Now each thread adds the value of its sum
                for (int k = 0; k < blockSize; k++) {
                    tmp +=
                        pBA[localY * blockSize + k] * pBB[localX * blockSize + k];
                }
                // The barrier ensures that all threads have written to local
                // memory before continuing
                it.barrier(access::fence_space::local_space);
            }
            auto elemIndex = it.get_global_id(1) * it.get_global_range()[0] +
                it.get_global_id(0);
            // Each thread updates its position
            pC[elemIndex] = tmp;
        });
    });
}
return false;
}

int main(int argc, char* argv[]) {
float* MA;
float* MB;
float* MC;
float* MD;
bool sycl = true;
bool error = false;

int matSize = 4;
MA = new float[matSize * matSize];
MB = new float[matSize * matSize];
MC = new float[matSize * matSize];
MD = new float[matSize * matSize];



std::cout << " Input matrix " << std::endl;
display_matrix(MA, matSize);
display_matrix(MB, matSize);
display_matrix(MC, matSize);
display_matrix(MD, matSize);



if (sycl) {
    std::cout << " ***** SYCL " << std::endl;
    // MatrixC initialization
    std::cout << "MATRIX D" << std::endl;
    for (int i = 0; i < matSize; i++) {
        for (int j = 0; j < matSize; j++) {
            MD[i * matSize + j] = 0.0f;  // i * matSize + j;
            std::cout << MD[i * matSize + j] << " ";
        }
        std::cout << std::endl;
    }
    std::cout << "=======" << std::endl;
    // MatrixC initialization
    std::cout << "MATRIX C" << std::endl;
    for (int i = 0; i < matSize; i++) {
        for (int j = 0; j < matSize; j++) {
            MC[i * matSize + j] = 0.0f;  // i * matSize + j;
            std::cout << MC[i * matSize + j] << " ";
        }
        std::cout << std::endl;
    }
    std::cout << "=======" << std::endl;
    // MatrixA initialization
    std::cout << "MATRIX A" << std::endl;
    for (int i = 0; i < matSize; i++) {
        for (int j = 0; j < matSize; j++) {
            MA[i * matSize + j] = 0.0f + j;  // i * matSize + j;
            std::cout << MA[i * matSize + j] << " ";
        }
        std::cout << std::endl;
    }
    std::cout << "=======" << std::endl;
    // MatrixB initialization
    std::cout << "MATRIX B" << std::endl;
    for (int i = 0; i < matSize; i++) {
        for (int j = 0; j < matSize; j++) {
            MB[i * matSize + j] = 0.0f + j;  // i * matSize + j;
            std::cout << MB[i * matSize + j] << " ";
        }
        std::cout << std::endl;
    }
    std::cout << "=======" << std::endl;
    {
        {
            cpu_selector device_selector;
            queue q(device_selector);


            auto start = std::chrono::steady_clock::now();
            error = local_mxm(q, MA, MB, MC, MD, matSize);
            q.wait_and_throw();
            auto end = std::chrono::steady_clock::now();
            auto time =
                std::chrono::duration_cast<std::chrono::milliseconds>(end - start)
                .count();
            std::cout << "SYCL: ";
            std::cout << "Time: " << time << std::endl;
            float flops =
                (2.0f * matSize * matSize * matSize / (time / 1000.0f)) * 1.0e-9f;
            std::cout << "GFLOPs: " << flops << std::endl;
            std::cout << " Output " << std::endl;
        }
        display_matrix(MC, matSize);
        display_matrix(MD, matSize);
    }
}

delete[] MA;
delete[] MB;
delete[] MC;

return error ? 1 : 0;
}

person ZSA    schedule 10.09.2019    source источник


Ответы (1)


Проблема здесь в том, что parallel_for вызовы, которые у вас есть в вашей «комбинированной» функции, находятся внутри одной отправки группы команд, что не допускается SYCL. Область действия одной группы команд (queue::submit(command_group_handler)) обрабатывает только одно ядро ​​в своем теле. Поэтому вы должны разделить свои parallel_for на две отдельные очереди. И просто чтобы убедиться, что то, что я говорю, достаточно ясно и соответствует вашим намерениям, да, вы можете оставить их в одной и той же области действия, то есть:

template <typename T>
bool local_mxm(cl::sycl::queue& q, T* MA, T* MB, T* MC, T* MD, int matSize) {
  ...
  // Command group submission for the matrix addition kernel
  ...
  // Command group submission for the matrix multiplication kernel
  ...
}

Добавление матрицы
Отправка группы команд для ядра добавления матрицы. Вот как будет выглядеть ваше matrix_add представление:

q.submit([&](handler& cgh) {
  auto pA = bA.template get_access<access::mode::read>(cgh);
  auto pB = bB.template get_access<access::mode::read>(cgh);
  // You don't need accessor (pC) to the buffer (bC) anymore
  auto pD = bD.template get_access<access::mode::write>(cgh);

  cgh.parallel_for<matrix_add>(
      range<1>{static_cast<size_t>(matSize * matSize)},
      [=](id<1> it) { pD[it] = pA[it] + pB[it]; });
});

Теперь обратите внимание, как я удалил ключевое слово class перед matrix_add при передаче в качестве аргумента шаблона в parallel_for. Это связано с тем, что вы (начиная с SYCL 1.2.1) должны предварительно объявить свои классы ядра или определить их как объекты функций, если хотите, но вы не можете получить их на ходу. Компилятору также необходимо, чтобы имена ядер были уникальными, поэтому рекомендуется сделать последнее, если вы собираетесь иметь несколько вызовов этих ядер в своей программе.

Далее идут перепутанные диапазоны или индексное пространство (в OpenCL NDRange относится к индексному пространству входных данных для приложений с параллельными данными).

Причина в том, что в коде умножения матриц (который должен быть из наших примеров в computecpp- sdk) индексное пространство имеет размер nd_range<2> с nd_item<2> (информация об идентификаторах и размерах рабочих элементов/групп) так, чтобы можно было реализовать заблокированный (или мозаичный) алгоритм умножения матриц, но все методы доступа являются одномерными. Правильные позиции выводятся с помощью какой-то хитрой арифметики указателей, но в любом случае вы ни в коем случае не могли бы сделать следующее:

// !kinda pseudo-code, so don't take it literally
accessor<T, 1> acc(...);
parallel_for<kernel>(range<2>{size,size}, [=](id<2> i) {
  acc[i] = i;
}

Обычно вы делаете следующее:

// !kinda pseudo-code, so don't take it literally
accessor<T, 1> acc(...);
parallel_for<kernel>(range<1>{size * size}, [=](id<1> i) {
  acc[i] = i;
}

Это потому, что вы хотите напрямую получить доступ к линейному идентификатору внутреннего указателя в случае сложения матрицы. И если бы ваши входные массивы были двумерными, вы бы поступили наоборот: range<2>{size,size}, [=](id<2> i)


Матричное умножение
Представление группы команд для ядра матричного умножения остается таким же, как и в исходном коде.

q.submit([&](handler& cgh) {
  auto pA = bA.template get_access<access::mode::read>(cgh);
  auto pB = bB.template get_access<access::mode::read>(cgh);
  auto pC = bC.template get_access<access::mode::write>(cgh);
  auto pD = bD.template get_access<access::mode::write>(cgh);
  auto localRange = range<1>(blockSize * blockSize);

  accessor<T, 1, access::mode::read_write, access::target::local> pBA(
      localRange, cgh);
  accessor<T, 1, access::mode::read_write, access::target::local> pBB(
      localRange, cgh);

 cgh.parallel_for<mxm_kernel>(
      nd_range<2>{range<2>(static_cast<size_t>(matSize),
                           static_cast<size_t>(matSize)),
                  range<2>(blockSize, blockSize)}, 
      [=](nd_item<2> it) {
        ... 
      });
});

Попробуйте прочитать код умножения заблокированных матриц и, возможно, поищите некоторые ресурсы, объясняющие алгоритм. Это довольно сложно, но я могу себе представить, что этому есть достойное объяснение. (возможно, в учебных материалах CUDA).

Надеюсь это поможет!

person Georgi Mirazchiyski    schedule 10.09.2019
comment
Большое спасибо за столь подробное объяснение, это было действительно полезно. Я обнаружил проблему использования одной группы команд для обоих и исправил ее, но я перепутал их диапазоны и пропустил указанный вами класс. Я внесу эти изменения. Спасибо - person ZSA; 10.09.2019
comment
Вы можете использовать иерархический параллелизм вместо использования низкоуровневых конструкций, таких как барьеры. - person Ronan Keryell; 11.10.2019
comment
@RonanKeryell здесь прав. Иерархический параллелизм возможен в SYCL и обеспечивает более чистую и удобную для понимания структуру ваших ядер. Примитивы синхронизации, такие как барьеры рабочих групп, будут неявно размещены для вас. - person Georgi Mirazchiyski; 16.10.2019