Я провел следующие тесты CUDA для сравнения показателей производительности умножения (квадратных) матриц, работающих на Ubuntu 24.04 с картой графического процессора Quadro T1000 Mobile с вычислительными возможностями 7.5 (Arch=SM_75) и драйвером графического процессора nvidia-driver. -535 .
Примечание
Конфигурация
Test Lib Cores
---- --- ----
mat_mul_custom cuda_runtime v12.0.1 cuda-cores
mat_mul_blas cublas_v2 v12.0.1 cuda-cores
mat_mul_tensor cutensor v2.0.2.4 dedicated-cuda-cores (non-tensor-cores)
n/a cutensor tensor-cores
Mat_mul_test.cu
#include <cuda_runtime.h>
#define STRIDE 1024
#define SUB_STRIDE 32
__global__ void mat_mul_custom_d(float*, float*, float*);
__global__ void mat_mul_custom_d(float* A, float* B, float* C) {
int by = blockIdx.y, bx = blockIdx.x, ty = threadIdx.y, tx = threadIdx.x;
int aBegin = by * SUB_STRIDE * STRIDE + ty * STRIDE + tx,
aEnd = aBegin + STRIDE, bBegin = SUB_STRIDE * bx + ty * STRIDE + tx,
bStep = SUB_STRIDE * STRIDE;
float sC = 0;
for (int a = aBegin, b = bBegin; a < aEnd; a += SUB_STRIDE, b += bStep) {
__shared__ float As[SUB_STRIDE][SUB_STRIDE], Bs[SUB_STRIDE][SUB_STRIDE];
As[ty][tx] = A[a];
Bs[ty][tx] = B[b];
__syncthreads();
for (int k = 0; k < SUB_STRIDE; ++k) sC += As[ty][k] * Bs[k][tx];
__syncthreads();
}
C[by * SUB_STRIDE * STRIDE + SUB_STRIDE * bx + ty * STRIDE + tx] = sC;
}
void mat_mul_custom(float*, float*, float*);
void mat_mul_custom(float* A, float* B, float* C) {
dim3 block(SUB_STRIDE, SUB_STRIDE);
dim3 grid(STRIDE / SUB_STRIDE, STRIDE / SUB_STRIDE);
mat_mul_custom_d<<<grid, block>>>(A, B, C);
}
#include <cublas_v2.h>
#define ALPHA 1
#define BETA 0
void mat_mul_blas(float*, float*, float*);
void mat_mul_blas(float* A, float* B, float* C) {
const float alpha = ALPHA, beta = BETA;
cublasHandle_t cublasH;
cublasCreate(&cublasH);
cublasSgemm(cublasH, CUBLAS_OP_N, CUBLAS_OP_N, STRIDE, STRIDE, STRIDE,
&alpha, A, STRIDE, B, STRIDE, &beta, C, STRIDE);
}
#include <cutensor.h>
#define N_MODES 2
#define K_ALIGNMENT 128
void mat_mul_tensor(float*, float*, float*);
void mat_mul_tensor(float* A, float* B, float* C) {
const float alpha = ALPHA, beta = BETA;
const int modeC[] = {'i', 'j'}, modeA[] = {'i', 'k'}, modeB[] = {'k', 'j'};
const int64_t extentC[] = {STRIDE, STRIDE}, extentA[] = {STRIDE, STRIDE},
extentB[] = {STRIDE, STRIDE};
cutensorHandle_t handle;
cutensorCreate(&handle);
cutensorTensorDescriptor_t descA, descB, descC;
cutensorCreateTensorDescriptor(handle, &descA, N_MODES, extentA, 0,
CUTENSOR_R_32F, K_ALIGNMENT);
cutensorCreateTensorDescriptor(handle, &descB, N_MODES, extentB, 0,
CUTENSOR_R_32F, K_ALIGNMENT);
cutensorCreateTensorDescriptor(handle, &descC, N_MODES, extentC, 0,
CUTENSOR_R_32F, K_ALIGNMENT);
cutensorOperationDescriptor_t desc;
cutensorCreateContraction(handle, &desc, descA, modeA, CUTENSOR_OP_IDENTITY,
descB, modeB, CUTENSOR_OP_IDENTITY, descC, modeC,
CUTENSOR_OP_IDENTITY, descC, modeC,
CUTENSOR_COMPUTE_DESC_32F);
cutensorPlanPreference_t planPref;
cutensorCreatePlanPreference(handle, &planPref, CUTENSOR_ALGO_DEFAULT,
CUTENSOR_JIT_MODE_NONE);
cutensorPlan_t plan;
cutensorCreatePlan(handle, &plan, desc, planPref, 0);
cutensorContract(handle, plan, (void*)&alpha, A, B, (void*)&beta, C, C, 0,
0, 0);
}
#include <assert.h>
#include <stdlib.h>
#define RAND_UPPER 2
void mat_mul(void (*mat_mul_impl)(float*, float*, float*), bool);
void mat_mul(void (*mat_mul_impl)(float*, float*, float*), bool column_major) {
float *A, *B, *C;
cudaMallocManaged(&A, sizeof(float) * STRIDE * STRIDE);
cudaMallocManaged(&B, sizeof(float) * STRIDE * STRIDE);
cudaMallocManaged(&C, sizeof(float) * STRIDE * STRIDE);
for (int i = 0; i < STRIDE; i++)
for (int k = 0; k < STRIDE; k++)
A[i * STRIDE + k] = rand() % RAND_UPPER;
for (int j = 0; j < STRIDE; j++)
for (int k = 0; k < STRIDE; k++)
B[k * STRIDE + j] = rand() % RAND_UPPER;
if (column_major)
mat_mul_impl(B, A, C);
else
mat_mul_impl(A, B, C);
cudaDeviceSynchronize();
for (int i = 0; i < STRIDE; i++)
for (int j = 0; j < STRIDE; j++) {
float res = 0;
for (int k = 0; k < STRIDE; k++)
res += A[i * STRIDE + k] * B[k * STRIDE + j];
assert(res == C[i * STRIDE + j]);
}
cudaFree(A);
cudaFree(B);
cudaFree(C);
}
#include <string.h>
#include <stdio.h>
int main(int argc, char** argv) {
if (argc == 2) {
if (!strcmp(argv[1], "custom")) {
printf("Test: mat_mul_custom\n");
mat_mul(mat_mul_custom, false);
return 0;
}
if (!strcmp(argv[1], "blas")) {
printf("Test: mat_mul_blas\n");
mat_mul(mat_mul_blas, true);
return 0;
}
if (!strcmp(argv[1], "tensor")) {
printf("Test: mat_mul_tensor\n");
mat_mul(mat_mul_tensor, true);
return 0;
}
}
printf("Usage: nvprof ./Mat_mul_test [custom|blas|tensor]\n");
}
Тесты
$ nvcc ./Mat_mul_test.cu \
-lcublas \
-L${CUTENSOR_ROOT}/lib/12 \
-lcutensor \
-I${CUTENSOR_ROOT}/include \
-o ./Mat_mul_test
$ nvprof ./Mat_mul_test custom
Avg Name
--- ----
15.352ms mat_mul_custom_d
Count Total Time Name
----- ---------- ----
35 4.641ms Gpu page fault groups
$ nvprof ./Mat_mul_test blas
4.628ms volta_sgemm_128x64_nn
28 3.627ms Gpu page fault groups
$ nvprof ./Mat_mul_test tensor
4.879ms volta_sgemm_128x64_nn
31 4.441ms Gpu page fault groups
Тестируемый графический процессор не имеет тензорных ядер в аппаратном обеспечении, но имеет аппаратное обеспечение для обработки тензорных инструкций. Тесты между mat_mul_blas и mat_mul_tensor показывают почти одинаковые показатели производительности. Соответствуют ли они ожиданиям? Что именно выполняет ядро volta_sgemm_128x64_nn? Тест mat_mul_blas намного быстрее, чем mat_mul_custom. Это просто потому, что ядро volta_sgemm_128x64_nn сильно оптимизировано, или главным образом потому, что они выполняются на разных типах ядер CUDA?
Ссылка
Поскольку ваше ядро содержит достаточное количество __syncthreads()
, вы можете попробовать использовать блоки меньшего размера (32x16), чтобы обеспечить 2 блока на SM и, следовательно, избежать недостаточного использования, когда большинство деформаций блока ждут на барьере. Тьюринг (sm_75) допускает максимум 32 деформации на SM.
Спасибо, @paleonix! Графические процессоры Nvidia быстро развиваются, и повсюду разбрасывается огромное количество информации. Это всего лишь мои собственные предположения, и я с нетерпением ищу доказательства. Позже я рассмотрю предложенные инструменты Nsight.
Хотя для этого сравнения это не может быть проблемой, я обычно предварительно загружаю данные на устройство после инициализации единой памяти на хосте, чтобы избежать измерения времени ошибок/передачи страницы (не знаю, как nvprof с этим справляется).
В вашем Quadro T1000 Mobile используется то же оборудование/кристалл (TU117), что и в GTX 1650, поэтому сообщение о тензорных инструкциях, на которое вы ссылаетесь, безусловно, применимо. Я просто не уверен, будет ли это ядро вообще использовать тензорные инструкции, поскольку тензорных ядер fp32 нет, есть только tf32.
@paleonix, я обновил количество ошибок страниц. Они довольно близки.
Да, ошибки случаются независимо от метода, поэтому я и написал, что при сравнении они могут не иметь значения. Но относительная производительность может отличаться при предварительной выборке. Или, может быть, nvprof просто запускает ядро несколько раз и проводит измерения в точке, где страницы уже находятся на графическом процессоре.
@paleonix, я попробовал блоки еще меньшего размера (16x16), и показатели производительности стали немного хуже. Позже попробую на 32х16.
Это компромисс между сокрытием задержки и повторным использованием данных, поэтому трудно предсказать, где находится золотая середина.
@paleonix, чтобы прояснить компромисс: чем больше деформаций на блок, тем лучше потенциально скрывается задержка, но на самом деле тем хуже эффективный параллелизм из-за барьера синхронизации. ДРУГОЕ, чем больше разделяемой памяти, тем лучше локальность данных. Ты это имел в виду?
Чем больше блоков на SM, тем лучше скрывается задержка при наличии синхронизации блоков. Чем больше блок (а вместе с ним и общая память), тем лучше повторное использование данных.
Соответствуют ли они ожиданиям?
Да, ожидается, что кублы будут выполнять операции умножения матриц быстрее, чем код ядра, который вы показали. Также очевидно, что дизайнеры кутсенсоров используют примерно тот же подход, что и кублы - на самом деле очевидно, что они вызывают кублы аналогично вашему вызову кубласов. (продолжительность в основном такая же, и имя ядра такое же)
Что именно выполняет ядро volta_sgemm_128x64_nn?
Это высокооптимизированное ядро gemm (матрица-матрица умножения) для данных FP32 (именно поэтому вы видите sgemm
), написанное дизайнерами Cublas как часть библиотеки Cublas с закрытым исходным кодом. Поэтому я не могу дать подробное описание. Однако написание высокооптимизированного ядра Gemm — нетривиальная задача. Эта статья может дать вам некоторое представление о сложности по сравнению с кодом вашего ядра, который мы теперь можем называть «наивным» кодом умножения матриц с общей памятью.
Это просто потому, что ядро volta_sgemm_128x64_nn сильно оптимизировано, или главным образом потому, что они выполняются на разных типах ядер CUDA?
Это потому, что он очень оптимизирован. Они не используют разные ядра (функциональные блоки в СМ). Вы можете получить некоторые доказательства/доказательства этого, используя бинарные утилиты CUDA. Если вы выгрузите код SASS в каждом случае, вы обнаружите, что и ваше ядро, и ядро cublas sgemm используют инструкции FFMA SASS для выполнения арифметики умножения матрицы.
Как уже упоминалось, в настоящее время не существует графических процессоров CUDA, которые обеспечивают пути вычисления тензорных ядер для входных данных FP32, поэтому на данный момент мы можем полностью отказаться от этого понятия; это не то, что здесь происходит, и тот факт, что и ваш вызов cublas, и ваш вызов Cutensor вызывают нетензорную sgemm-подпрограмму cublas, является еще одним свидетельством этого.
Спасибо, @Robert Crovella! SM_7x состоит из 64 (FP32) и 32 (FP64) и т. д. Почему мой T1000 сообщает только о 64 ядрах на SM? Были ли отключены остальные ядра на кристалле графического процессора при отправке?
@sof Прочтите сноску: «2 ядра FP64 для арифметических операций двойной точности для устройств с вычислительными возможностями 7.5». Лишь немногие крупные серверные ускорители имеют хорошие вычислительные возможности FP64, в данном случае V100.
@paleonix, ну, на самом деле там указано: 64 (FP32), 32 (FP64 для серверов), 64 (INT64), 8 (тензор), 16 (SFU) и 4 (WS).
@палеоникс. Я прочитала это, где частично есть ответ. (Цитата: Термин «ядро» в CUDA обычно используется для обозначения блока SP. И то, что SM содержит, например, 128 ядер CUDA (т.е. блоков SP), не означает, что он также содержит 128 блоков DP, или 128 блоков LD/ST или определенное количество функциональных блоков любого другого типа. Количество функциональных блоков в SM может варьироваться и зависит от типа функционального блока.)
@sof Да, «ядра CUDA» — это маркетинговый термин, который описывает только количество модулей FP32. Я не осознавал, что это стало для вас источником замешательства. Что еще неясно?
@paleonix, спасибо за твою постоянную помощь. Существует множество Nvidia/CUDA/маркетинговых терминов. В частности, «ядро» вызвало у меня большое замешательство. Поскольку характеристики/руководство графического процессора (если я смогу найти) просто предоставляют мне информацию о недостатке подробностей о ядрах cuda, и мы не можем запросить устройство, например. типы ядер и их количество, иногда я с трудом могу определить некоторые показатели производительности без этих конкретных аппаратных средств. OTH, просто из любопытства, я хотел бы знать, изготовлены ли кристаллы графического процессора гибко, просто включая/отключая некоторые функциональные блоки для дифференциации рынка.
@sof Обычно я думаю, что целые SM сливаются, а не части SM.
Как вы сами написали, тензорных ядер float32 нет, так какие еще типы ядер CUDA должны быть? Вы можете использовать такие инструменты, как страница исходного кода Nsight Compute или
cuobjdump
, чтобы просмотреть фактические используемые инструкции SASS.