Сравнение производительности кастомного ядра cuda, cublas и Cutensor

Я провел следующие тесты 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?

Ссылка

Как вы сами написали, тензорных ядер float32 нет, так какие еще типы ядер CUDA должны быть? Вы можете использовать такие инструменты, как страница исходного кода Nsight Compute или cuobjdump, чтобы просмотреть фактические используемые инструкции SASS.

paleonix 04.07.2024 15:44

Поскольку ваше ядро ​​содержит достаточное количество __syncthreads(), вы можете попробовать использовать блоки меньшего размера (32x16), чтобы обеспечить 2 блока на SM и, следовательно, избежать недостаточного использования, когда большинство деформаций блока ждут на барьере. Тьюринг (sm_75) допускает максимум 32 деформации на SM.

paleonix 04.07.2024 15:48

Спасибо, @paleonix! Графические процессоры Nvidia быстро развиваются, и повсюду разбрасывается огромное количество информации. Это всего лишь мои собственные предположения, и я с нетерпением ищу доказательства. Позже я рассмотрю предложенные инструменты Nsight.

sof 04.07.2024 15:52

Хотя для этого сравнения это не может быть проблемой, я обычно предварительно загружаю данные на устройство после инициализации единой памяти на хосте, чтобы избежать измерения времени ошибок/передачи страницы (не знаю, как nvprof с этим справляется).

paleonix 04.07.2024 16:00

В вашем Quadro T1000 Mobile используется то же оборудование/кристалл (TU117), что и в GTX 1650, поэтому сообщение о тензорных инструкциях, на которое вы ссылаетесь, безусловно, применимо. Я просто не уверен, будет ли это ядро ​​вообще использовать тензорные инструкции, поскольку тензорных ядер fp32 нет, есть только tf32.

paleonix 04.07.2024 16:12

@paleonix, я обновил количество ошибок страниц. Они довольно близки.

sof 04.07.2024 16:48

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

paleonix 04.07.2024 17:02

@paleonix, я попробовал блоки еще меньшего размера (16x16), и показатели производительности стали немного хуже. Позже попробую на 32х16.

sof 04.07.2024 17:37

Это компромисс между сокрытием задержки и повторным использованием данных, поэтому трудно предсказать, где находится золотая середина.

paleonix 04.07.2024 17:43

@paleonix, чтобы прояснить компромисс: чем больше деформаций на блок, тем лучше потенциально скрывается задержка, но на самом деле тем хуже эффективный параллелизм из-за барьера синхронизации. ДРУГОЕ, чем больше разделяемой памяти, тем лучше локальность данных. Ты это имел в виду?

sof 06.07.2024 09:28

Чем больше блоков на SM, тем лучше скрывается задержка при наличии синхронизации блоков. Чем больше блок (а вместе с ним и общая память), тем лучше повторное использование данных.

paleonix 06.07.2024 09:30
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
11
134
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Соответствуют ли они ожиданиям?

Да, ожидается, что кублы будут выполнять операции умножения матриц быстрее, чем код ядра, который вы показали. Также очевидно, что дизайнеры кутсенсоров используют примерно тот же подход, что и кублы - на самом деле очевидно, что они вызывают кублы аналогично вашему вызову кубласов. (продолжительность в основном такая же, и имя ядра такое же)

Что именно выполняет ядро ​​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 06.07.2024 05:39

@sof Прочтите сноску: «2 ядра FP64 для арифметических операций двойной точности для устройств с вычислительными возможностями 7.5». Лишь немногие крупные серверные ускорители имеют хорошие вычислительные возможности FP64, в данном случае V100.

paleonix 06.07.2024 09:36

@paleonix, ну, на самом деле там указано: 64 (FP32), 32 (FP64 для серверов), 64 (INT64), 8 (тензор), 16 (SFU) и 4 (WS).

sof 06.07.2024 10:04

@палеоникс. Я прочитала это, где частично есть ответ. (Цитата: Термин «ядро» в CUDA обычно используется для обозначения блока SP. И то, что SM содержит, например, 128 ядер CUDA (т.е. блоков SP), не означает, что он также содержит 128 блоков DP, или 128 блоков LD/ST или определенное количество функциональных блоков любого другого типа. Количество функциональных блоков в SM может варьироваться и зависит от типа функционального блока.)

sof 06.07.2024 10:11

@sof Да, «ядра CUDA» — это маркетинговый термин, который описывает только количество модулей FP32. Я не осознавал, что это стало для вас источником замешательства. Что еще неясно?

paleonix 06.07.2024 10:31

@paleonix, спасибо за твою постоянную помощь. Существует множество Nvidia/CUDA/маркетинговых терминов. В частности, «ядро» вызвало у меня большое замешательство. Поскольку характеристики/руководство графического процессора (если я смогу найти) просто предоставляют мне информацию о недостатке подробностей о ядрах cuda, и мы не можем запросить устройство, например. типы ядер и их количество, иногда я с трудом могу определить некоторые показатели производительности без этих конкретных аппаратных средств. OTH, просто из любопытства, я хотел бы знать, изготовлены ли кристаллы графического процессора гибко, просто включая/отключая некоторые функциональные блоки для дифференциации рынка.

sof 06.07.2024 11:05

@sof Обычно я думаю, что целые SM сливаются, а не части SM.

paleonix 06.07.2024 12:14

Другие вопросы по теме