Вычисление максимальных флопов графического процессора с использованием OpenCL

Я пишу простое приложение OpenCL, которое будет вычислять максимальный экспериментальный FLOPS целевого устройства с графическим процессором. Я решил сделать ядро ​​cl максимально простым. Вот мое ядро ​​OpenCL и мой хост-код. Код ядра:

__kernel void flops(__global float *data) {

  int gid = get_global_id(0);
  double s = data[gid];
  data[gid] = s * 0.35;
}

И код хоста:

#include <iostream>
#include <sstream>
#include <stdlib.h>
#include <string.h>
#include <math.h>
#include "support.h"
#include "Event.h"
#include "ResultDatabase.h"
#include "OptionParser.h"
#include "ProgressBar.h"

using namespace std;

std::string kernels_folder = "/home/users/saman/shoc/src/opencl/level3/FlopsFolder/";
std::string kernel_file = "flops.cl";

static const char *opts = "-cl-mad-enable -cl-no-signed-zeros "
  "-cl-unsafe-math-optimizations -cl-finite-math-only";

cl_program createProgram (cl_context context,
                          cl_device_id device,
                          const char* fileName) {
  cl_int errNum;
  cl_program program;

  std::ifstream kernelFile (fileName, std::ios::in);
  if (!kernelFile.is_open()) {
    std::cerr << "Failed to open file for reading: " << fileName << std::endl;
  }

  std::ostringstream oss;
  oss << kernelFile.rdbuf();

  std::string srcStdStr = oss.str();
  const char *srcStr = srcStdStr.c_str();
  program = clCreateProgramWithSource (context, 1, (const char **)&srcStr,
                                       NULL, &errNum);

        CL_CHECK_ERROR(errNum);

  errNum = clBuildProgram (program, 0, NULL, NULL, NULL, NULL);
  CL_CHECK_ERROR (errNum);

  return program;
}

bool createMemObjects (cl_context context, cl_command_queue queue,
                       cl_mem* memObject,
                       const int memFloatsSize, float *a) {

        cl_int err;
  *memObject = clCreateBuffer (context, CL_MEM_READ_WRITE,
                              memFloatsSize * sizeof(float), NULL, &err);
        CL_CHECK_ERROR(err);

  if (*memObject == NULL) {
    std::cerr << "Error creating memory objects. " << std::endl;
    return false;
  }

  Event evWrite("write");
        err = clEnqueueWriteBuffer (queue, *memObject, CL_FALSE, 0, memFloatsSize * sizeof(float),
                        a, 0, NULL, &evWrite.CLEvent());
  CL_CHECK_ERROR(err);
        err = clWaitForEvents (1, &evWrite.CLEvent());
  CL_CHECK_ERROR(err);

  return true;

}

void cleanup (cl_context context, cl_command_queue commandQueue,
              cl_program program, cl_kernel kernel, cl_mem memObject) {

  if (memObject != NULL)
                clReleaseMemObject (memObject);

  if (kernel != NULL)
    clReleaseKernel (kernel);

  if (program != NULL)
    clReleaseProgram (program);

}

void addBenchmarkSpecOptions(OptionParser &op) {

}
void RunBenchmark(cl_device_id id,
                  cl_context ctx,
                  cl_command_queue queue,
                  ResultDatabase &resultDB,
                  OptionParser &op)
{

  for (float i = 0.1; i <= 0.2; i+=0.1 ) {
    std::cout << "Deploying " << 100*i << "%" << std::endl;
                bool verbose = false;

                cl_int errNum;

        cl_program program = 0;
        cl_kernel kernel;
        cl_mem memObject = 0;

        char maxFloatsStr[128];
    char testStr[128];
                program = createProgram (ctx, id, (kernels_folder + kernel_file).c_str());
                if (program == NULL) {
        exit (0);
        }

        if (verbose) std::cout << "Program created successfully!" << std::endl;

        kernel = clCreateKernel (program, "flops", &errNum);
        CL_CHECK_ERROR(errNum);

        if (verbose) std::cout << "Kernel created successfully!" << std::endl;
        // Identify maximum size of the global memory on the device side
                cl_long maxAllocSizeBytes = 0;
        cl_long maxComputeUnits = 0;
        cl_long maxWorkGroupSize = 0;
        clGetDeviceInfo (id, CL_DEVICE_MAX_MEM_ALLOC_SIZE,
                         sizeof(cl_long), &maxAllocSizeBytes, NULL);
        clGetDeviceInfo (id, CL_DEVICE_MAX_COMPUTE_UNITS,
                         sizeof(cl_long), &maxComputeUnits, NULL);
        clGetDeviceInfo (id, CL_DEVICE_MAX_WORK_GROUP_SIZE,
                         sizeof(cl_long), &maxWorkGroupSize, NULL);

                // Let's use 80% of this memory for transferring data
        cl_long maxFloatsUsageSize = ((maxAllocSizeBytes / 4) * 0.8);

        if (verbose) std::cout << "Max floats usage size is " << maxFloatsUsageSize << std::endl;
        if (verbose) std::cout << "Max compute unit is " << maxComputeUnits << std::endl;
        if (verbose) std::cout << "Max Work Group size is " << maxWorkGroupSize << std::endl;

        // Prepare buffer on the host side
        float *a = new float[maxFloatsUsageSize];
        for (int j = 0; j < maxFloatsUsageSize; j++) {
        a[j] = (float) (j % 77);
        }
        if (verbose) std::cout << "Host buffer been prepared!" << std::endl;
        // Creating buffer on the device side
        if (!createMemObjects(ctx, queue, &memObject, maxFloatsUsageSize, a)) {
        exit (0);
        }

        errNum = clSetKernelArg (kernel, 0, sizeof(cl_mem), &memObject);
                CL_CHECK_ERROR(errNum);

        size_t wg_size, wg_multiple;
        cl_ulong local_mem, private_usage, local_usage;
        errNum = clGetKernelWorkGroupInfo (kernel, id,
                                         CL_KERNEL_WORK_GROUP_SIZE,
                                         sizeof (wg_size), &wg_size, NULL);
        CL_CHECK_ERROR (errNum);
        errNum = clGetKernelWorkGroupInfo (kernel, id,
                                         CL_KERNEL_PREFERRED_WORK_GROUP_SIZE_MULTIPLE,
                                         sizeof (wg_multiple), &wg_multiple, NULL);
        CL_CHECK_ERROR (errNum);
        errNum = clGetKernelWorkGroupInfo (kernel, id,
                                         CL_KERNEL_LOCAL_MEM_SIZE,
                                         sizeof (local_usage), &local_usage, NULL);
        CL_CHECK_ERROR (errNum);
        errNum = clGetKernelWorkGroupInfo (kernel, id,
                                         CL_KERNEL_PRIVATE_MEM_SIZE,
                                         sizeof (private_usage), &private_usage, NULL);
        CL_CHECK_ERROR (errNum);
        if (verbose) std::cout << "Work Group size is " << wg_size << std::endl;
        if (verbose) std::cout << "Preferred Work Group size is " << wg_multiple << std::endl;
        if (verbose) std::cout << "Local memory size is " << local_usage << std::endl;
                if (verbose) std::cout << "Private memory size is " << private_usage << std::endl;


        size_t globalWorkSize[1] = {maxFloatsUsageSize};
        size_t localWorkSize[1] = {1};

        Event evKernel("flops");
        errNum = clEnqueueNDRangeKernel (queue, kernel, 1, NULL,
                                       globalWorkSize, localWorkSize,
                                       0, NULL, &evKernel.CLEvent());
                CL_CHECK_ERROR (errNum);
        if (verbose) cout << "Waiting for execution to finish ";
        errNum = clWaitForEvents(1, &evKernel.CLEvent());
        CL_CHECK_ERROR(errNum);
        evKernel.FillTimingInfo();
        if (verbose) cout << "Kernel execution terminated successfully!" << std::endl;
                delete[] a;

        sprintf (maxFloatsStr, "Size: %d", maxFloatsUsageSize);
    sprintf (testStr, "Flops: %f\% Memory", 100*i);
        double flopCount = maxFloatsUsageSize * 16000;
        double gflop = flopCount / (double)(evKernel.SubmitEndRuntime());
                resultDB.AddResult (testStr, maxFloatsStr, "GFLOPS", gflop);

        // Now it's time to read back the data
                a = new float[maxFloatsUsageSize];
                errNum = clEnqueueReadBuffer(queue, memObject, CL_TRUE, 0, maxFloatsUsageSize*sizeof(float), a, 0, NULL, NULL);
        CL_CHECK_ERROR(errNum);
    if (verbose) {
                        for (int j = 0; j < 10; j++) {
                std::cout << a[j] << " ";
                }
    }

    delete[] a;
    if (memObject != NULL)
      clReleaseMemObject (memObject);
    if (program != NULL)
      clReleaseProgram (program);
    if (kernel != NULL)
      clReleaseKernel (kernel);
  }
        std::cout << "Program executed successfully!" << std::endl;

}

Объясняя код, в коде ядра я фактически выполняю одну операцию с плавающей запятой, что означает, что каждая отдельная задача будет выполняться на FOPS. В хост-коде я сначала получаю максимальный размер глобальной памяти графического процессора, выделяю ее часть (цикл for определяет ее объем), затем помещаю в нее данные и выполнение ядра. Я измерю время выполнения clEnqueueNDRangeKernel, а затем рассчитаю GFLOPS приложения. В моей текущей реализации, независимо от размера cl_mem, я получаю около 0,28 GFLOPS производительности, что намного меньше заявленной мощности. Я предполагаю, что делаю определенные вещи здесь неэффективно. Или вообще моя методика расчета производительности GPU неверна. Кто-нибудь может сказать, какие изменения я должен внести в код?


person saman    schedule 11.04.2017    source источник
comment
Доступ к памяти осуществляется по 8 байт на рабочий элемент, что означает, что пропускная способность 8x0,28=2,24 ГБ/с является узким местом. Для iGPU это может быть узким местом внутреннего соединения (pci-e с небольшим количеством линий). Тогда вы запускаете ядро ​​​​только один раз. Обычно он начинается очень медленно, но последовательно становится намного быстрее с оптимизацией компилятора. Запустите хотя бы 10 раз. Если он увеличится до 3-5 GFLOP, то это должен быть младший графический процессор с узким местом в основной памяти.   -  person huseyin tugrul buyukisik    schedule 11.04.2017
comment
Я в основном запускаю его на Tesla K40m.   -  person saman    schedule 12.04.2017
comment
если тест принимает все как тест, накладные расходы на копирование массива скрывают реальные тайминги выполнения ядра.   -  person huseyin tugrul buyukisik    schedule 12.04.2017
comment
Именно поэтому я не считаю накладные расходы на копирование данных производительностью выполнения. Я замеряю время начала перед clEnqueueNDRangeKernel, а затем замеряю время окончания сразу после успешного завершения этой функции!   -  person saman    schedule 12.04.2017
comment
что это было за время? меньше миллисекунды?   -  person huseyin tugrul buyukisik    schedule 12.04.2017
comment
Кажется, это занимает около 2,104 секунды.   -  person saman    schedule 12.04.2017
comment
одноядерное выполнение? Запустите ndrangekernel 10-100 раз   -  person huseyin tugrul buyukisik    schedule 12.04.2017


Ответы (1)


  1. При размере локальной группы 1 вы тратите впустую 31/32 ресурсов (таким образом, вы можете иметь максимум 1/32 пиковой производительности). Вам нужен размер локальной группы не менее 32 (и кратен 32), чтобы полностью использовать вычислительные ресурсы, и 64, чтобы достичь 100% занятости (хотя 100% занятость не требуется).

  2. Доступ к памяти имеет высокую задержку и низкую пропускную способность. Ваше ядро ​​всегда будет ждать контроллеров памяти, если все остальное в порядке. Вам нужно выполнить больше арифметических операций, чтобы ALU был занят.

  3. Сначала вам нужно прочитать документ и использовать Visual Profiler. В предыдущих двух частях я просто хочу сказать, что все гораздо страннее, чем вы думали. Но нас ждут более странные вещи.


Вы можете легко достичь максимальной производительности на процессоре с помощью языка ассемблера (выполняя только независимые арифметические операции. Если вы напишете такой код на C, он просто будет удален компилятором). NVidia предоставляет нам только интерфейс IL под названием PTX, и я не уверен, что компилятор его оптимизирует. И я думаю, что вы можете использовать PTX только в CUDA.

редактировать: кажется, что компилятор оптимизирует неиспользуемый код PTX, по крайней мере, во встроенных ассемблерах.

person BlueWanderer    schedule 12.04.2017