#cuda #fortran
Вопрос:
Я выполняю базовое умножение матрицы с использованием CUDA Fortran и C без каких-либо оптимизаций. И Fortran, и C делают одно и то же, но время выполнения для Fortran медленнее.
Ядро C
#define idx(x,y,z) x*y z
__global__ void matrixMultiply(double *d_mat, double *d_matT, double *d_matSym) {
//Global inidices
int tx = blockIdx.y * blockDim.y threadIdx.y;
int ty = blockIdx.x * blockDim.x threadIdx.x;
int k;
if (tx < SIZE amp;amp; ty < SIZE) {
double accum = 0.0;
//Accumulation for (tx,ty) position
for (k=0; k<SIZE; k) {
accum = d_mat[idx(tx,SIZE,k)] * d_matT[idx(k,SIZE,ty)];
}
d_matSym[idx(tx,SIZE,ty)] = accum;
}
}
//Call
dim3 grid_dim(SIZE/32, SIZE/32, 1);
dim3 blk_dim(32, 32, 1);
matrixMultiply<<<grid_dim, blk_dim>>>(d_mat, d_matT, d_matSym);
cudaDeviceSynchronize();
Ядро Фортрана
attributes(global) subroutine matrixMultiply(d_mat, d_matT, d_matSym)
integer :: tx, ty, k
real*8 :: accum
real*8, dimension(:,:) :: d_mat, d_matT, d_matSym
tx = threadIdx%x (blockIdx%x - 1) * blockDim%x
ty = threadIdx%y (blockIdx%y - 1) * blockDim%y
if (tx <= SIZE_ .and. ty <=SIZE_) then
accum = 0.0
do k=1, SIZE_
accum = accum d_mat(tx,k) * d_matT(k,ty)
end do
d_matSym(tx,ty) = accum
end if
end subroutine matrixMultiply
!Call
type(dim3) :: grid_dim, blk_dim
grid_dim = dim3(SIZE_/32, SIZE_/32, 1)
blk_dim = dim3(32, 32, 1)
call matrixMultiply<<<grid_dim, blk_dim>>>(d_mat, d_matT, d_matSym)
err = cudaDeviceSynchronize()
Разница в том, что C использует массив 1D, тогда как Fortran использует 2D. Но это не должно быть проблемой, так как под памятью будет непрерывная память.
Если это доступ к памяти, то в обоих случаях цикл K обращается к одной матрице последовательно, а другой доступ увеличивается по РАЗМЕРУ.
И то, и другое дает одинаковые результаты.
Для матрицы 16384 x 16384, C : 5,4 сек Фортран : 6,3 сек
Графический процессор: Tesla V100 32 ГБ
Я не уверен, что я здесь делаю не так?
Комментарии:
1. Почему вы считаете, что делаете что-то не так? Почему код, написанный на разных языках и скомпилированный с помощью разных компиляторов, должен обеспечивать одинаковую производительность? Разберите получившуюся наглость и поищите отличия. Возможно, ваши неописанные настройки компилятора в обоих случаях отличаются….
2. @talonmies Я согласен с вами, что здесь, возможно, нет ничего плохого. Но этот вопрос справедлив для того факта, что Fortran, как правило, во многих случаях работает так же быстро, как или быстрее, чем C, хотя разница часто минимальна, как и здесь. @abhishekpurandare1297 : У меня нет опыта работы с Cuda Fortran. Но если вы не получите ответа на свой вопрос, предполагая , что ваш компилятор есть
nvfortran
, пожалуйста, задайте свой вопрос также на форуме NVIDIA Fortran (и опубликуйте свой ответ здесь тоже): forums.developer.nvidia.com/c/accelerated-computing/…3. @ abhishekpurandare1297 Вы несколько раз просчитывали примеры и усредняли результаты?
4. «Фортран использует 2D» : У меня есть опыт, что большее количество измерений массива часто замедляет код (возможно, из-за большего количества арифметики для вычисления адресов).
5. Просто из любопытства, прикрепление
contiguous
атрибута к этой строке «реальный*8, измерение(:,:) :: d_mat, d_matT, d_matSym» может иметь какой-то эффект? (и я также думаю, что форум nvidia может быть полезен)
Ответ №1:
Прежде всего, я предлагаю, чтобы вопросы о производительности включали полные коды. Как правило, мне нужно уметь что-то запускать, и вы можете сэкономить мне время на наборе текста. Конечно, вы можете оставить все как есть. Конечно, я, наверное, смогу понять, что это такое. Но у меня меньше шансов помочь вам таким образом, и я подозреваю, что я не одинок в этом мнении. Мой совет: Сделайте так, чтобы другим было легко вам помогать. Ниже я привел примеры того, что было бы полезно.
Переходим к вопросу:
Разница в том, что C использует массив 1D, тогда как Fortran использует 2D. Но это не должно быть проблемой, так как под памятью будет непрерывная память.
TL;DR: Ваше утверждение («это не должно быть проблемой») , очевидно, не может быть поддержано. Разница между распределением 1D и распределением 2D имеет значение не только с точки зрения хранения, но и с точки зрения расчета индекса. Если вы чувствительны к длине этого ответа, перейдите к примечанию D внизу этого поста.
Подробные сведения:
Когда у нас есть такая петля, как эта:
for (k=0; k<SIZE; k) {
accum = d_mat[idx(tx,SIZE,k)] * d_matT[idx(k,SIZE,ty)];
}
Если компилятор сможет обнаружить, что вычисления индексации могут быть просто «увеличены» по мере выполнения цикла, это будет довольно просто. ОТО, если индексирование должно быть решено с учетом 2D-вычисления (например row*width column
), на которое ранее ссылались на каждой итерации цикла, это будет сложнее. Важным фактором здесь также является то, может ли массив width
использоваться/использоваться компилятором во время компиляции.
Похоже, именно это и происходит, и я предполагаю, что это разница во времени. Мы рассмотрим 2 доказательства этого.
- Анализ SASS
Если мы сравним SASS между реализациями, мы увидим некоторые важные различия. Для этого нам понадобятся полные примеры:
CUDA Фортран:
$ cat t11.cuf
module mmk
USE cudafor
!
! Definition of symbols for real types (RP)
!
IMPLICIT NONE
!
INTEGER, PARAMETER :: SP = SELECTED_REAL_KIND(6, 37) ! REAL32
INTEGER, PARAMETER :: DP = SELECTED_REAL_KIND(15, 307) ! REAL64
INTEGER, PARAMETER :: SIZE_ = 16384
!
Contains
attributes(global) subroutine matrixMultiply(d_mat, d_matT, d_matSym)
integer :: tx, ty, k
REAL(DP) :: accum
REAL(DP), dimension(:,:) :: d_mat, d_matT, d_matSym
tx = threadIdx%x (blockIdx%x - 1) * blockDim%x
ty = threadIdx%y (blockIdx%y - 1) * blockDim%y
if (tx <= SIZE_ .and. ty <=SIZE_) then
accum = 0.0
do k=1, SIZE_
accum = accum d_mat(tx,k) * d_matT(k,ty)
end do
d_matSym(tx,ty) = accum
end if
end subroutine matrixMultiply
end module mmk
PROGRAM Test
!
! This is the main program for Test
!
USE cudafor
USE mmk
!
IMPLICIT NONE
!
REAL(DP), ALLOCATABLE, DEVICE, DIMENSION(:,:) :: d_mat, d_matT, d_matSym
!
INTEGER :: err, i1, i2
type(dim3) :: grid_dim, blk_dim
!
!
! Allocate storage for the arrays
!
Allocate(d_mat(SIZE_,SIZE_),d_matT(SIZE_,SIZE_),d_matSym(SIZE_,SIZE_))
!
! invoke the kernel
!
!Call
grid_dim = dim3(SIZE_/32, SIZE_/32, 1)
blk_dim = dim3(32, 32, 1)
call matrixMultiply<<<grid_dim, blk_dim>>>(d_mat, d_matT, d_matSym)
call matrixMultiply<<<grid_dim, blk_dim>>>(d_mat, d_matT, d_matSym)
err = cudaDeviceSynchronize()
!
! Free storage for the arrays
!
Deallocate(d_mat,d_matT,d_matSym)
!
END PROGRAM Test
$ nvfortran t11.cuf -o t11
$ nvprof ./t11
==38508== NVPROF is profiling process 38508, command: ./t11
==38508== Profiling application: ./t11
==38508== Profiling result:
Type Time(%) Time Calls Avg Min Max Name
GPU activities: 100.00% 12.7168s 2 6.35838s 6.35229s 6.36447s mmk_matrixmultiply_
0.00% 9.2480us 6 1.5410us 1.3120us 2.1760us [CUDA memcpy HtoD]
API calls: 97.08% 12.7260s 9 1.41400s 10.596us 6.36561s cudaFree
2.85% 374.24ms 9 41.583ms 3.5780us 369.55ms cudaMalloc
0.03% 4.5190ms 4 1.1297ms 1.0551ms 1.2608ms cuDeviceTotalMem
0.02% 2.5060ms 404 6.2020us 111ns 1.2057ms cuDeviceGetAttribute
0.01% 1.1501ms 4 287.52us 33.014us 1.0465ms cuDeviceGetName
0.00% 120.00us 6 20.000us 5.1530us 62.523us cudaMemcpy
0.00% 99.593us 2 49.796us 44.275us 55.318us cudaLaunchKernel
0.00% 43.414us 1 43.414us 43.414us 43.414us cudaDeviceSynchronize
0.00% 11.563us 4 2.8900us 1.4400us 5.5870us cuDeviceGetPCIBusId
0.00% 1.7030us 8 212ns 124ns 580ns cuDeviceGet
0.00% 901ns 3 300ns 163ns 534ns cuDeviceGetCount
0.00% 855ns 4 213ns 183ns 263ns cuDeviceGetUuid
$
NVIDIA HPC SDK, 2021.2, Tesla V100
Мы видим, что продолжительность выполнения составляет около 6,3 секунды, в соответствии с вашим отчетом.
CUDA C :
$ cat t12.cu
#define idx(x,y,z) x*y z
const int SIZE = 16384;
__global__ void matrixMultiply(double *d_mat, double *d_matT, double *d_matSym) {
//Global inidices
int tx = blockIdx.y * blockDim.y threadIdx.y;
int ty = blockIdx.x * blockDim.x threadIdx.x;
int k;
if (tx < SIZE amp;amp; ty < SIZE) {
double accum = 0.0;
//Accumulation for (tx,ty) position
for (k=0; k<SIZE; k) {
accum = d_mat[idx(tx,SIZE,k)] * d_matT[idx(k,SIZE,ty)];
}
d_matSym[idx(tx,SIZE,ty)] = accum;
}
}
int main(){
//Call
dim3 grid_dim(SIZE/32, SIZE/32, 1);
dim3 blk_dim(32, 32, 1);
double *d_mat, *d_matT, *d_matSym;
cudaMalloc(amp;d_mat, SIZE*SIZE*sizeof(double));
cudaMalloc(amp;d_matT, SIZE*SIZE*sizeof(double));
cudaMalloc(amp;d_matSym, SIZE*SIZE*sizeof(double));
matrixMultiply<<<grid_dim, blk_dim>>>(d_mat, d_matT, d_matSym);
matrixMultiply<<<grid_dim, blk_dim>>>(d_mat, d_matT, d_matSym);
cudaDeviceSynchronize();
}
$ nvcc -o t12 t12.cu -arch=sm_70
$ nvprof ./t12
==40364== NVPROF is profiling process 40364, command: ./t12
==40364== Profiling application: ./t12
==40364== Profiling result:
Type Time(%) Time Calls Avg Min Max Name
GPU activities: 100.00% 11.0379s 2 5.51893s 5.50503s 5.53282s matrixMultiply(double*, double*, double*)
API calls: 97.80% 11.0379s 1 11.0379s 11.0379s 11.0379s cudaDeviceSynchronize
2.15% 242.74ms 3 80.914ms 1.9747ms 238.78ms cudaMalloc
0.04% 4.3105ms 4 1.0776ms 1.0728ms 1.0890ms cuDeviceTotalMem
0.01% 1.4584ms 404 3.6090us 110ns 163.21us cuDeviceGetAttribute
0.00% 142.28us 4 35.571us 32.475us 41.980us cuDeviceGetName
0.00% 51.695us 2 25.847us 7.0790us 44.616us cudaLaunchKernel
0.00% 11.198us 4 2.7990us 1.6260us 5.5950us cuDeviceGetPCIBusId
0.00% 1.7930us 3 597ns 149ns 1.2200us cuDeviceGetCount
0.00% 1.6590us 8 207ns 120ns 704ns cuDeviceGet
0.00% 733ns 4 183ns 161ns 215ns cuDeviceGetUuid
$
CUDA 11.2, та же машина V100
Снова мы видим время работы ядра ~5,5 секунд, совпадающее с вашим отчетом. Теперь давайте сравним НАХАЛЬСТВО. Мы используем cubojdump
утилиту для его извлечения, и в обоих случаях очевидно, что компилятор каким-то образом разворачивает цикл ядра. Ключевая операция DFMA
-сложение с двойной точностью с плавким умножением , которое вызывается один раз за итерацию цикла, для вычисления одного продукта (и сохранения текущей суммы). Поэтому я буду извлекать только короткую последовательность операций DFMA в каждом случае, в середине развернутого цикла, чтобы получить представление о «стоимости» и «накладных расходах»на каждом флопе.
CUDA Фортран:
/*1670*/ LDG.E.64.SYS R18, [R12 0x1b8] ; /* 0x0001b8000c127381 */
/* 0x000ea200001eeb00 */
/*1680*/ IADD3 R20, P0, R16, R29, RZ ; /* 0x0000001d10147210 */
/* 0x001fc60007f1e0ff */
/*1690*/ LDG.E.64.SYS R22, [R12 0x1b0] ; /* 0x0001b0000c167381 */
/* 0x000e6400001eeb00 */
/*16a0*/ IMAD.X R21, R17, 0x1, R27, P0 ; /* 0x0000000111157824 */
/* 0x000fe400000e061b */
/*16b0*/ LDG.E.64.SYS R24, [R16] ; /* 0x0000000010187381 */
/* 0x00006c00001eeb00 */
/*16c0*/ LDG.E.64.SYS R16, [R20] ; /* 0x0000000014107381 */
/* 0x001ea200001eeb00 */
/*16d0*/ DFMA R22, R24, R22, R14 ; /* 0x000000161816722b */
/* 0x002084000000000e */
/*16e0*/ IADD3 R14, P0, R20, R29, RZ ; /* 0x0000001d140e7210 */
/* 0x001fca0007f1e0ff */
/*16f0*/ IMAD.X R15, R21, 0x1, R27, P0 ; /* 0x00000001150f7824 */
/* 0x000fe200000e061b */
/*1700*/ DFMA R16, R16, R18, R22 ; /* 0x000000121010722b */
Описанная выше последовательность повторяется в области развернутого цикла. Мы отмечаем, что существует 2 DFMA
операции, 4 LDG
операции (имеет смысл — по две на умножение), а остальные инструкции (4) предположительно представляют собой обновления индексации по мере прохождения цикла через его итерации.
CUDA C :
/*02d0*/ LDG.E.64.SYS R16, [R6 0x40000] ; /* 0x0400000006107381 */
/* 0x001f2800001eeb00 */
/*02e0*/ LDG.E.64.SYS R24, [R4 0x10] ; /* 0x0000100004187381 */
/* 0x000f2200001eeb00 */
/*02f0*/ DFMA R10, R20, R10, R22 ; /* 0x0000000a140a722b */
/* 0x0200860000000016 */
/*0300*/ LDG.E.64.SYS R22, [R6 0x60000] ; /* 0x0600000006167381 */
/* 0x001f6800001eeb00 */
/*0310*/ LDG.E.64.SYS R20, [R4 0x18] ; /* 0x0000180004147381 */
/* 0x000f6200001eeb00 */
/*0320*/ DFMA R14, R8, R14, R10 ; /* 0x0000000e080e722b */
И снова эта последовательность повторяется. Мы видим 2 DFMA
и 4 LDG
инструкции, но никаких других инструкций в повторяющейся последовательности. Отметим, что LDG
инструкции, похоже, взаимозачетов, которые могут быть включены во время компиляции в инструкцию, и эти смещения исключить необходимость каких-либо дополнительных инструкций, ведь они только «приращение» ранее вычисленных смещений (Отметим также, что «с шагом» по-видимому, на уровне столбца смещения в одном случае, и построчный смещения в друга, как можно было бы ожидать, чтобы забрать умножение операндов в ядре петли.)
- Рефакторинг
Можем ли мы реорганизовать код Fortran, чтобы он работал так же, как код C ? ДА:
$ cat t11a.cuf
module mmk
USE cudafor
!
! Definition of symbols for real types (RP)
!
IMPLICIT NONE
!
INTEGER, PARAMETER :: SP = SELECTED_REAL_KIND(6, 37) ! REAL32
INTEGER, PARAMETER :: DP = SELECTED_REAL_KIND(15, 307) ! REAL64
INTEGER, PARAMETER :: SIZE_ = 16384
!
Contains
attributes(global) subroutine matrixMultiply(d_mat, d_matT, d_matSym)
integer :: tx, ty, k
REAL(DP) :: accum
REAL(DP), dimension(:) :: d_mat, d_matT, d_matSym
tx = threadIdx%x (blockIdx%x - 1) * blockDim%x
ty = threadIdx%y (blockIdx%y - 1) * blockDim%y
if (tx <= SIZE_ .and. ty <=SIZE_) then
accum = 0.0
do k=1, SIZE_
accum = accum d_mat((ty-1)*SIZE_ k) * d_matT((k-1)*SIZE_ tx)
end do
d_matSym((ty-1)*SIZE_ tx) = accum
end if
end subroutine matrixMultiply
end module mmk
PROGRAM Test
!
! This is the main program for Test
!
USE cudafor
USE mmk
!
IMPLICIT NONE
!
REAL(DP), ALLOCATABLE, DEVICE, DIMENSION(:) :: d_mat, d_matT, d_matSym
!
INTEGER :: err, i1, i2
type(dim3) :: grid_dim, blk_dim
!
! Allocate storage for the arrays
!
Allocate(d_mat(SIZE_*SIZE_),d_matT(SIZE_*SIZE_),d_matSym(SIZE_*SIZE_))
!
! invoke the kernel
!
!Call
grid_dim = dim3(SIZE_/32, SIZE_/32, 1)
blk_dim = dim3(32, 32, 1)
call matrixMultiply<<<grid_dim, blk_dim>>>(d_mat, d_matT, d_matSym)
call matrixMultiply<<<grid_dim, blk_dim>>>(d_mat, d_matT, d_matSym)
err = cudaDeviceSynchronize()
!
! Free storage for the arrays
!
Deallocate(d_mat,d_matT,d_matSym)
!
END PROGRAM Test
$ nvfortran t11a.cuf -o t11a
$ nvprof ./t11a
==45544== NVPROF is profiling process 45544, command: ./t11a
==45544== Profiling application: ./t11a
==45544== Profiling result:
Type Time(%) Time Calls Avg Min Max Name
GPU activities: 100.00% 10.8169s 2 5.40847s 5.39118s 5.42576s mmk_matrixmultiply_
0.00% 9.2160us 6 1.5360us 1.3120us 2.1440us [CUDA memcpy HtoD]
API calls: 96.72% 10.8240s 9 1.20266s 12.102us 5.42594s cudaFree
3.22% 360.34ms 9 40.038ms 3.2760us 355.64ms cudaMalloc
0.04% 4.3488ms 4 1.0872ms 1.0598ms 1.1593ms cuDeviceTotalMem
0.02% 2.3633ms 404 5.8490us 111ns 869.22us cuDeviceGetAttribute
0.00% 144.50us 4 36.125us 33.254us 41.317us cuDeviceGetName
0.00% 106.43us 6 17.737us 4.7910us 51.453us cudaMemcpy
0.00% 101.46us 2 50.732us 44.479us 56.985us cudaLaunchKernel
0.00% 20.926us 1 20.926us 20.926us 20.926us cudaDeviceSynchronize
0.00% 7.2850us 4 1.8210us 491ns 4.6210us cuDeviceGetPCIBusId
0.00% 1.7650us 8 220ns 118ns 672ns cuDeviceGet
0.00% 926ns 4 231ns 218ns 264ns cuDeviceGetUuid
0.00% 614ns 3 204ns 130ns 310ns cuDeviceGetCount
$
И мы видим, что производительность примерно такая же, как у версии CUDA C .
Примечания:
О. Я опустил анализ SASS для окончательного варианта. Упражнение оставлено на усмотрение читателя. Однако развернутая часть цикла представляет собой повторяющуюся последовательность LDG-LDG-DFMA, как это видно в версии CUDA C .
Б. У вас могут возникнуть два вопроса: 1. Почему это происходит именно так? Я пытался ответить на этот вопрос. 2. Должно ли это быть так/должно ли это быть так? Я не ответил на этот вопрос, я полагаю, что это комбинация некоторых утверждений Fortran, которые я не хочу делать, а также вопрос для инженеров-компиляторов. Если вы считаете, что 2D-реализация должна дублировать 1D-реализацию, я предлагаю сообщить об ошибке.
C. Я немного поскреб в затылке, прежде чем заметил, что вы сделали это в случае C , но не в случае fortran:
int tx = blockIdx.y * blockDim.y threadIdx.y;
^ ^ ^ ^
D. Одна из причин, по которой я считаю, что это несоответствие не поддается тривиальному разрешению во время компиляции, заключается в том, что обработка 2D-массива компилятором в CUDA Fortran (независимо от того, нужна она или нет), по-видимому, связана с созданием пакета метаданных во время выполнения, который включает ширину массива, возможно, среди другой информации. Этот пакет метаданных передается на устройство отдельно от полезной нагрузки массива данных-об этом можно догадаться из приведенного выше вывода профилировщика или путем запуска, --print-gpu-trace
чтобы увидеть его более четко. В вашем случае CUDA C , по сути, ширина массива ( SIZE
) известна во время компиляции. В случае Фортрана это не так (или, в качестве альтернативы, если это так, компилятор, похоже, не распространяет эти знания через генерацию кода ядра). Вы можете видеть, что «улучшенный» режим адресации выше предполагает смещения, которые известны во время компиляции. Можно ли решить это «должно» или нет при более тщательном обращении с Фортраном, я не могу сказать. Однако я бы ожидал, что какое-то contiguous
объявление/украшение само по себе недостаточно для устранения этого несоответствия для компилятора. Одно это объявление не устранит необходимость в вычислениях индексации во время выполнения на лету, основанных на ширине массива.
Комментарии:
1. Когда компилятор поддерживает ОБЪЕДИНЕНИЕ/СОПОСТАВЛЕНИЕ, обычно может быть удобно работать с матрицей как с 2D. Но для операций ЭЛЕМЕНТАРНОГО типа обратитесь к нему как к 1D массиву/вектору.
2. Спасибо, что нашли для этого время. Я действительно рад, что у вас все еще есть энтузиазм по поводу такой глубины анализа. Обычно я этого не делаю. Как обычно, многие из новых функций Fortran (указатели, интеллектуальное индексирование массивов, нарезка, широковещательная передача и арифметика и т. Д.) Имеют удивительно жадные реализации, которые ослабили большинство традиционных преимуществ производительности устаревшего Fortran по сравнению с «новыми» языками….
3. Проблема заключалась в том, что компилятор не знал размер массива. Я все еще новичок в Fortran, поэтому я ошибочно проигнорировал определение размера массивов параметров ядра.
real*8, dimension(SIZE_,SIZE_) :: d_mat, d_matT, d_matSym
декларация устранила проблему. Я ценю ваше время и глубокое понимание!