Обновить

Комментарии 14

А вот интересно: у меня сделаны самодельные нейронки и там в основе это самое матричное умножение (и для свёрток тоже) с использованием shared памяти на CUDA. Так вот, игрался я на 1070 ti и решил купить Tesla P100 потому как игры с памятью (банальная перестановка доступа к массиву) сильно влияют на быстродействие, а у Tesla P100 шина 4096 бит, и я полагал, что это сильно ускорит чтение/запись памяти (не shared). Купил. И ускорения практически не получил. И возникает вопрос, а где же эти 4096 бит тогда используют и где они дают существенный прирост производительности?

В моем понимании, для того чтобы воспользоваться преимуществом широкой шины надо что бы приложение могло за такт обеспечить требуемое количество запросов к памяти. Если доступ не сгруппированный то будет много отдельных транзакций которые забьют этот размер шины и пропускная способность упадёт. Или если SM недостаточно загружены, или много синхронизаций то тоже не получиться сформировать нужное количество запросов. Для более точного ответа нужно наверное начать с профилирования и определить узкие места.

Спасибо за статью, тема очень интересная.

Жалко, много фактических ошибок:

Если реализовывать это обычным способом для CPU, все выходит достаточно просто. Мы получаем три вложенных цикла, в которых перемножаем элементы и сохраняем результирующий элемент матрицы C:

void matmul(int M, int N, int K,
            const float* A, const float* const B, float* C) {
    for (int m = 0; m < M; m++)
        for (int n = 0; n < N; n++)
            for (int k = 0; k < K; k++) 
                C[m * M  + n] += A[m * M + k] * B[k * K + n];
}

Ладно матрица С не зануляется, так еще и индексация полностью неверна. Стоило запускать тесты на неквадратных матрицах, вероятно был бы сегфолт

Одному потоку еще доступна private memory или регистры в терминологии CUDA. Также они разделяют пространство памяти с кешем первого уровня: можно регулировать, сколько отойдет на регистры, сколько на кеш первого уровня.

Нет, регистровый файл стоит отдельно, его размер фиксирован. Настраивается только доля Shared Memory vs L1-cache.

Размер файла регистров ограничен для каждого стриминг-процессора. Например для A100 — 16384х32bit.

Это не для SM, а на partition (который вы называете streaming processor). Вот из спеки Ampere, отличие как раз в 4 раза:

  • The register file size is 64K 32-bit registers per SM.

И вот так выглядит код простого подхода для вычисления одного элемента матрицы C:

__kernel void naive_gemm(int m, int n, int k,
                         const __global float* a, 
                         const __global float* b,  
                        __global float* c) {
  const int global_row = get_global_id(0);
  const int global_col = get_global_id(1);
  float acc = 0.0f;
  for (int i = 0; i < k; ++i) {
    acc += a[i * m + global_row] * b[global_col * i + k];
  }
  c[global_col * m + global_row] = acc;
}

Опять беда с индексами, еще обязательно надо добавить гвард: if (global_row>=m || global_col>=n) return

По итогу дальнейшие сравнения бессмыслены -- умножается совсем не то, и паттерн доступа к памяти уже другой.

Хотелось бы еще увидеть методологию измерения перфа, на GPU это очень чувствительная тема. Без неё результаты остаются невоспроизводимыми. И результаты обычно приводят в GFLOP/s

Каждый поток во время выполнения будет дожидаться завершения всех операций c локальной памятью:

const int tiled_row = TS * t + row;
const int tiled_col = TS * t + col;

a_sub[col][row] = A[tiled_col * M + global_row];
b_sub[col][row] = B[global_col * K + tiled_row];
 
// синхронизируем что быть уверенным, что tile загружен
barrier(CLK_LOCAL_MEM_FENCE);
for (int k=0; k < TS; k++) {
    acc += a_sub[k][row] * b_sub[col][k];
}
 
// синхронизируем перед следующим tile
barrier(CLK_LOCAL_MEM_FENCE);

Здесь сильно запутанно: почему и зачем загрузка в shared memory происходит с транпонированием? В каком вообще формате входные матрицы (col- или row- major)?

Например, для доступа к 32-байтовым элементам одна транзакция будет размером 128 байт

32-битным элементам

Важно помнить о нюансах точности таких специальных ядер: они могут выполнять вычисления в половинной точности (FP16), что подходит не для всех задач.

Стоило упомянуть, что TensorOps умеют умножать матрицы и в TF32, что ускоряет умножение с достаточной для многих задач точностью

Все предложенные статьи в конце действительно очень интересные, спасибо за список

Спасибо за развернутый комментарий и найденные неточности. Я действительно допустил ошибки в индексации для простых реализаций. В более сложных все правильно. Также я специально опустил проверку размерностей для упрощения и уменьшения количества кода, т.е. примеры работают только с квадратными матрицами в формате column-major. С размером  регистрового файла и локальной памяти тоже было написано неверно. И под стриминг процессором я понимаю Streaming Processor(SP) а не Streaming Multiprocessor(SM), т.е. четвертую часть как вы и написали. Методика измерения производительности использовалась самая простая - просто N раз запустил и усреднил, конечно для серьезных измерений такое не подходит так есть много нюансов, включая плавающую частоту. Но думаю для демонстрации такой подход вполне приемлем учитывая разницу производительности в несколько порядков.
Еще раз спасибо что обратили внимание на ошибки, я внес изменения в статью.

Еще в статье не описаны многие переменные. Например, что такое TSK, K, RTSN, RTSM?

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

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

Так же не понятно, вот эта игра с регистрами, она выполняется после того, как потоки положили данные тайла в разделяемую память? То есть, это просто замена цикла сложений результатов умножения по строке и столбцу тайла?

Посмотрите пожалуйста исходный код проекта https://gitverse.ru/kolkir/clgemm думаю вы там найдете ответы. Вот описания констант:

#define TS 32
#define TSM 128            // The tile-size in dimension M
#define TSN 128            // The tile-size in dimension N
#define TSK 16             // The tile-size in dimension K
#define WPTM 8             // The amount of work-per-thread in dimension M
#define WPTN 8             // The amount of work-per-thread in dimension N
#define RTSM (TSM / WPTM)  // The reduced tile-size in dimension M (== number of threads)
#define RTSN (TSN / WPTN)  // The reduced tile-size in dimension N (== number of threads)
...

В файле optim_gemm.cc находится реализация ядра со всеми оптимизациями, в тут запуск ядер.

Спасибо, посмотрю.

Там же по сути всего лишь такая конструкция:

 static const size_t m_size=512;

 CTensor<type_t> cTensorA(1,m_size,m_size);
 CTensor<type_t> cTensorB(1,m_size,m_size);
 CTensor<type_t> cTensorC(1,m_size,m_size);

 //задаём матрицы

 for(size_t y=0;y<m_size;y++)
 {
  for(size_t x=0;x<m_size;x++)
  {
   cTensorA.SetElement(0,y,x,(x+y*m_size)%5);
   cTensorB.SetElement(0,y,x,(x*m_size+y)%5);
  }
 }

 //очищаем тензор результата
 cTensorC.Zero();

 //делаем умножение
 for(size_t y=0;y<cTensorC.GetSizeY();y++)
 {
  for(size_t x=0;x<cTensorC.GetSizeX();x++)
  {
   type_t line[m_size];//массив размера cTensorA.GetSizeX()=cTensorB.GetSizeY()
   //заполняем линию
   for(size_t k=0;k<m_size;k++) line[k]=cTensorB.GetElement(0,x,k);
   //берём элемент a
   type_t a=cTensorA.GetElement(0,y,x);
   //перемножаем с элементами b и складываем в выходном тензоре
   for(size_t k=0;k<m_size;k++)
   {
    type_t v=cTensorC.GetElement(0,y,k);
    v+=a*line[k];
    cTensorC.SetElement(0,y,k,v);
   }
  }
 }

При этом x и y внешнего цикла это как раз координаты потока CUDA внутри выходного тензора.

Наврал. :)

Вот так должно быть.

 static const size_t ma_size_x=512;
 static const size_t ma_size_y=384;

 static const size_t mb_size_x=128;
 static const size_t mb_size_y=ma_size_x;

 static const size_t mc_size_x=mb_size_x;
 static const size_t mc_size_y=ma_size_y;

 CTensor<type_t> cTensorA(1,ma_size_y,ma_size_x);
 CTensor<type_t> cTensorB(1,mb_size_y,mb_size_x);
 CTensor<type_t> cTensorC(1,mc_size_y,mc_size_x);
 
 //заполняем матрицы
 for(size_t y=0;y<ma_size_y;y++)
 {
  for(size_t x=0;x<ma_size_x;x++) cTensorA.SetElement(0,y,x,(x+y*ma_size_x)%5);
 }
 for(size_t y=0;y<mb_size_y;y++)
 {
  for(size_t x=0;x<mb_size_x;x++) cTensorB.SetElement(0,y,x,(x*mb_size_y+y)%5);
 }

 //очищаем тензор результата
 cTensorC.Zero();
 //делаем умножение
 type_t line[mc_size_x];//размер выходного тензора по x
 for(size_t ax=0;ax<ma_size_x;ax++)
 {
  for(size_t k=0;k<mc_size_x;k++) line[k]=cTensorB.GetElement(0,ax,k);
  for(size_t ay=0;ay<ma_size_y;ay++)
  {
   //берём элемент a
   type_t a=cTensorA.GetElement(0,ay,ax);

   for(size_t k=0;k<mc_size_x;k++)
   {
    type_t v=cTensorC.GetElement(0,ay,k);
    v+=a*line[k];
    cTensorC.SetElement(0,ay,k,v);
   }
  }
 }

Очень странно. Вот если взять файл optim_gemm.cc и в файле запуска ядер глянуть параметры запуска

    lrange = cl::NDRange{TSM / WPTM, TSN / WPTN};
    grange = cl::NDRange{matrix_data.m / WPTM, matrix_data.n / WPTN};

То получится что-то странное для grange. Потому что в ядре написано так:

  const int tidm = get_local_id(0);            // Local row ID (max: TSM/WPTM == RTSM)
  const int tidn = get_local_id(1);            // Local col ID (max: TSN/WPTN == RTSN)
  const int offset_m = TSM * get_group_id(0);  // Work-group offset
  const int offset_n = TSN * get_group_id(1);  // Work-group offset

То есть, offset_m и offset_n дают смещение на сетке TSM и TSN. Вероятно, там всё-таки grange должно быть получено делением размеров матриц на эти самые TSM и TSN, соответственно. А так - WPTM=8. То есть, количество блоков при делении размера на 8 будет много больше, чем при делении на TSM=128. И быстродействие сильно просядет для матрицы 8192x8192.

А если адаптировать всё это для CUDA и попутно поменять все эти TSM и TSN в которых я путаюсь на удобные (мне) названия:

static const size_t TENSOR_OPERATION_TILE_SIZE_X=128;///<размер блока операций с тензорами X
static const size_t TENSOR_OPERATION_TILE_SIZE_Y=128;///<размер блока операций с тензорами Y
static const size_t TENSOR_OPERATION_TILE_SIZE_K=16;///<размер блока операций с тензорами K
static const size_t WORK_PER_TILE_SIZE_X=8;///<сколько элементов обрабатывает поток X
static const size_t WORK_PER_TILE_SIZE_Y=8;///<сколько элементов обрабатывает поток Y
static const size_t WORK_PER_TILE_X=TENSOR_OPERATION_TILE_SIZE_X/WORK_PER_TILE_SIZE_X;///<количество обрабатываемых блоков по WORK_PER_TILE_SIZE_X элементов
static const size_t WORK_PER_TILE_Y=TENSOR_OPERATION_TILE_SIZE_Y/WORK_PER_TILE_SIZE_Y;///<количество обрабатываемых блоков по WORK_PER_TILE_SIZE_Y элементов

//----------------------------------------------------------------------------------------------------
//функция CUDA для умножения тензоров
//----------------------------------------------------------------------------------------------------
template<class type_t,class kernel_output_t,class kernel_left_t,class kernel_right_t>
__global__ void CUDATensorMulTensorFunction(kernel_output_t tensor_output,kernel_left_t tensor_left,kernel_right_t tensor_right)
{
 const size_t tidm=threadIdx.y;// Local row ID (max: TSM/WPTM == RTSM)
 const size_t tidn=threadIdx.x;// Local col ID (max: TSN/WPTN == RTSN)
 const size_t offset_m=TENSOR_OPERATION_TILE_SIZE_Y*blockIdx.y;// Work-group offset
 const size_t offset_n=TENSOR_OPERATION_TILE_SIZE_X*blockIdx.x;// Work-group offset
 const size_t z=blockIdx.z;

 // Local memory to fit a tile of A and B
 __shared__ type_t a_sub[TENSOR_OPERATION_TILE_SIZE_K][TENSOR_OPERATION_TILE_SIZE_Y];
 __shared__ type_t b_sub[TENSOR_OPERATION_TILE_SIZE_X][TENSOR_OPERATION_TILE_SIZE_K+2];

 // Allocate register space
 type_t a_reg;
 type_t b_reg[WORK_PER_TILE_SIZE_X];
 type_t acc[WORK_PER_TILE_SIZE_Y][WORK_PER_TILE_SIZE_X];

 // Initialise the accumulation registers
 #pragma unroll
 for(size_t wm=0;wm<WORK_PER_TILE_SIZE_Y;wm++)
 {
  #pragma unroll
  for (size_t wn=0;wn<WORK_PER_TILE_SIZE_X;wn++) acc[wm][wn]=0;
 }

 // Loop over all tiles
 size_t numTiles=tensor_left.Size_X/TENSOR_OPERATION_TILE_SIZE_K;
 if (tensor_left.Size_X%(TENSOR_OPERATION_TILE_SIZE_K)) numTiles++;

 size_t LPTA=(TENSOR_OPERATION_TILE_SIZE_K*WORK_PER_TILE_SIZE_Y*WORK_PER_TILE_SIZE_X)/(TENSOR_OPERATION_TILE_SIZE_X);// The amount of loads-per-thread for A
 size_t LPTB=(TENSOR_OPERATION_TILE_SIZE_K*WORK_PER_TILE_SIZE_Y*WORK_PER_TILE_SIZE_X)/(TENSOR_OPERATION_TILE_SIZE_Y);// The amount of loads-per-thread for B

 size_t t=0;
 do
 {
  // Load one tile of A and B into local memory
  #pragma unroll
  for(size_t la=0;la<LPTA;la++)
  {
   size_t tid=tidn*WORK_PER_TILE_Y+tidm;
   size_t id=la*WORK_PER_TILE_X*WORK_PER_TILE_Y+tid;
   size_t col=id/TENSOR_OPERATION_TILE_SIZE_Y;
   size_t row=id-col*TENSOR_OPERATION_TILE_SIZE_Y;
   size_t tiled_index=TENSOR_OPERATION_TILE_SIZE_K*t+col;
   a_sub[col][row]=tensor_left.GetElement(z,offset_m+row,tiled_index);//A[tiled_index*M+offset_m+row];
   b_sub[row][col]=tensor_right.GetElement(z,offset_n+row,tiled_index);//B[tiled_index*N+offset_n+row];
  }
  // Synchronise to make sure the tile is loaded
  __syncthreads();

  // Loop over the values of a single tile
  for(size_t k=0;k<TENSOR_OPERATION_TILE_SIZE_K;k++)
  {
   // Cache the values of b_sub in registers
   #pragma unroll
   for(size_t wn=0;wn<WORK_PER_TILE_SIZE_X;wn++)
   {
    size_t col=tidn+wn*WORK_PER_TILE_X;
    b_reg[wn]=b_sub[col][k];
   }
   // Perform the computation
   #pragma unroll
   for(size_t wm=0;wm<WORK_PER_TILE_SIZE_Y;wm++)
   {
    size_t row=tidm+wm*WORK_PER_TILE_Y;
    a_reg=a_sub[k][row];
    #pragma unroll
    for(size_t wn=0;wn<WORK_PER_TILE_SIZE_X;wn++) acc[wm][wn]+=a_reg*b_reg[wn];
   }
  }
  // Synchronise before loading the next tile
  __syncthreads();
 // Next tile
  t++;
 }
 while(t<numTiles);

 // Store the final results in C
 #pragma unroll
 for(size_t wm=0;wm<WORK_PER_TILE_SIZE_Y;wm++)
 {
  size_t global_row=offset_m+tidm+wm*WORK_PER_TILE_Y;
  #pragma unroll
  for(size_t wn=0;wn<WORK_PER_TILE_SIZE_X;wn++)
  {
   size_t global_col=offset_n+tidn+wn*WORK_PER_TILE_X;
   tensor_output.SetElement(z,global_row,global_col,acc[wm][wn]);
  }
 }
}

//----------------------------------------------------------------------------------------------------
//умножить тензоры
//----------------------------------------------------------------------------------------------------
template<class type_t,class kernel_output_t,class kernel_left_t,class kernel_right_t>
__host__ void MulAbstract(CTensor<type_t> &cTensor_Output,kernel_output_t &sTensorKernel_Output,const CTensor<type_t> &cTensor_Left,kernel_left_t &sTensorKernel_Left,const CTensor<type_t> &cTensor_Right,kernel_right_t &sTensorKernel_Right)
{
 if (sTensorKernel_Left.Size_X!=sTensorKernel_Right.Size_Y  || sTensorKernel_Left.Size_Z!=sTensorKernel_Right.Size_Z ||
     sTensorKernel_Output.Size_Y!=sTensorKernel_Left.Size_Y || sTensorKernel_Output.Size_X!=sTensorKernel_Right.Size_X ||
     sTensorKernel_Output.Size_Z!=sTensorKernel_Right.Size_Z)
 {
  throw "CTensor::MulAbstract: Размерности тензоров не совпадают!";
 }
 //копируем данные с процессора
 cTensor_Left.CopyToDevice();
 cTensor_Right.CopyToDevice();

 dim3 thread(WORK_PER_TILE_X,WORK_PER_TILE_Y);
 
 size_t block_x=sTensorKernel_Right.Size_X/(TENSOR_OPERATION_TILE_SIZE_X);
 size_t block_y=sTensorKernel_Left.Size_Y/(TENSOR_OPERATION_TILE_SIZE_Y);
 size_t block_z=sTensorKernel_Output.Size_Z;

 dim3 blocks(block_x,block_y,block_z);
 if (blocks.x==0) blocks.x=1;
 if (blocks.y==0) blocks.y=1;
 if (blocks.z==0) blocks.z=1;

 CUDATensorMulTensorFunction<type_t,kernel_output_t,kernel_left_t,kernel_right_t><<<blocks,thread>>>(sTensorKernel_Output,sTensorKernel_Left,sTensorKernel_Right);
 HANDLE_ERROR(cudaGetLastError());
 HANDLE_ERROR(cudaDeviceSynchronize());

 cTensor_Output.SetDeviceOnChange();
}

Ядро тензора для CUDA:

//****************************************************************************************************
///!структура ядра тензора
//****************************************************************************************************
template<class type_t>
struct STensorKernel
{
 size_t Size_X;///<размер по x
 size_t Size_Y;///<размер по y
 size_t Size_Z;///<размер по z
 size_t StrideX;///<строка по X
 size_t StrideZ;///<размер блока по Z
 type_t *TensorData_Ptr;///<указатель на данные тензора на стороне GPU

 __host__ __device__ STensorKernel(void)///<конструктор
 {
 }
 __host__ __device__ STensorKernel(const CTensor<type_t> &cTensor)///<конструктор
 {
  Set(cTensor);
 }

 __forceinline__ __host__ __device__ type_t* GetTensorDataPtr(size_t z)///<получить указатель на элементы с глубиной z
 {
  return(&TensorData_Ptr[z*StrideZ]);
 }

 __forceinline__ __host__ __device__ type_t GetElement(size_t z,size_t y,size_t x)
 {
  if (x>=Size_X || y>=Size_Y || z>=Size_Z) return(0);
  return(TensorData_Ptr[z*StrideZ+y*StrideX+x]);
 }
 __forceinline__ __host__ __device__ void SetElement(size_t z,size_t y,size_t x,type_t value)
 {
  if (x>=Size_X || y>=Size_Y || z>=Size_Z) return;
  TensorData_Ptr[z*StrideZ+y*StrideX+x]=value;
 }

 __forceinline__ __host__ __device__ size_t GetSizeX(void) const
 {
  return(Size_X);
 }

 __forceinline__ __host__ __device__ size_t GetSizeY(void) const
 {
  return(Size_Y);
 }

 __forceinline__ __host__ __device__ size_t GetSizeZ(void) const
 {
  return(Size_Z);
 }

 __host__ __device__ void Reset(void)
 {
  Size_X=0;
  Size_Y=0;
  Size_Z=0;
  StrideX=0;
  StrideZ=0;
  TensorData_Ptr=NULL;
 }

 __host__ __device__ void Set(const CTensor<type_t> &cTensor)
 {
  Size_Z=cTensor.Size_Z;
  Size_Y=cTensor.Size_Y;
  Size_X=cTensor.Size_X;
  StrideX=cTensor.Size_X;
  StrideZ=cTensor.Size_X*cTensor.Size_Y;
  TensorData_Ptr=cTensor.DeviceItem.get();
 }
};

И мой текущий вариант умножения:

static const size_t TENSOR_OPERATION_BLOCK_SIZE_SCALE=2;///<размер множителя блока операций с тензорами
static const size_t TENSOR_OPERATION_BLOCK_SIZE=16;///<размер блока операций с тензорами
static const size_t TENSOR_OPERATION_TILE_SIZE=TENSOR_OPERATION_BLOCK_SIZE*TENSOR_OPERATION_BLOCK_SIZE_SCALE;
//----------------------------------------------------------------------------------------------------
//функция CUDA для умножения тензоров
//----------------------------------------------------------------------------------------------------
template<class type_t,class kernel_output_t,class kernel_left_t,class kernel_right_t>
__global__ void CUDATensorMulTensorFunction(kernel_output_t tensor_output,kernel_left_t tensor_left,kernel_right_t tensor_right)
{
 //блок TENSOR_OPERATION_BLOCK_SIZE x TENSOR_OPERATION_BLOCK_SIZE в выходном тензоре
 size_t block_x=blockIdx.x;
 size_t block_y=blockIdx.y;
 //координаты элементов блока в выходном тензоре
 size_t out_in_block_x=threadIdx.x*TENSOR_OPERATION_BLOCK_SIZE_SCALE;
 size_t out_in_block_y=threadIdx.y*TENSOR_OPERATION_BLOCK_SIZE_SCALE;
 //координаты блока в выходном тензоре
 size_t out_block_x=TENSOR_OPERATION_TILE_SIZE*block_x;
 size_t out_block_y=TENSOR_OPERATION_TILE_SIZE*block_y;
 //глобальные координаты в выходном тензоре
 size_t out_x=out_in_block_x+out_block_x;
 size_t out_y=out_in_block_y+out_block_y;
 size_t out_z=blockIdx.z;

 //получаем подматрицу выходной матрицы
 type_t Cvalue[TENSOR_OPERATION_BLOCK_SIZE_SCALE][TENSOR_OPERATION_BLOCK_SIZE_SCALE];
 for(size_t ky=0;ky<TENSOR_OPERATION_BLOCK_SIZE_SCALE;ky++)
 {
  for(size_t kx=0;kx<TENSOR_OPERATION_BLOCK_SIZE_SCALE;kx++)
  {
   Cvalue[ky][kx]=0;
  }
 }

 //считаем, сколькно нужно проходов блоком по X
 size_t m_max=tensor_left.Size_X/(TENSOR_OPERATION_TILE_SIZE);
 if (tensor_left.Size_X%(TENSOR_OPERATION_TILE_SIZE)) m_max++;

 __shared__ type_t As[TENSOR_OPERATION_TILE_SIZE][TENSOR_OPERATION_TILE_SIZE];
 __shared__ type_t Bs[TENSOR_OPERATION_TILE_SIZE][TENSOR_OPERATION_TILE_SIZE];

 for(size_t m=0;m<m_max;m++)
 {
  size_t offset=m*TENSOR_OPERATION_TILE_SIZE;
  size_t py_left=out_in_block_y+out_block_y;
  size_t py_right=out_in_block_y+offset;
  size_t py=out_in_block_y;
  for(size_t ky=0;ky<TENSOR_OPERATION_BLOCK_SIZE_SCALE;ky++,py_left++,py_right++,py++)
  {
   size_t px_left=out_in_block_x+offset;
   size_t px_right=out_in_block_x+out_block_x;
   size_t px=out_in_block_x;
   for(size_t kx=0;kx<TENSOR_OPERATION_BLOCK_SIZE_SCALE;kx++,px_left++,px_right++,px++)
   {
    As[py][px]=tensor_left.GetElement(out_z,py_left,px_left);
    Bs[py][px]=tensor_right.GetElement(out_z,py_right,px_right);
   }
  }
  __syncthreads();

  //выполняем умножение только для тех элементов, которые не выходят за размер выходного тензора
  if ((out_y<tensor_output.Size_Y) &&
      (out_x<tensor_output.Size_X))
  {
   for(size_t ky=0;ky<TENSOR_OPERATION_BLOCK_SIZE_SCALE;ky++)
   {
    for(size_t kx=0;kx<TENSOR_OPERATION_BLOCK_SIZE_SCALE;kx++)
    {
     type_t &cv=Cvalue[ky][kx];
     size_t ay=out_in_block_y+ky;
     size_t bx=out_in_block_x+kx;
     for(size_t e=0;e<TENSOR_OPERATION_TILE_SIZE;e++) cv+=As[ay][e]*Bs[e][bx];
    }
   }
  }
  __syncthreads();
 }

 size_t py=out_y;
 for(size_t ky=0;ky<TENSOR_OPERATION_BLOCK_SIZE_SCALE;ky++,py++)
 {
  size_t px=out_x;
  for(size_t kx=0;kx<TENSOR_OPERATION_BLOCK_SIZE_SCALE;kx++,px++)
  {
   tensor_output.SetElement(out_z,py,px,Cvalue[ky][kx]);
  }
 }
 __syncthreads();
}

//----------------------------------------------------------------------------------------------------
//умножить тензоры
//----------------------------------------------------------------------------------------------------
template<class type_t,class kernel_output_t,class kernel_left_t,class kernel_right_t>
__host__ void MulAbstract(CTensor<type_t> &cTensor_Output,kernel_output_t &sTensorKernel_Output,const CTensor<type_t> &cTensor_Left,kernel_left_t &sTensorKernel_Left,const CTensor<type_t> &cTensor_Right,kernel_right_t &sTensorKernel_Right)
{
 if (sTensorKernel_Left.Size_X!=sTensorKernel_Right.Size_Y  || sTensorKernel_Left.Size_Z!=sTensorKernel_Right.Size_Z ||
     sTensorKernel_Output.Size_Y!=sTensorKernel_Left.Size_Y || sTensorKernel_Output.Size_X!=sTensorKernel_Right.Size_X ||
     sTensorKernel_Output.Size_Z!=sTensorKernel_Right.Size_Z)
 {
  throw "CTensor::MulAbstract: Размерности тензоров не совпадают!";
 }
 //копируем данные с процессора
 cTensor_Left.CopyToDevice();
 cTensor_Right.CopyToDevice();
 
 dim3 thread(TENSOR_OPERATION_TILE_SIZE,TENSOR_OPERATION_TILE_SIZE);

 size_t block_x=sTensorKernel_Output.Size_X/thread.x;
 if (sTensorKernel_Output.Size_X%thread.x) block_x++;
 size_t block_y=sTensorKernel_Output.Size_Y/thread.y;
 if (sTensorKernel_Output.Size_Y%thread.y) block_y++;
 size_t block_z=sTensorKernel_Output.Size_Z;

 dim3 blocks(block_x,block_y,block_z);
 if (blocks.x==0) blocks.x=1;
 if (blocks.y==0) blocks.y=1;
 if (blocks.z==0) blocks.z=1;

 dim3 thread_basic(TENSOR_OPERATION_BLOCK_SIZE,TENSOR_OPERATION_BLOCK_SIZE);

 CUDATensorMulTensorFunction<type_t,kernel_output_t,kernel_left_t,kernel_right_t><<<blocks,thread_basic>>>(sTensorKernel_Output,sTensorKernel_Left,sTensorKernel_Right);
 HANDLE_ERROR(cudaGetLastError());
 HANDLE_ERROR(cudaDeviceSynchronize());

 cTensor_Output.SetDeviceOnChange();
}

Так вот, такая вот штука для матрицы 8192x8192 работает на I5-3570 так:

Тип 2 - это мой вариант. Тип 3 - это умножение на регистрах.

Вроде как 500 мс выиграны. Но я не уверен в правильности работы умножений на регистрах во всех размерностях матриц (неквадратные я не проверял ещё).

Завтра проверю на p106-100 (аналог 1060 с урезанной pci-e x1). Там вроде как результаты были противоположные. Вариант с регистрами дал 3700 мс, а мой вариант дал 933 мс. Проверю завтра. Может, что-то не так запускал.

Нет, всё-таки, похоже

grange = cl::NDRange{matrix_data.m / WPTM, matrix_data.n / WPTN};

Но тогда быстродействие просто в ноль уходит. То, что мой вариант делает за 0.11 мс, этот делает аж за 3.82. И это всего лишь небольшая матрица. С большой я ждал результата около минуты-двух. :O


А ещё вот тут b_sub[row][col]=tensor_right.GetElement(z,offset_n+row,tiled_index);//B[tiled_index*N+offset_n+row];

должно быть

b_sub[row][col]=tensor_right.GetElement(z,tiled_index,offset_n+row);//B[tiled_index*N+offset_n+row];

Смущает только tiled_index*N у B. Ведь матрица B задана как N по X и K по Y. Следовательно, точка (x,y) имеет смещение x*K+y (тут хранение массива как [y][x]); А в индексах x умножается на N, а не на K.

Это всё вообще работало у автора репозитория?

Вы в предыдущем посте правильно обратили внимание на момент с расчетом элементов в глобальном NDRange, дело в том, что в OpenCL global_range должен задавать общее количество элементов сетки, и local_range будет использоваться для определения количества групп. Т.е. не так как в CUDA когда задается количество блоков. Я действительно не упомянул про это в статье, сейчас добавил. Спасибо что обратили внимание. И у меня используются матрицы в column-major формате это немного запутывает индексацию.

То есть, число блоков будет отношением global к local?

Да, \text{work group instancies} = \frac{\text{global work size}}{\text{local work size}}, вот ссылка на спецификацию.

Спасибо.

Жаль только, что выигрыш появляется только на очень больших матрицах. Вот на 8192 выигрыш есть. А на 512 результат хуже простого тайлового разбиения, где поток обрабатывает квадрат 2x2.

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Информация

Сайт
yadro.com
Дата регистрации
Дата основания
Численность
5 001–10 000 человек
Местоположение
Россия
Представитель
Ульяна Соловьева